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

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

哈喽大家好,我是阿Q!点星标不迷路😃

前情提要

看了上一篇文章看了同事写的代码,我竟然开始默默的模仿了。。。的小伙伴,应该已经对使用参数解析器来完成第三方接口的统一验签有了清晰的认识。

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

image.png

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

image.png

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

问题产生的原因

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

image.gifimage.png

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

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

image.gifimage.png

这里对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

image.gifimage.png

可以看到,只有被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;
    }
}

最重要的是这里的修改

image.gifimage.png

这里加上对自定义注解修饰的入参进行了判定,使得被自定义注解修饰的入参可以被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功能冲突的问题。

ideaQQ

 

相关文章
|
1月前
|
存储 缓存 算法
HashMap深度解析:从原理到实战
HashMap,作为Java集合框架中的一个核心组件,以其高效的键值对存储和检索机制,在软件开发中扮演着举足轻重的角色。作为一名资深的AI工程师,深入理解HashMap的原理、历史、业务场景以及实战应用,对于提升数据处理和算法实现的效率至关重要。本文将通过手绘结构图、流程图,结合Java代码示例,全方位解析HashMap,帮助读者从理论到实践全面掌握这一关键技术。
96 14
|
11天前
|
搜索推荐 UED Python
实现一个带有昼夜背景切换的动态时钟:从代码到功能解析
本文介绍了一个使用Python和Tkinter库实现的动态时钟程序,具有昼夜背景切换、指针颜色随机变化及整点和半点报时功能。通过设置不同的背景颜色和随机变换指针颜色,增强视觉吸引力;利用多线程技术确保音频播放不影响主程序运行。该程序结合了Tkinter、Pygame、Pytz等库,提供了一个美观且实用的时间显示工具。欢迎点赞、关注、转发、收藏!
127 94
|
3天前
|
运维 Shell 数据库
Python执行Shell命令并获取结果:深入解析与实战
通过以上内容,开发者可以在实际项目中灵活应用Python执行Shell命令,实现各种自动化任务,提高开发和运维效率。
39 20
|
13天前
|
供应链 搜索推荐 API
深度解析1688 API对电商的影响与实战应用
在全球电子商务迅猛发展的背景下,1688作为知名的B2B电商平台,为中小企业提供商品批发、分销、供应链管理等一站式服务,并通过开放的API接口,为开发者和电商企业提供数据资源和功能支持。本文将深入解析1688 API的功能(如商品搜索、详情、订单管理等)、应用场景(如商品展示、搜索优化、交易管理和用户行为分析)、收益分析(如流量增长、销售提升、库存优化和成本降低)及实际案例,帮助电商从业者提升运营效率和商业收益。
101 20
|
1月前
|
物联网 调度 vr&ar
鸿蒙HarmonyOS应用开发 |鸿蒙技术分享HarmonyOS Next 深度解析:分布式能力与跨设备协作实战
鸿蒙技术分享:HarmonyOS Next 深度解析 随着万物互联时代的到来,华为发布的 HarmonyOS Next 在技术架构和生态体验上实现了重大升级。本文从技术架构、生态优势和开发实践三方面深入探讨其特点,并通过跨设备笔记应用实战案例,展示其强大的分布式能力和多设备协作功能。核心亮点包括新一代微内核架构、统一开发语言 ArkTS 和多模态交互支持。开发者可借助 DevEco Studio 4.0 快速上手,体验高效、灵活的开发过程。 239个字符
223 13
鸿蒙HarmonyOS应用开发 |鸿蒙技术分享HarmonyOS Next 深度解析:分布式能力与跨设备协作实战
|
1月前
|
自然语言处理 搜索推荐 数据安全/隐私保护
鸿蒙登录页面好看的样式设计-HarmonyOS应用开发实战与ArkTS代码解析【HarmonyOS 5.0(Next)】
鸿蒙登录页面设计展示了 HarmonyOS 5.0(Next)的未来美学理念,结合科技与艺术,为用户带来视觉盛宴。该页面使用 ArkTS 开发,支持个性化定制和无缝智能设备连接。代码解析涵盖了声明式 UI、状态管理、事件处理及路由导航等关键概念,帮助开发者快速上手 HarmonyOS 应用开发。通过这段代码,开发者可以了解如何构建交互式界面并实现跨设备协同工作,推动智能生态的发展。
147 10
鸿蒙登录页面好看的样式设计-HarmonyOS应用开发实战与ArkTS代码解析【HarmonyOS 5.0(Next)】
|
1月前
|
设计模式 XML Java
【23种设计模式·全精解析 | 自定义Spring框架篇】Spring核心源码分析+自定义Spring的IOC功能,依赖注入功能
本文详细介绍了Spring框架的核心功能,并通过手写自定义Spring框架的方式,深入理解了Spring的IOC(控制反转)和DI(依赖注入)功能,并且学会实际运用设计模式到真实开发中。
【23种设计模式·全精解析 | 自定义Spring框架篇】Spring核心源码分析+自定义Spring的IOC功能,依赖注入功能
|
11天前
|
人工智能 自然语言处理 搜索推荐
销售易、悟空、神州云动CRM:全方位功能解析与优势特色盘点
销售易CRM、悟空CRM和神州云动CRM各自具备独特的产品功能与优势,适用于不同类型的企业。销售易CRM提供移动化、社交化和AI驱动的全流程管理,适合大型企业及跨国公司;悟空CRM以智能数据分析和移动办公支持见长,适合中大型企业;神州云动CRM则凭借灵活定制和多行业适配能力,特别适合大中型企业。企业在选择时应根据自身需求和发展战略,挑选最适合的CRM系统,以实现客户关系管理的最大化效益。
|
18天前
|
数据采集 XML API
深入解析BeautifulSoup:从sohu.com视频页面提取关键信息的实战技巧
深入解析BeautifulSoup:从sohu.com视频页面提取关键信息的实战技巧
|
24天前
|
存储 数据库 对象存储
新版本发布:查询更快,兼容更强,TDengine 3.3.4.3 功能解析
经过 TDengine 研发团队的精心打磨,TDengine 3.3.4.3 版本正式发布。作为时序数据库领域的领先产品,TDengine 一直致力于为用户提供高效、稳定、易用的解决方案。本次版本更新延续了一贯的高标准,为用户带来了多项实用的新特性,并对系统性能进行了深度优化。
43 3

热门文章

最新文章

推荐镜像

更多