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

Posted Java爱好狂.

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实战篇:解决swagger和自定义参数解析器的功能冲突相关的知识,希望对你有一定的参考价值。

前言

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

 

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

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

问题产生的原因

产生这个问题的根本原因就是spring mvcswagger都对@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

可以看到,该参数解析器对加上@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

此案例中用到的自定义参数解析器为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

可以看到,只有被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当做@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功能冲突的问题。

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

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

Swagger OpenApi 动态查询参数名称使用 Spring 自定义解析器的注释

【中文标题】Swagger OpenApi 动态查询参数名称使用 Spring 自定义解析器的注释【英文标题】:Swagger OpenApi dynamic query param name using annotation for Spring custom resolver 【发布时间】:2021-02-14 23:03:50 【问题描述】:

我试图弄清楚如何向SpringDoc OpenApi 表明正在使用HandlerMethodArgumentResolver 生成自定义对象,该对象拦截一些传入的请求参数并从中创建一个对象。这个概念实际上和 Spring 对@Pageable 所做的一样。

这是我的自定义对象的一部分:

class FlightRequestHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver 
FlightRequestHandlerMethodArgumentResolver() 


@Override
boolean supportsParameter(final MethodParameter parameter) 
    return FlightRequest.class.equals(parameter.getParameterType());


@Override
Object resolveArgument(
        final MethodParameter parameter,
        final ModelAndViewContainer mavContainer,
        final NativeWebRequest webRequest, final WebDataBinderFactory binderFactory) throws Exception 
    final Map<String, String[]> parameterMap = webRequest.getParameterMap()
    // query param names are configurable
    // implementation about finding query param names is ommited
    return new FlightRequest(parameterMap)

FlightRequest类:

public class FlightRequest 

@Parameter(name = "startDate")
private final LocalDate startDate;

@Parameter(name = "endDate")
private final LocalDate endDate;

FlightRequest (LocalDate startDate, LocalDate endDate) 
    this.submissionStartDate = submissionStartDate;
    this.submissionEndDate = submissionEndDate;

使用 FlightRequest 和默认查询参数名称的控制器:

@RequestMapping(value = "/flights", method = RequestMethod.GET)
Flights flights(FlightRequest flightRequest, HttpServletResponse response) 
    return flightSearchService.retrieveFlights(flightRequest);

使用带有自定义查询参数名称的FlightRequest 的控制器:

@RequestMapping(value = "/current-flights", method = RequestMethod.GET)
Flights flights(@FlightRequestParams(fromDateParamName="current-flights-from", toDateParamName="current-flights-till) FlightRequest flightRequest, HttpServletResponse response) 
    return flightSearchService.retrieveFlights(flightRequest);

就像您在上面的示例中看到的那样,每个端点都可以覆盖默认查询参数名称。 如何向Spring Doc 表明这一点?

【问题讨论】:

这个问题已经有一段时间了。请张贴在Springdoc - issues。如果问题已经公开,请务必将其放在问题的 cmets 中。 【参考方案1】:

看来您在这里使用的不是 spring 注释,而是您自己的:@FlightRequestParams。

如果您真的希望动态添加默认值,那么您可以为您的注释DelegatingMethodParameterCustomizer 编写自己的注释,就像为@PageableDefault 所做的那样。 然后将其声明为一个spring Bean,为follow。

或者最简单的方法,就是使用@Parameter注解来声明参数值。可以看PageableAsQueryParam的例子。

【讨论】:

以上是关于实战篇:解决swagger和自定义参数解析器的功能冲突的主要内容,如果未能解决你的问题,请参考以下文章

伪类和自定义选择器的相对性能是啥?

Springfox集成swagger实战篇

Android进阶之注解解析和自定义注解

关于路径和自定义装饰器的 Python3 Django 问题

hadoop调度器的原理和应用场景解析

.NET Core基础篇之:集成Swagger文档与自定义Swagger UI