0
点赞
收藏
分享

微信扫一扫

实战篇:解决swagger和自定义参数解析器的功能冲突


前言

​@RequestBody​​使用的参数解析器​​RequestResponseBodyMethodProcessor​​优先级高于我们自定义的参数解析器,所以为了正常使用,需要将​​@RequestBody​​ 注解去掉。这就会导致​​swagger​​无法识别正确的参数类型,将请求体识别为​​Query Params​​,然后将​​body​​展开。

实战篇:解决swagger和自定义参数解析器的功能冲突_开发语言


可以看到,所有参数都被识别为​​ModelAttribute​​类型(​​query​​标志),而我们所期待的正确格式应当是如下样子

实战篇:解决swagger和自定义参数解析器的功能冲突_程序人生_02

因为该方式可以大大提高代码的可读性和可复用性,所以我们要知难而上,找出问题,解决问题!

问题产生的原因

产生这个问题的根本原因就是​​spring mvc​​和​​swagger​​都对​​@RequestBody​​注解进行了单独的判定,功能上都依赖于该注解本身。

​springmvc​​​对​​@RequestBody​​注解的依赖

就拿当前自定义的参数解析器来说,如果对请求参数加上了 ​​@RequestBody​​ 注解,对参数的反序列化会提前被​​RequestResponseBodyMethodProcessor​​拦截,自定义的参数解析器会失效。

具体源代码位置:​​https://github.com/spring-projects/spring-framework/blob/5.2.x/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java#L111​

实战篇:解决swagger和自定义参数解析器的功能冲突_程序人生_03

可以看到,该参数解析器对加上​​@ReuqestBody​​注解的参数都支持解析,然后做序列化的操作。然而它在参数解析器列表中的优先级比较高,自定义的参数解析器添加到参数解析器列表之后会排在它的后面,所以如果加上​​@RequestBody​​注解,自定义的参数解析器就失效了。

因此使用自定义参数解析器​一定不能​使用​​@RequestBody​​注解

下图源代码位置:​​https://github.com/spring-projects/spring-framework/blob/5.2.x/spring-web/src/main/java/org/springframework/web/method/support/HandlerMethodArgumentResolverComposite.java#L129​

实战篇:解决swagger和自定义参数解析器的功能冲突_Java程序员_04


此案例中用到的自定义参数解析器为​​HdxArgumentResolver​


​swagger​​​对​​@Requestbody​​的依赖

经过调用栈追踪,最终发现在两个地方的功能会对​​@RequestBody​​注解有单独判定!(感兴趣的可以自行追踪????)

  • 请求类型判定:也就是说​​POST​​请求类型是哪种类型,这决定了入参是否会作为​​Request Parameter​​被展开参数,也就是文中的第一张图,整个​​model​​都被视为​​ModelAttribute​​展开了。

  • ​Definition​​属性值填充:这确保被​​@RequestBody​​注解修饰的入参会被正常显示,如文中第二张图片所示。

请求类型判定

源代码位置:​​https://github.com/springfox/springfox/blob/2.9.2/springfox-spring-web/src/main/java/springfox/documentation/spring/web/readers/operation/OperationParameterReader.java#L151​


这里对​​RequestBody​​​等常用注解进行了单独的判定,确保这些注解修饰的入参不会被作为​​RequestParam​​展开。

​Definition​​属性值填充

​Definition​​​属性中填充了入参、出参等参数类型,如果没有相应的​​Model​​​定义,则​​swagger​​​信息就会是不完整的,在浏览器页面中的显示也会是不全的。填充​​Definition​​​的逻辑也依赖于​​@RequestBody​​注解。

源代码位置:​​https://github.com/springfox/springfox/blob/2.9.2/springfox-spring-web/src/main/java/springfox/documentation/spring/web/readers/operation/OperationModelsProvider.java#L80​

实战篇:解决swagger和自定义参数解析器的功能冲突_Java程序员_05

可以看到,只有被​​RequestBody​​​注解和​​RequestPart​​​注解修饰的入参才会被接收进入​​Definition​​属性。

综合以上两张图的源代码分析,可以看到,​​swagger​​​功能依赖于​​@RequestBody​​​注解,入参如果不被该注解修饰,则​​swagger​​​功能就会不完整,这和在​​springmvc​​​中使用独立的参数解析器功能不得使用​​@RequestBody​​注解矛盾。

解决问题

从以上分析可以得到结论,这里的根本问题是​​springmvc​​​中独立的参数解析器功能和​​swagger​​​功能上的冲突,一个要求不能加上​​@RequestBody​​​注解,一个要求必须加上​​@RequestBody​​注解,所以解决方法上可以使用两种方式

  • 从​​springmvc​​​入手,想办法提高自定义参数解析器的优先级,只要自定义的参数解析器优先级比​​RequestResponseBodyMethodProcessor​​​高,则就可以在自定义的参数上加上​​@RequestBody​​​注解,​​swagger​​功能自然而然就能正常了。

  • 从​​swagger​​​入手,想办法解决掉上面两部分对​​@RequestBody​​​的单独判定,不修改​​springmvc​​​相关功能也可以让​​swagger​​功能正常。

考虑到修改​​springmvc​​​功能可能会对以后的版本升级造成较大影响,这里决定利用切面修改原有的​​swagger​​​对​​@RequestBody​​​的两个地方的行为,从而让​​swagger​​功能正常。

请求类型判定的逻辑调整

首先,定义一个注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface NoSwaggerExpand {

/**
* default swagger expand disable
* @see OperationParameterReader#shouldExpand(springfox.documentation.service.ResolvedMethodParameter, com.fasterxml.classmate.ResolvedType)
*/
boolean expand() default false;
}

