同一个实体用在多个不同的controller接口展示不同的字段,如果用过spring Validation校验框架应该都懂它里面有个分组概念,作用就是同一个实体用在多个接口里面但是校验参数不同,但swagger并没有分组概念,swagger显然做不到类似作用,ApiModelProperty注解的有个hidden属性,但这个作用只能要么全显示要么全隐藏,那下面的方法就是针对同一个实体在一个接口的某些属性是隐藏,在另一个接口中是显示的这种情况。
io.springfox
springfox-swagger2
2.8.0
io.springfox
springfox-swagger-ui
2.8.0
org.apache.commons
commons-lang3
3.12.0
org.mybatis
mybatis
3.4.6
添加ApiIgp和ApiNeed自定义注解。
ApiIgp排除实体类中不显示在swagger文档的属性
ApiNeed是只显示实体类中的某些属性
tip:这两个注解都只能操作实体的属性,如果实体中不存在的属性不会报错也不会生效
ApiIgp
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义aop注解 支持swagger的动态属性 排除属性
*/
@Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIgp {
String[] value(); //对象属性值
}
ApiNeed
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义aop注解 支持swagger的动态属性 (只)需要属性
*/
@Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiNeed {
//对象属性值
String[] value();
}
添加两个配置类 MyParameterBuilderPlugin和MyOperationBuilderPlugin。一个大概针对的是json数据,另一个是非json数据
MyParameterBuilderPlugin
import com.fasterxml.classmate.TypeResolver;
import com.google.common.base.Optional;
import com.honger1234.annotation.ApiIgp;
import com.honger1234.annotation.ApiNeed;
import io.swagger.annotations.ApiModelProperty;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.javassist.*;
import org.apache.ibatis.javassist.bytecode.AnnotationsAttribute;
import org.apache.ibatis.javassist.bytecode.ConstPool;
import org.apache.ibatis.javassist.bytecode.annotation.Annotation;
import org.apache.ibatis.javassist.bytecode.annotation.StringMemberValue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
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.Random;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* 重写 swagger2 的 ParameterBuilderPlugin 支持自定义白名单黑名单注解
* 局限:只能对post中的json数据生效,如果要对get的拼接参数请看MyOperationBuilderPlugin
*/
@Component
@Order
@Slf4j
public class MyParameterBuilderPlugin implements ParameterBuilderPlugin {
@Autowired
private TypeResolver typeResolver;
@Override
public void apply(ParameterContext parameterContext) {
ResolvedMethodParameter methodParameter = parameterContext.resolvedMethodParameter();
Class> originClass = parameterContext.resolvedMethodParameter().getParameterType().getErasedType();
// 排除属性
ApiIgp igpOptional = null;
@SuppressWarnings("Guava")
Optional apiIgpOptional = methodParameter.findAnnotation(ApiIgp.class);
if ( apiIgpOptional.isPresent() ) {
igpOptional = apiIgpOptional.get();
}
// 需要属性
ApiNeed needOptional = null;
@SuppressWarnings("Guava")
Optional apiNeedOptional = methodParameter.findAnnotation(ApiNeed.class);
if ( apiNeedOptional.isPresent() ) {
needOptional = apiNeedOptional.get();
}
if (null != igpOptional || null != needOptional ) {
Random random = new Random();
//model 名称
String name = originClass.getSimpleName() + "_" + UUID.randomUUID().toString().replace("-", "");
try {
// 排除 (黑名单)
if ( null != igpOptional ) {
String[] properties = igpOptional.value();
parameterContext.getDocumentationContext()
.getAdditionalModels()
//向documentContext的Models中添加我们新生成的Class
.add(typeResolver.resolve(createRefModelIgp(properties, originClass.getPackage()+"."+name, originClass)));
}
// 需要 (白名单)
if ( null != needOptional ) {
String[] properties = needOptional.value();
parameterContext.getDocumentationContext()
.getAdditionalModels()
//向documentContext的Models中添加我们新生成的Class
.add(typeResolver.resolve(createRefModelNeed(properties, originClass.getPackage()+"."+name, originClass)));
}
} catch (Exception e) {
log.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);
try {
Field[] fields = origin.getDeclaredFields();
List fieldList = Arrays.asList(fields);
List ignoreProperties = Arrays.asList(properties);
// 过滤掉 properties 的参数
List dealFields = fieldList.stream().filter(s -> !ignoreProperties.contains(s.getName())).collect(Collectors.toList());
addField2CtClass(dealFields, origin, ctClass);
return ctClass.toClass();
} catch (Exception e) {
log.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 fieldList = Arrays.asList(fields);
List ignoreProperties = Arrays.asList(properties);
// 过滤掉 非 properties 的参数
List dealFields = fieldList.stream().filter(s -> ignoreProperties.contains(s.getName())).collect(Collectors.toList());
addField2CtClass(dealFields, origin, ctClass);
return ctClass.toClass();
} catch (Exception e) {
log.error("swagger切面异常", e);
return null;
}
}
private void addField2CtClass(List 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;
}
}
MyOperationBuilderPlugin
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.honger1234.annotation.ApiIgp;
import com.honger1234.annotation.ApiNeed;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
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.Operation;
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.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import static com.google.common.base.Predicates.not;
import static com.google.common.collect.Lists.newArrayList;
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;
/**
* 重写 swagger2 的 ModelAttributeParameterExpander 支持get请求的自定义注解
* 局限:json数据不生效
*/
@SuppressWarnings({"Guava", "rawtypes"})
@Component
@Order
@Slf4j
public class MyOperationBuilderPlugin extends OperationParameterReader implements OperationBuilderPlugin {
private final EnumTypeDeterminer enumTypeDeterminer;
private final ModelAttributeParameterExpander expander;
private Boolean changed;
@Autowired
private DocumentationPluginsManager pluginsManager;
@Autowired
public MyOperationBuilderPlugin(ModelAttributeParameterExpander expander, EnumTypeDeterminer enumTypeDeterminer) {
super(expander, enumTypeDeterminer);
this.enumTypeDeterminer = enumTypeDeterminer;
this.expander = expander;
}
/**
* Implement this method to override the Operation using the OperationBuilder available in the context
*
* @param context - context that can be used to override the parameter attributes
* @see Operation
* @see OperationBuilder
*/
@Override
public void apply(OperationContext context) {
changed = false;
List parameters = readParameters(context);
if (changed){
// 反射给parameters赋值
try {
Field parametersField = OperationBuilder.class.getDeclaredField("parameters");
parametersField.setAccessible(true);
List source = BuilderDefaults.nullToEmptyList(parameters);
parametersField.set(context.operationBuilder(), source);
} catch ( Exception e ) {
log.error("动态更改swagger参数错误", e);
}
}
context.operationBuilder().parameters(context.getGlobalOperationParameters());
}
@Override
public boolean supports(DocumentationType documentationType) {
return super.supports(documentationType);
}
@SuppressWarnings("Guava")
private List readParameters(final OperationContext context) {
List methodParameters = context.getParameters();
List parameters = newArrayList();
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 tempItems;
if (shouldExpand(methodParameter, alternate)) {
tempItems = expander.expand(
new ExpansionContext("", alternate, context.getDocumentationContext()));
} else {
tempItems = new ArrayList<>(Collections.singleton(pluginsManager.parameter(parameterContext)));
// tempItems = new ArrayList<>();
}
// 判断遍历 是否有自定义注解 有就进行操作
Optional apiIgpOptional = methodParameter.findAnnotation(ApiIgp.class);
if ( apiIgpOptional.isPresent() ) {
String[] properties = apiIgpOptional.get().value();
tempItems = tempItems.stream().filter(parameter -> {
for (String property : properties) {
// 匹配黑名单
if ( property.equals(parameter.getName())
|| parameter.getName().startsWith(property + ".") ) {
return false;
}
}
return true;
}).collect(Collectors.toList());
changed = true;
}
Optional apiNeedOptional = methodParameter.findAnnotation(ApiNeed.class);
if ( apiNeedOptional.isPresent() ) {
String[] properties = apiNeedOptional.get().value();
tempItems = tempItems.stream().filter(parameter -> {
for (String property : properties) {
// 匹配 白名单
if ( property.equals(parameter.getName())
|| 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 hiddenParams() {
return Parameter::isHidden;
}
private boolean shouldIgnore(
final ResolvedMethodParameter parameter,
ResolvedType resolvedParameterType,
final Set ignorableParamTypes) {
if (ignorableParamTypes.contains(resolvedParameterType.getErasedType())) {
return true;
}
return FluentIterable.from(ignorableParamTypes)
.filter(isAnnotation())
.filter(parameterIsAnnotatedWithIt(parameter)).size() > 0;
}
private Predicate parameterIsAnnotatedWithIt(final ResolvedMethodParameter parameter) {
return parameter::hasParameterAnnotation;
}
private Predicate 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);
}
}
实体
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
public class UseInfo {
@ApiModelProperty(value = "用户名")
private String username;
@ApiModelProperty(value = "密码",hidden = true)
private String password;
@ApiModelProperty(value = "年龄")
private Integer age;
}
接口
@ApiOperation(value = "排除属性注解")
@GetMapping("/swaggerTest1")
public String swaggerTest1(@ApiIgp ({"password"})@RequestBody UseInfo useInfo){
return "success";
}
@ApiOperation(value = "只需要属性注解")
@GetMapping("/swaggerTest2")
public String swaggerTest2(@ApiNeed({"username","age"}) UseInfo useInfo){
return "success";
}