前言
swagger是目前使用最多的文档生成工具,它能满足绝大多数的需求,但总有一些场景是它默认的逻辑所不能满足的,为此,springfox提供了一些扩展方法,只需要实现对应的Plugin接口就可以覆盖掉swagger默认的逻辑,以自定义的逻辑执行
接下来讲述本文所自定义的方法:在swagger中,如果在get请求中使用了Object参数,并且没有加@Requestbody或者@RequestParam这类的注解时,swagger会将该Object的属性拆解为一个个字段。
本贴使用的sprinfox版本为springfox-swagger2-3.0.0
3.0.0与之前版本有较大不同所以网上相关贴子比较少
接口:
@ApiOperation(value = "分页查询", notes = "分页查询")
@GetMapping("/item/page")
public Result<Page<SystemDictItem>> getSystemDictItemPage(Page<SystemDictItem> page, Integer dictId) {
return Result.success(systemDictService.getItemPage(page, dictId));
}
接口由一个page对象和一个Integer类的id组成
swagger展示效果:
可以看到,swagger将page对象的所有字段全部拆解了出来,包括子对象records的字段
protected List<T> records;
protected long total;
protected long size;
protected long current;
protected List<OrderItem> orders;
protected boolean optimizeCountSql;
protected boolean isSearchCount;
protected boolean hitCount;
protected String countId;
protected Long maxLimit;
问题一:我们其实并不需要这么多字段,关于page对象,我们一般只需要size和current这两个字段即可,其他字段会显得比较多余,如果这是我们自定义的对象,可以通过加@ApiModelProperty(hidden = true)来进行字段的隐藏,但page对象是mybatisplus自带的,我们无法进行修改,所以说,使用swagger的默认逻辑是无法隐藏page的其他字段的,这就需要我们实现springfox的plugin接口来进行自定义逻辑了
问题二:当我们在对象属性上加了@NotNull、@NotBlan等注解时,swagger在拆解的过程中会将该字段的必填改为true,但其实在查询的时候我们并不希望该字段必填。这个问题我们同样可以通过问题一同样的方法进行修改。
实现Plugin接口
能过debug源码我们发现,swagger将Object对象解析成单个属性的过程是在OperationParameterReader
类中进行完成的,而这个类实现了``OperationBuilderPlugin`接口
那接下来,我们只需要也实现``OperationBuilderPlugin接口并重写它的apply和supports方法即可,关于这一步可以模仿
OperationParameterReader`类的实现逻辑,只需要将我们需要的内容加进去即可
首先需要自定义两个注解
@ApiIgnoreField:用来指定该对象需要哪些属性
package com.ralph.common.docs.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE,ElementType.METHOD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIgnoreField {
String[] value();
}
@ApiNeedField:用来指定哪些字段需要排除掉
package com.ralph.common.docs.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE,ElementType.METHOD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiNeedField {
String[] value();
}
自定义处理类:
package com.ralph.common.docs.support;
import cn.hutool.core.collection.CollUtil;
import com.fasterxml.classmate.ResolvedType;
import com.google.common.collect.Lists;
import com.ralph.common.docs.annotation.ApiIgnoreField;
import com.ralph.common.docs.annotation.ApiNeedField;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.builders.RequestParameterBuilder;
import springfox.documentation.common.Compatibility;
import springfox.documentation.schema.Collections;
import springfox.documentation.schema.Maps;
import springfox.documentation.schema.ScalarTypes;
import springfox.documentation.service.Parameter;
import springfox.documentation.service.RequestParameter;
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.operation.ParameterAggregator;
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.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Component
@Order(Integer.MIN_VALUE)
@Slf4j
public class RalphCusParamHandler implements OperationBuilderPlugin {
@Autowired
public RalphCusParamHandler(ModelAttributeParameterExpander expander, EnumTypeDeterminer enumTypeDeterminer, ParameterAggregator aggregator) {
this.expander = expander;
this.enumTypeDeterminer = enumTypeDeterminer;
this.aggregator = aggregator;
}
private static final Logger LOGGER = LoggerFactory.getLogger(OperationParameterReader.class);
private final ModelAttributeParameterExpander expander;
private final EnumTypeDeterminer enumTypeDeterminer;
private final ParameterAggregator aggregator;
private boolean change = false;
@Autowired
private DocumentationPluginsManager pluginsManager;
public void apply(OperationContext context) {
change = false;
context.operationBuilder().parameters(context.getGlobalOperationParameters());
List<Compatibility<Parameter, RequestParameter>> compatibilities = this.readParameters(context);
if (change) {
// 由于OperationBuilder类没有提供set方法,需要通过反射给parameters赋值
try {
Field parametersField = OperationBuilder.class.getDeclaredField("parameters");
parametersField.setAccessible(true);
List<Parameter> parameters = compatibilities.stream().map(Compatibility::getLegacy).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList());
List<Parameter> source = BuilderDefaults.nullToEmptyList(parameters);
parametersField.set(context.operationBuilder(), source);
Field requestParameters = OperationBuilder.class.getDeclaredField("requestParameters");
requestParameters.setAccessible(true);
Set<RequestParameter> requestParametersList = compatibilities.stream().map(Compatibility::getModern).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toSet());
Set<RequestParameter> requestSource = BuilderDefaults.nullToEmptySet(requestParametersList);
requestParameters.set(context.operationBuilder(), requestSource);
} catch (Exception e) {
log.error("动态更改swagger参数错误", e);
}
}else {
context.operationBuilder().parameters((List)compatibilities.stream().map(Compatibility::getLegacy).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList()));
context.operationBuilder().requestParameters(new HashSet(context.getGlobalRequestParameters()));
Collection<RequestParameter> requestParameters = (Collection)compatibilities.stream().map(Compatibility::getModern).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toSet());
context.operationBuilder().requestParameters(this.aggregator.aggregate(requestParameters));
}
}
public boolean supports(DocumentationType delimiter) {
return true;
}
private List<Compatibility<Parameter, RequestParameter>> readParameters(OperationContext context) {
List<ResolvedMethodParameter> methodParameters = context.getParameters();
List<Compatibility<Parameter, RequestParameter>> parameters = new ArrayList();
LOGGER.debug("Reading parameters for method {} at path {}", context.getName(), context.requestMappingPattern());
int index = 0;
Iterator var5 = methodParameters.iterator();
while(var5.hasNext()) {
ResolvedMethodParameter methodParameter = (ResolvedMethodParameter)var5.next();
// 判断参数是否加了自定义注解以及注解的值
Optional<ApiIgnoreField> ignoreFieldAnnotation = methodParameter.findAnnotation(ApiIgnoreField.class);
Optional<ApiNeedField> needFieldAnnotation = methodParameter.findAnnotation(ApiNeedField.class);
List<String> needList = new ArrayList<>();
List<String> ignoreList = new ArrayList<>();
if (needFieldAnnotation.isPresent()) {
String[] fields = needFieldAnnotation.get().value();
needList.addAll(Lists.newArrayList(fields));
}else if (ignoreFieldAnnotation.isPresent()){
String[] fields = ignoreFieldAnnotation.get().value();
ignoreList.addAll(Lists.newArrayList(fields));
}
LOGGER.debug("Processing parameter {}", methodParameter.defaultName().orElse("<unknown>"));
ResolvedType alternate = context.alternateFor(methodParameter.getParameterType());
if (!this.shouldIgnore(methodParameter, alternate, context.getIgnorableParameterTypes())) {
ParameterContext parameterContext = new ParameterContext(methodParameter, context.getDocumentationContext(), context.getGenericsNamingStrategy(), context, index++);
if (this.shouldExpand(methodParameter, alternate)) {
List<Compatibility<Parameter, RequestParameter>> expand = this.expander.expand(new ExpansionContext("", alternate, context));
// 处理需要的字段
if (CollUtil.isNotEmpty(needList)){
expand = expand.stream().filter(item->{
String modelName = item.getModern().map(RequestParameter::getName).orElse("");
String legacyName = item.getLegacy().map(Parameter::getName).orElse("");
return needList.stream().anyMatch(need-> need.matches(modelName) && need.matches(legacyName));
}).collect(Collectors.toList());
change = true;
}else if (CollUtil.isNotEmpty(ignoreList)){
// 处理不需要的字段
expand = expand.stream().filter(item->{
String modelName = item.getModern().map(RequestParameter::getName).orElse("");
String legacyName = item.getLegacy().map(Parameter::getName).orElse("");
return ignoreList.stream().noneMatch(ignore-> ignore.matches(modelName) && ignore.matches(legacyName));
}).collect(Collectors.toList());
change = true;
}
// 对象类型的参数都改为非必填
expand = expand.stream().map(item->{
RequestParameterBuilder requestParameterBuilder = new RequestParameterBuilder();
RequestParameterBuilder resRequestParameter = requestParameterBuilder.copyOf(item.getModern().get());
Parameter sourceParameter = item.getLegacy().get();
ParameterBuilder parameterBuilder = new ParameterBuilder();
ParameterBuilder resParameter = parameterBuilder.name(sourceParameter.getName()).allowableValues(sourceParameter.getAllowableValues()).allowMultiple(sourceParameter.isAllowMultiple()).defaultValue(sourceParameter.getDefaultValue()).description(sourceParameter.getDescription()).modelRef(sourceParameter.getModelRef()).parameterAccess(sourceParameter.getParamAccess()).parameterType(sourceParameter.getParamType()).required(sourceParameter.getRequired()).type((ResolvedType)sourceParameter.getType().orElse((ResolvedType) null)).hidden(sourceParameter.isHidden()).allowEmptyValue(sourceParameter.isAllowEmptyValue()).order(sourceParameter.getOrder()).vendorExtensions(sourceParameter.getVendorExtentions()).collectionFormat(sourceParameter.getCollectionFormat());
if (item.getModern().isPresent() && item.getLegacy().isPresent() &&
(item.getModern().get().getRequired() || item.getLegacy().get().isRequired())){
resParameter.required(false);
try {
Field required = RequestParameterBuilder.class.getDeclaredField("required");
required.setAccessible(true);
required.set(resRequestParameter,false);
} catch (Exception e){
// 无作用
resRequestParameter.required(false);
}
change = true;
return new Compatibility<Parameter, RequestParameter>(resParameter.build(), resRequestParameter.build());
}
return item;
}).collect(Collectors.toList());
parameters.addAll(expand);
} else {
parameters.add(this.pluginsManager.parameter(parameterContext));
}
}
}
return (List)parameters.stream().filter(this.hiddenParameter().negate()).collect(Collectors.toList());
}
private Predicate<Compatibility<Parameter, RequestParameter>> hiddenParameter() {
return (c) -> {
return (Boolean)c.getLegacy().map(Parameter::isHidden).orElse(false);
};
}
private boolean shouldIgnore(
final ResolvedMethodParameter parameter,
ResolvedType resolvedParameterType,
final Set<Class> ignorableParamTypes) {
if (ignorableParamTypes.contains(resolvedParameterType.getErasedType())) {
return true;
} else {
Stream var10000 = ignorableParamTypes.stream();
Objects.requireNonNull(Annotation.class);
var10000 = var10000.filter(item-> Annotation.class.isAssignableFrom((Class<?>) item));
Objects.requireNonNull(parameter);
return var10000.anyMatch(item->parameter.hasParameterAnnotation((Class<? extends Annotation>) item));
}
}
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) && !ScalarTypes.builtInScalarType(resolvedParamType.getErasedType()).isPresent() && !this.enumTypeDeterminer.isEnum(resolvedParamType.getErasedType()) && !Collections.isContainerType(resolvedParamType) && !Maps.isMapType(resolvedParamType);
}
}
处理类完成后,只需要在接口上增加对应注解即可实现想要的效果
@ApiOperation(value = "分页查询", notes = "分页查询")
@GetMapping("/page")
public Page<SystemDict> getSystemDictPage(@ApiNeedField({"size","current"}) Page<SystemDict> page,
@ApiIgnoreField({"updateDate","updateBy"}) SystemDict systemDict) {
return systemDictService.page(page, Wrappers.query(systemDict).lambda()
.like(SystemDict::getType,systemDict.getType())
.orderByDesc(SystemDict::getCreateDate));
}
如上接口,在swagger中展示的效果如下:
我们看到关于page的字段只展示了size和current,而SystemDict相关的字段并没有updateDate和updateBy这两个字段,这说明我们的改动已经成功了
如果想参考源码可以通过贴子底部跳转连接跳转查看相关代码
👍 欢迎前往博客主页查看更多内容
👍 如果觉得不错,期待您的点赞、收藏、评论、关注
👍 如有错误欢迎指正!
👍 Gitee地址:https://gitee.com/ralphchen/ralph-cloud