Practical part: solve the function conflict between swagger and user-defined parameter parser

previously on

Read the last article After reading the code written by my colleagues, I began to imitate silently... You should have a clear understanding of using the parameter parser to complete the unified signature verification of the third-party interface.

As mentioned above, the parameter parser RequestResponseBodyMethodProcessor used by @ RequestBody takes precedence over our customized parameter parser. Therefore, in order to use it normally, the @ RequestBody annotation needs to be removed. This will cause swagger to fail to recognize the correct parameter type, recognize the request body as Query Params, and then expand the body.

You can see that all parameters are recognized as ModelAttribute type (query flag), and the correct format we expect should be as follows

Because this method can greatly improve the readability and reusability of the code, we should overcome difficulties, find out and solve problems!

Causes of problems

The root cause of this problem is that both spring mvc and swagger have made a separate judgment on the @ RequestBody annotation, which functionally depends on the annotation itself.

springmvc's dependence on @ RequestBody annotation

Take the current customized parameter parser for example. If the @ RequestBody annotation is added to the request parameters, the deserialization of the parameters will be intercepted by the RequestResponseBodyMethodProcessor in advance, and the customized parameter parser will fail.

Specific source code location: 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

You can see that the parameter parser supports parsing the parameters annotated with @ ReuqestBody, and then serialization. However, it has a high priority in the parameter parser list, and the user-defined parameter parser will rank behind it after being added to the parameter parser list. Therefore, if the @ RequestBody annotation is added, the user-defined parameter parser will fail.

Therefore, the @ RequestBody annotation must not be used by the parser using custom parameters

The following figure shows the source code location: https://github.com/spring-projects/spring-framework/blob/5.2.x/spring-web/src/main/java/org/springframework/web/method/support/HandlerMethodArgumentResolverComposite.java#L129

The custom parameter resolver used in this case is HdxArgumentResolver

swagger's dependence on @ Requestbody

After tracing the call stack, it is finally found that the functions in two places will judge the @ RequestBody annotation separately! (those interested can be tracked by themselves 😃)

  • Determination of request type: that is, what type of POST request type is, which determines whether the input parameter will be expanded as a Request Parameter, that is, in the first figure in the text, the whole model is regarded as ModelAttribute expanded.
  • Make sure that the value of @ request attribute will be filled in the second picture as shown in the comment.

Request type determination

Source code location: https://github.com/springfox/springfox/blob/2.9.2/springfox-spring-web/src/main/java/springfox/documentation/spring/web/readers/operation/OperationParameterReader.java#L151

Here, a separate judgment is made for common annotations such as RequestBody to ensure that the input parameters modified by these annotations will not be expanded as RequestParam.

Definition property value filling

The Definition attribute is filled with parameter types such as input parameter and output parameter. If there is no corresponding Model Definition, the swagger information will be incomplete and the display in the browser page will be incomplete. The logic for filling in the Definition also depends on the @ RequestBody annotation.

Source code location: https://github.com/springfox/springfox/blob/2.9.2/springfox-spring-web/src/main/java/springfox/documentation/spring/web/readers/operation/OperationModelsProvider.java#L80

As you can see, only the input parameters modified by the RequestBody annotation and RequestPart annotation will be received into the Definition attribute.

Based on the source code analysis of the above two figures, we can see that the swagger function depends on the @ RequestBody annotation. If the input parameter is not modified by the annotation, the swagger function will be incomplete, which is in contradiction with the use of the independent parameter parser function in spring MVC and the use of @ RequestBody annotation is not allowed.

solve the problem

It can be concluded from the above analysis that the fundamental problem here is the conflict between the independent parameter parser function and swagger function in spring MVC. One request cannot be annotated with @ RequestBody, and the other request must be annotated with @ RequestBody. Therefore, two methods can be used to solve the problem

  • Start with spring MVC and find ways to improve the priority of custom parameter parser. As long as the priority of custom parameter parser is higher than RequestResponseBodyMethodProcessor, you can add @ RequestBody annotation to the custom parameters, and the swagger function will naturally work normally.
  • Start with swagger and try to solve the separate judgment of @ RequestBody in the above two parts. You can make swagger function normally without modifying the relevant functions of spring MVC.

Considering that modifying the spring MVC function may have a great impact on future version upgrades, it is decided to use the aspect to modify the behavior of the original swagger on two places of @ RequestBody, so as to make the swagger function normal.

Logical adjustment of request type determination

First, define an annotation

@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;
}

Add it to the input parameter

    @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;
    }

Then define the section

@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("Start deciding whether to expand the problem");
        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;
    }

}

The most important thing is the modification here

Here, the input parameters modified by the user-defined annotation are judged, so that the input parameters modified by the user-defined annotation can be processed by Swagger as @ RequestBody.

Logical adjustment of Definition attribute value filling

Define another section

@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());
    }
}

There is only one code change here, so that the input parameter modified by the custom annotation can be added to the Definition attribute.

After completing the above two steps, you can fix the conflict between spring MVC's independent parameter parser function and swagger function.

That's all for today

Added by jemrys on Wed, 16 Feb 2022 04:50:38 +0200