将其加到入参上

@ApiOperation(value = "demo", notes = "demo")
@PostMapping(value = "/test")
public Result<boolean> test(@HdxDecrypt @NoSwaggerExpand @ApiParam(required = true) ReqDTO reqDTO) {
try {
log.info(ObjectMapperFactory.getObjectMapper().writeValueAsString(reqDTO));
} catch (JsonProcessingException e) {
log.error("", e);
}
return null;
}

然后定义切面

@Slf4j
@Aspect
@Component
public class SwaggerExpandAspect {

private final ModelAttributeParameterExpander expander;
private final EnumTypeDeterminer enumTypeDeterminer;

@Autowired
private DocumentationPluginsManager pluginsManager;

@Autowired
public SwaggerExpandAspect(
ModelAttributeParameterExpander expander,
EnumTypeDeterminer enumTypeDeterminer) {
this.expander = expander;
this.enumTypeDeterminer = enumTypeDeterminer;
}

@Around("execution(* springfox.documentation.spring.web.readers.operation.OperationParameterReader.apply(..))")
public Object pointCut(ProceedingJoinPoint point) throws Throwable {
Object[] args = point.getArgs();
OperationContext context = (OperationContext) args[0];
context.operationBuilder().parameters(context.getGlobalOperationParameters());
context.operationBuilder().parameters(readParameters(context));
return null;
}

private List<parameter> readParameters(final OperationContext context) {

List<resolvedmethodparameter> methodParameters = context.getParameters();
List<parameter> 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);

if (shouldExpand(methodParameter, alternate)) {
parameters.addAll(
expander.expand(
new ExpansionContext("", alternate, context)));
} else {
parameters.add(pluginsManager.parameter(parameterContext));
}
}
}
return FluentIterable.from(parameters).filter(not(hiddenParams())).toList();
}


private Predicate<parameter> hiddenParams() {
return new Predicate<parameter>() {
@Override
public boolean apply(Parameter input) {
return input.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 new Predicate<class>() {
@Override
public boolean apply(Class input) {
return parameter.hasParameterAnnotation(input);
}
};
}

private Predicate<class> isAnnotation() {
return new Predicate<class>() {
@Override
public boolean apply(Class input) {
return Annotation.class.isAssignableFrom(input);
}
};
}

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)
&& !noExpandAnnotaion(parameter);

}

private boolean noExpandAnnotaion(ResolvedMethodParameter parameter) {
log.info("开始决定是否展开问题");
if (!parameter.hasParameterAnnotation(NoSwaggerExpand.class)) {
return false;
}
NoSwaggerExpand noSwaggerExpand = (NoSwaggerExpand) parameter.getAnnotations().stream().filter(item -> item instanceof NoSwaggerExpand).findAny().orElse(null);
if (noSwaggerExpand.expand()) {
return false;
}
return true;
}

}

最重要的是这里的修改

实战篇:解决swagger和自定义参数解析器的功能冲突_spring_06

这里加上对自定义注解修饰的入参进行了判定,使得被自定义注解修饰的入参可以被​​Swagger​​​当做​​@RequestBody​​一样处理。

Definition属性值填充的逻辑调整

再定义一个切面

@Slf4j
@Aspect
@Component
public class SwaggerDefinitionAspect {

private static final Logger LOG = LoggerFactory.getLogger(OperationModelsProvider.class);
private final TypeResolver typeResolver;

@Autowired
public SwaggerDefinitionAspect(TypeResolver typeResolver) {
this.typeResolver = typeResolver;
}


@Around("execution(* springfox.documentation.spring.web.readers.operation.OperationModelsProvider.apply(..))")
public Object pointCut(ProceedingJoinPoint point) throws Throwable {
Object[] args = point.getArgs();
RequestMappingContext context = (RequestMappingContext) args[0];
collectFromReturnType(context);
collectParameters(context);
collectGlobalModels(context);
return null;
}

private void collectGlobalModels(RequestMappingContext context) {
for (ResolvedType each : context.getAdditionalModels()) {
context.operationModelsBuilder().addInputParam(each);
context.operationModelsBuilder().addReturn(each);
}
}

private void collectFromReturnType(RequestMappingContext context) {
ResolvedType modelType = context.getReturnType();
modelType = context.alternateFor(modelType);
LOG.debug("Adding return parameter of type {}", resolvedTypeSignature(modelType).or("<null>"));
context.operationModelsBuilder().addReturn(modelType);
}

private void collectParameters(RequestMappingContext context) {


LOG.debug("Reading parameters models for handlerMethod |{}|", context.getName());

List<resolvedmethodparameter> parameterTypes = context.getParameters();
for (ResolvedMethodParameter parameterType : parameterTypes) {
if (parameterType.hasParameterAnnotation(RequestBody.class)
|| parameterType.hasParameterAnnotation(RequestPart.class)
|| parameterType.hasParameterAnnotation(NoSwaggerExpand.class)
) {
ResolvedType modelType = context.alternateFor(parameterType.getParameterType());
LOG.debug("Adding input parameter of type {}", resolvedTypeSignature(modelType).or("<null>"));
context.operationModelsBuilder().addInputParam(modelType);
}
}
LOG.debug("Finished reading parameters models for handlerMethod |{}|", context.getName());
}
}

在这里只改动了一处代码,使得被自定义注解修饰的入参能够被添加到​​Definition​​属性中去。

做完以上两步,即可修复​​springmvc​​独立的参数解析器功能和​​swagger​​功能冲突的问题。

如果觉得本文对你有帮助,麻烦点赞发加关注支持一下


举报

相关推荐

0 条评论