實(shí)戰(zhàn)篇:解決 Swagger 和自定義參數(shù)解析器的功能沖突
前情提要
看了上一篇文章看了同事寫的代碼,我竟然開始默默的模仿了。。。的小伙伴,應(yīng)該已經(jīng)對使用參數(shù)解析器來完成第三方接口的統(tǒng)一驗(yàn)簽有了清晰的認(rèn)識。
我們在上文中提到過,@RequestBody使用的參數(shù)解析器RequestResponseBodyMethodProcessor優(yōu)先級高于我們自定義的參數(shù)解析器,所以為了正常使用,需要將@RequestBody 注解去掉。這就會導(dǎo)致swagger無法識別正確的參數(shù)類型,將請求體識別為Query Params,然后將body展開。
可以看到,所有參數(shù)都被識別為ModelAttribute類型(query標(biāo)志),而我們所期待的正確格式應(yīng)當(dāng)是如下樣子
因?yàn)樵摲绞娇梢源蟠筇岣叽a的可讀性和可復(fù)用性,所以我們要知難而上,找出問題,解決問題!
問題產(chǎn)生的原因
產(chǎn)生這個問題的根本原因就是spring mvc和swagger都對@RequestBody注解進(jìn)行了單獨(dú)的判定,功能上都依賴于該注解本身。
springmvc對@RequestBody注解的依賴
就拿當(dāng)前自定義的參數(shù)解析器來說,如果對請求參數(shù)加上了 @RequestBody 注解,對參數(shù)的反序列化會提前被RequestResponseBodyMethodProcessor攔截,自定義的參數(shù)解析器會失效。
具體源代碼位置: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
可以看到,該參數(shù)解析器對加上@ReuqestBody注解的參數(shù)都支持解析,然后做序列化的操作。然而它在參數(shù)解析器列表中的優(yōu)先級比較高,自定義的參數(shù)解析器添加到參數(shù)解析器列表之后會排在它的后面,所以如果加上@RequestBody注解,自定義的參數(shù)解析器就失效了。
因此使用自定義參數(shù)解析器一定不能使用@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
此案例中用到的自定義參數(shù)解析器為HdxArgumentResolver
swagger對@Requestbody的依賴
經(jīng)過調(diào)用棧追蹤,最終發(fā)現(xiàn)在兩個地方的功能會對@RequestBody注解有單獨(dú)判定!(感興趣的可以自行追蹤??)
- 請求類型判定:也就是說POST請求類型是哪種類型,這決定了入?yún)⑹欠駮鳛镽equest Parameter被展開參數(shù),也就是文中的第一張圖,整個model都被視為ModelAttribute展開了。
- Definition屬性值填充:這確保被@RequestBody注解修飾的入?yún)徽o@示,如文中第二張圖片所示。
請求類型判定
源代碼位置: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等常用注解進(jìn)行了單獨(dú)的判定,確保這些注解修飾的入?yún)⒉粫蛔鳛镽equestParam展開。
Definition屬性值填充
Definition屬性中填充了入?yún)?、出參等參?shù)類型,如果沒有相應(yīng)的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注解修飾的入?yún)⒉艜唤邮者M(jìn)入Definition屬性。
綜合以上兩張圖的源代碼分析,可以看到,swagger功能依賴于@RequestBody注解,入?yún)⑷绻槐辉撟⒔庑揎棧瑒tswagger功能就會不完整,這和在springmvc中使用獨(dú)立的參數(shù)解析器功能不得使用@RequestBody注解矛盾。
解決問題
從以上分析可以得到結(jié)論,這里的根本問題是springmvc中獨(dú)立的參數(shù)解析器功能和swagger功能上的沖突,一個要求不能加上@RequestBody注解,一個要求必須加上@RequestBody注解,所以解決方法上可以使用兩種方式
- 從springmvc入手,想辦法提高自定義參數(shù)解析器的優(yōu)先級,只要自定義的參數(shù)解析器優(yōu)先級比RequestResponseBodyMethodProcessor高,則就可以在自定義的參數(shù)上加上@RequestBody注解,swagger功能自然而然就能正常了。
- 從swagger入手,想辦法解決掉上面兩部分對@RequestBody的單獨(dú)判定,不修改springmvc相關(guān)功能也可以讓swagger功能正常。
考慮到修改springmvc功能可能會對以后的版本升級造成較大影響,這里決定利用切面修改原有的swagger對@RequestBody的兩個地方的行為,從而讓swagger功能正常。
請求類型判定的邏輯調(diào)整
首先,定義一個注解
- @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;
- }
將其加到入?yún)⑸?/p>
- @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;
- }
- }
最重要的是這里的修改
這里加上對自定義注解修飾的入?yún)⑦M(jìn)行了判定,使得被自定義注解修飾的入?yún)⒖梢员籗wagger當(dāng)做@RequestBody一樣處理。
Definition屬性值填充的邏輯調(diào)整
再定義一個切面
- @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());
- }
- }
在這里只改動了一處代碼,使得被自定義注解修飾的入?yún)⒛軌虮惶砑拥紻efinition屬性中去。
做完以上兩步,即可修復(fù)springmvc獨(dú)立的參數(shù)解析器功能和swagger功能沖突的問題。