Swagger2接口参数实体类字段自定义显示
使用swagger文档的时候发现实体类参数每次都是全部显示出来,导致和前端对接很麻烦,就产生了想要自定义的实体类参数的想法,于是在百度了很久,找到了flymoringbird大神的文章Swagger2 自定义注解 :解决一个简单的model类 适用于controller的多个方法,参考了一下发现只能实现body传参,query传参还是没有办法实现,于是又开始了百度,找到了x-easy大神swagger2 同一个实体用在多个不同的controller接口展示不同的字段的文章,最终实现了功能。
话不多说,直接上代码:
pom.xml
这里只贴出了最重要的类,其他的还请自行查找
<properties>
<swagger.version>2.9.2</swagger.version>
<swagger-models.version>1.6.2</swagger-models.version>
<transmittable.version>2.12.1</transmittable.version>
</properties>
<!--swagger2-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.version}</version>
<exclusions>
<exclusion>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 防止进入swagger页面报类型转换错误For input string: "",排除swagger-models引用, ,手动增加1.6.2版本 -->
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
<version>${swagger-models.version}</version>
</dependency>
<!--swagger2-ui-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger.version}</version>
</dependency>
<!--注意这里用的是alibaba 的 javassist 的包最好-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>${transmittable.version}</version>
</dependency>
自定义注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author 李永杰
* @date 2021/10/11
* @description swagger ApiModel参数排除字段
*/
@Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiExclude {
/**
* 忽略的属性值
*/
String[] value() default {};
}
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author 李永杰
* @date 2021/10/11
* @description swagger ApiModel参数包含字段
*/
@Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiInclude {
/**
* 需要的属性值
*/
String[] value() default {};
}
测试参数实体类
/**
* @author 李永杰
* @date 2021/11/4
* @description
*/
@Data
@ApiModel("测试参数")
public class Demo {
@ApiModelProperty("参数A")
private String a;
@ApiModelProperty("参数B")
private String b;
@ApiModelProperty("参数C")
private String c;
@ApiModelProperty("参数D")
private String d;
}
静态常量
/**
* swagger 自定义model 前缀
*/
public static String MY_MODEL_NAME_PRE="SWAGGER";
最重要的两个plugin类
下面这个是解决body传参的
import cn.hutool.core.util.IdUtil;
import com.alibaba.ttl.threadpool.agent.internal.javassist.*;
import com.alibaba.ttl.threadpool.agent.internal.javassist.bytecode.AnnotationsAttribute;
import com.alibaba.ttl.threadpool.agent.internal.javassist.bytecode.ConstPool;
import com.alibaba.ttl.threadpool.agent.internal.javassist.bytecode.annotation.Annotation;
import com.alibaba.ttl.threadpool.agent.internal.javassist.bytecode.annotation.StringMemberValue;
import com.fasterxml.classmate.TypeResolver;
import com.google.common.base.Optional;
import com.ruoyi.common.annotation.ApiExclude;
import com.ruoyi.common.annotation.ApiInclude;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.utils.StringUtils;
import io.swagger.annotations.ApiModelProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ResolvedMethodParameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.ParameterBuilderPlugin;
import springfox.documentation.spi.service.contexts.ParameterContext;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author 李永杰
* @date 2021/11/05
* @description 读取自定义注解的dto属性并动态生成model, 只支持body参数(注意这里用的是alibaba的 javassist 的包最好)
*/
@Order
@Configuration
public class SwaggerBodyParameterPlugin implements ParameterBuilderPlugin {
private static final Logger logger = LoggerFactory.getLogger(SwaggerBodyParameterPlugin.class);
@Autowired
private TypeResolver typeResolver;
@Override
public void apply(ParameterContext parameterContext) {
ResolvedMethodParameter methodParameter = parameterContext.resolvedMethodParameter();
Class<?> originClass = parameterContext.resolvedMethodParameter().getParameterType().getErasedType();
// 排除属性
ApiExclude apiExclude = null;
@SuppressWarnings("Guava")
Optional<ApiExclude> apiExcludeOptional = methodParameter.findAnnotation(ApiExclude.class);
if (apiExcludeOptional.isPresent()) {
apiExclude = apiExcludeOptional.get();
}
// 需要属性
ApiInclude apiInclude = null;
@SuppressWarnings("Guava")
Optional<ApiInclude> apiIncludeOptional = methodParameter.findAnnotation(ApiInclude.class);
if (apiIncludeOptional.isPresent()) {
apiInclude = apiIncludeOptional.get();
}
if (null != apiExclude || null != apiInclude) {
//model名称,注意名称不能一样
String name = Constants.MY_MODEL_NAME_PRE.concat(originClass.getSimpleName()).concat(IdUtil.objectId());
try {
// 排除 (黑名单)
if (null != apiExclude) {
String[] properties = apiExclude.value();
parameterContext.getDocumentationContext()
.getAdditionalModels()
//向documentContext的Models中添加我们新生成的Class
.add(typeResolver.resolve(createRefModelIgp(properties, originClass.getPackage() + "." + name, originClass)));
}
// 需要 (白名单)
if (null != apiInclude) {
String[] properties = apiInclude.value();
parameterContext.getDocumentationContext()
.getAdditionalModels()
//向documentContext的Models中添加我们新生成的Class
.add(typeResolver.resolve(createRefModelNeed(properties, originClass.getPackage() + "." + name, originClass)));
}
} catch (Exception e) {
e.printStackTrace();
logger.error("swagger切面异常", e);
}
//修改Map参数的ModelRef为我们动态生成的class
parameterContext.parameterBuilder()
.parameterType("body")
.modelRef(new ModelRef(name))
.name(name);
}
}
/**
* 创建自定义mode给swagger2 排除参数
*
* @param properties 需要排除的参数
* @param name model 名称
* @param origin originClass
* @return r
*/
private Class<?> createRefModelIgp(String[] properties, String name, Class<?> origin) {
ClassPool pool = ClassPool.getDefault();
// 动态创建一个class
CtClass ctClass = pool.makeClass(name);
if (ctClass != null) {
// 这行代码很重要 如果没有的话 那就不能和devTool 一起使用
// 销毁旧的类
ctClass.detach();
}
try {
Field[] fields = origin.getDeclaredFields();
List<Field> fieldList = Arrays.asList(fields);
List<String> ignoreProperties = Arrays.asList(properties);
// 过滤掉 properties 的参数
List<Field> dealFields = fieldList.stream().filter(s -> !ignoreProperties.contains(s.getName())).collect(Collectors.toList());
addField2CtClass(dealFields, origin, ctClass);
return ctClass.toClass();
} catch (Exception e) {
logger.error("swagger切面异常", e);
return null;
}
}
/**
* 创建自定义mode给swagger2 需要参数
*
* @param properties 需要的参数
* @param name model 名称
* @param origin originClass
* @return r
*/
private Class<?> createRefModelNeed(String[] properties, String name, Class<?> origin) {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass(name);
try {
Field[] fields = origin.getDeclaredFields();
List<Field> fieldList = Arrays.asList(fields);
List<String> needProperties = Arrays.asList(properties);
// 过滤掉 非 properties 的参数
List<Field> dealFields = fieldList.stream().filter(s -> needProperties.contains(s.getName())).collect(Collectors.toList());
addField2CtClass(dealFields, origin, ctClass);
return ctClass.toClass();
} catch (Exception e) {
logger.error("swagger切面异常", e);
return null;
}
}
private void addField2CtClass(List<Field> dealFields, Class<?> origin, CtClass ctClass) throws NoSuchFieldException, NotFoundException, CannotCompileException {
// 倒序遍历
for (int i = dealFields.size() - 1; i >= 0; i--) {
Field field = dealFields.get(i);
CtField ctField = new CtField(ClassPool.getDefault().get(field.getType().getName()), field.getName(), ctClass);
ctField.setModifiers(Modifier.PUBLIC);
ApiModelProperty ampAnno = origin.getDeclaredField(field.getName()).getAnnotation(ApiModelProperty.class);
String attributes = java.util.Optional.ofNullable(ampAnno).map(ApiModelProperty::value).orElse("");
//添加model属性说明
if (StringUtils.isNotBlank(attributes)) {
ConstPool constPool = ctClass.getClassFile().getConstPool();
AnnotationsAttribute attr = new AnnotationsAttribute(constPool, AnnotationsAttribute.visibleTag);
Annotation ann = new Annotation(ApiModelProperty.class.getName(), constPool);
ann.addMemberValue("value", new StringMemberValue(attributes, constPool));
attr.addAnnotation(ann);
ctField.getFieldInfo().addAttribute(attr);
}
ctClass.addField(ctField);
}
}
@Override
public boolean supports(DocumentationType documentationType) {
return true;
}
}
下面这个是解决query传参的
import com.fasterxml.classmate.ResolvedType;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.FluentIterable;
import com.ruoyi.common.annotation.ApiExclude;
import com.ruoyi.common.annotation.ApiInclude;
import com.ruoyi.common.constant.Constants;
import org.apache.poi.ss.formula.functions.T;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import springfox.documentation.builders.BuilderDefaults;
import springfox.documentation.builders.OperationBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.service.Parameter;
import springfox.documentation.service.ResolvedMethodParameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.schema.EnumTypeDeterminer;
import springfox.documentation.spi.service.OperationBuilderPlugin;
import springfox.documentation.spi.service.contexts.OperationContext;
import springfox.documentation.spi.service.contexts.ParameterContext;
import springfox.documentation.spring.web.plugins.DocumentationPluginsManager;
import springfox.documentation.spring.web.readers.operation.OperationParameterReader;
import springfox.documentation.spring.web.readers.parameter.ExpansionContext;
import springfox.documentation.spring.web.readers.parameter.ModelAttributeParameterExpander;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.*;
import java.util.stream.Collectors;
import static com.google.common.base.Predicates.not;
import static springfox.documentation.schema.Collections.isContainerType;
import static springfox.documentation.schema.Maps.isMapType;
import static springfox.documentation.schema.Types.isBaseType;
import static springfox.documentation.schema.Types.typeNameFor;
/**
* @author 李永杰
* @date 2021/11/5
* @description 读取自定义注解的dto属性并动态生成model, 只支持query参数
*/
@Order
@Configuration
public class SwaggerQueryParameterPlugin extends OperationParameterReader implements OperationBuilderPlugin {
private static final Logger logger = LoggerFactory.getLogger(SwaggerQueryParameterPlugin.class);
private final EnumTypeDeterminer enumTypeDeterminer;
private final ModelAttributeParameterExpander expander;
private Boolean changed;
@Autowired
private DocumentationPluginsManager pluginsManager;
public SwaggerQueryParameterPlugin(ModelAttributeParameterExpander expander, EnumTypeDeterminer enumTypeDeterminer) {
super(expander, enumTypeDeterminer);
this.enumTypeDeterminer = enumTypeDeterminer;
this.expander = expander;
}
@Override
public void apply(OperationContext context) {
changed = false;
List<Parameter> parameters = readParameters(context);
if (changed) {
// 反射给parameters赋值
try {
Field parametersField = OperationBuilder.class.getDeclaredField("parameters");
parametersField.setAccessible(true);
List<Parameter> source = BuilderDefaults.nullToEmptyList(parameters);
parametersField.set(context.operationBuilder(), source);
} catch (Exception e) {
logger.error("动态更改swagger参数错误", e);
}
}
context.operationBuilder().parameters(context.getGlobalOperationParameters());
}
@Override
public boolean supports(DocumentationType delimiter) {
return super.supports(delimiter);
}
@SuppressWarnings("Guava")
private List<Parameter> readParameters(final OperationContext context) {
List<ResolvedMethodParameter> methodParameters = context.getParameters();
List<Parameter> parameters = new ArrayList();
for (ResolvedMethodParameter methodParameter : methodParameters) {
ResolvedType alternate = context.alternateFor(methodParameter.getParameterType());
if (!shouldIgnore(methodParameter, alternate, context.getIgnorableParameterTypes())) {
ParameterContext parameterContext = new ParameterContext(methodParameter,
new ParameterBuilder(),
context.getDocumentationContext(),
context.getGenericsNamingStrategy(),
context);
List<Parameter> tempItems;
if (shouldExpand(methodParameter, alternate)) {
tempItems = expander.expand(
new ExpansionContext("", alternate, context));
} else {
tempItems = new ArrayList<>(Collections.singleton(pluginsManager.parameter(parameterContext)));
}
// 判断遍历 是否有自定义注解 有就进行操作
Optional<ApiExclude> apiExcludeOptional = methodParameter.findAnnotation(ApiExclude.class);
if (apiExcludeOptional.isPresent()) {
String[] properties = apiExcludeOptional.get().value();
tempItems = tempItems.stream().filter(parameter -> {
for (String property : properties) {
//匹配 黑名单 注意这里和x-easy大神的判断不一样
//去掉了 ||!parameter.getName().contains(Constants.MY_MODEL_NAME_PRE),
//测试中发现加了这个判断回导致query排除参数失效,这里是我dedug一步步发现的
if (property.equals(parameter.getName())|| parameter.getName().startsWith(property + ".")){
return false;
}
}
return true;
}).collect(Collectors.toList());
changed = true;
}
Optional<ApiInclude> apiIncludeOptional = methodParameter.findAnnotation(ApiInclude.class);
if (apiIncludeOptional.isPresent()) {
String[] properties = apiIncludeOptional.get().value();
tempItems = tempItems.stream().filter(parameter -> {
for (String property : properties) {
//匹配 白名单
if (property.equals(parameter.getName())
|| parameter.getName().contains(Constants.MY_MODEL_NAME_PRE)
|| parameter.getName().startsWith(property + ".")
) {
return true;
}
}
return false;
}).collect(Collectors.toList());
changed = true;
}
parameters.addAll(tempItems);
}
}
return FluentIterable.from(parameters).filter(not(hiddenParams())).toList();
}
private Predicate<Parameter> hiddenParams() {
return Parameter::isHidden;
}
private boolean shouldIgnore(
final ResolvedMethodParameter parameter,
ResolvedType resolvedParameterType,
final Set<Class> ignorableParamTypes) {
if (ignorableParamTypes.contains(resolvedParameterType.getErasedType())) {
return true;
}
return FluentIterable.from(ignorableParamTypes)
.filter(isAnnotation())
.filter(parameterIsAnnotatedWithIt(parameter)).size() > 0;
}
private Predicate<Class> parameterIsAnnotatedWithIt(final ResolvedMethodParameter parameter) {
return parameter::hasParameterAnnotation;
}
private Predicate<Class> isAnnotation() {
return Annotation.class::isAssignableFrom;
}
private boolean shouldExpand(final ResolvedMethodParameter parameter, ResolvedType resolvedParamType) {
return !parameter.hasParameterAnnotation(RequestBody.class)
&& !parameter.hasParameterAnnotation(RequestPart.class)
&& !parameter.hasParameterAnnotation(RequestParam.class)
&& !parameter.hasParameterAnnotation(PathVariable.class)
&& !isBaseType(typeNameFor(resolvedParamType.getErasedType()))
&& !enumTypeDeterminer.isEnum(resolvedParamType.getErasedType())
&& !isContainerType(resolvedParamType)
&& !isMapType(resolvedParamType);
}
}
接口controller类
import com.ruoyi.common.annotation.ApiExclude;
import com.ruoyi.common.annotation.ApiInclude;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
/**
* @author 李永杰
* @date 2021/10/14
* @description 登录模块
*/
@Api(tags = "自定义参数测试模块")
@RestController
@RequestMapping("/test")
public class DemoController extends BaseController {
@PostMapping("/testExcludeBody")
@ApiOperation("body参数测试排除")
public AjaxResult testExcludeBody(@ApiExclude({"a", "b"}) @RequestBody Demo demo) {
return AjaxResult.success(demo);
}
@PostMapping("/testIncludeBody")
@ApiOperation("body参数测试包含")
public AjaxResult testIncludeBody(@ApiInclude({"a", "b"}) @RequestBody Demo demo) {
return AjaxResult.success(demo);
}
@GetMapping("/testExcludeQuery")
@ApiOperation("query参数测试排除")
public AjaxResult testExcludeQuery(@ApiExclude({"a", "b"}) Demo demo) {
return AjaxResult.success(demo);
}
@GetMapping("/testIncludeQuery")
@ApiOperation("query参数测试包含")
public AjaxResult testIncludeQuery(@ApiInclude({"a", "b"}) Demo demo) {
return AjaxResult.success(demo);
}
}
测试结果
body参数测试排除
query参数测试排除
body参数测试包含
query参数测试包含
参考文档
https://www.jianshu.com/p/09a4619fb0f7
https://blog.youkuaiyun.com/flymoringbird/article/details/102919322