解決swagger和自定義參數解析器的功能衝突問題

在上一篇文章spring mvc請求體偷樑換柱:HandlerMethodArgumentResolver
中,講解了如何使用spring mvc中的參數解析器解密傳入的字符串並反序列化的方法,大大提高了代碼的可讀性和可複用性,但是卻遺留了一個問題:springmvc自定義參數解析器的參數上不能再帶上@RequestBody註解,否則會被RequestResponseBodyMethodProcessor類提前適配參數類型,自定義的參數解析器就會失效,這帶來的後果就是swagger會無法識別正確的參數類型,將請求體識別爲Query Params,然後將body展開

image-20211011154924712

可以看到,所有參數都被識別爲ModelAttribute類型(query標誌)。正確的格式應當是如下樣子

2021-10-11_155520

一、問題產生的原因

產生這個問題的根本原因就是spring mvc和swagger都對@RequestBody註解進行了單獨的判定,功能上都依賴於該註解本身。

1.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

image-20211011160848305

可以看到,該參數解析器對加上@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

image-20211009151851091

2.swagger對@Requestbody的依賴

經過調用棧追蹤,最終發現在兩個地方的功能會對@RequestBody註解有單獨判定

  • 請求類型判定,也就是說POST請求請求類型是哪種類型,這決定了入參是否會作爲Request Parameter被展開參數,也就是開篇文章中的第一張圖,整個model都被視爲ModelAttribute展開了。
  • Definition屬性值填充,這確保被@RequestBody註解修飾的入參會被正常顯示,如本篇文章第二張圖片所示。

2.1 請求類型判定

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

image-20211011163142881

這裏對RequestBody等常用註解進行了單獨的判定,確保這些註解修飾的入參不會被作爲RequestParam展開。

2.2 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

image-20211011164020729

可以看到,只有被RequestBody註解和RequestPart註解修飾的入參纔會被接收進入Definition屬性。

綜合2.1和2.2的源代碼分析,可以看到,swagger功能依賴於@RequestBody註解,入參如果不被該註解修飾,則swagger功能就會不完整,這和在springmvc中使用獨立的參數解析器功能不得使用@RequestBody註解矛盾。

二、解決問題

從以上分析可以得到結論,這裏的根本問題是springmvc中獨立的參數解析器功能和swagger功能上的衝突,一個要求不能加上@RequestBody註解,一個要求必須加上@RequestBody註解,所以解決方法上可以使用兩種方式

  • 從springmvc入手,想辦法提高自定義參數解析器的優先級,只要自定義的參數解析器優先級比RequestResponseBodyMethodProcessor高,則就可以在自定義的參數上加上@RequestBody註解,swagger功能自然而然就能正常了。
  • 從swagger入手,想辦法解決掉上面兩部分對@RequestBody的單獨判定,不修改springmvc相關功能也可以讓swagger功能正常。

考慮到修改springmvc功能可能會對以後的版本升級造成較大影響,這裏決定利用切面修改原有的swagger對@RequestBody的兩個地方的行爲,從而讓swagger功能正常。

1.請求類型判定的邏輯調整

首先,定義一個註解

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

然後定義切面

import com.cosmoplat.qdind.swagger.annotation.NoSwaggerExpand;
import com.fasterxml.classmate.ResolvedType;
import com.google.common.base.Predicate;
import com.google.common.collect.FluentIterable;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
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.ParameterBuilder;
import springfox.documentation.service.Parameter;
import springfox.documentation.service.ResolvedMethodParameter;
import springfox.documentation.spi.schema.EnumTypeDeterminer;
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.parameter.ExpansionContext;
import springfox.documentation.spring.web.readers.parameter.ModelAttributeParameterExpander;

import java.lang.annotation.Annotation;
import java.util.List;
import java.util.Optional;
import java.util.Set;

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;

/**
 * @author kdyzm
 * @date 2021/10/11
 */
@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;
    }

}

最重要的是這裏的修改

image-20211011165542819

這裏加上對自定義註解修飾的入參進行了判定,使得被自定義註解修飾的入參可以被Swagger當做@RequestBody一樣處理。

2.Definition屬性值填充的邏輯調整

再定義一個切面

import com.cosmoplat.qdind.swagger.annotation.NoSwaggerExpand;
import com.fasterxml.classmate.ResolvedType;
import com.fasterxml.classmate.TypeResolver;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestPart;
import springfox.documentation.service.ResolvedMethodParameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.OperationContext;
import springfox.documentation.spi.service.contexts.RequestMappingContext;
import springfox.documentation.spring.web.readers.operation.OperationModelsProvider;

import java.util.List;

import static springfox.documentation.schema.ResolvedTypes.resolvedTypeSignature;

/**
 * @author kdyzm
 * @date 2021/10/11
 */
@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屬性中去。

image-20211011165919011

做完以上兩步,即可修復springmvc獨立的參數解析器功能和swagger功能衝突的問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章