ResponseBodyAdvice返回值统一处理当返回值为null时无效的问题原因及解决方案

问题描述

在使用Spring的ResponseBodyAdvice(@ControllerAdvice、@RestControllerAdvice)对Controller的返回值做统一业务处理时遇到了一个问题。当Controller的返回值为null时,并未触发ResponseBodyAdvice的supports和beforeBodyWrite方法,没有对Controller的返回值进行自定义的业务封装

问题研究

当出现上述问题时,发现了一个比较有意思的现象,并不是所有Controller返回值为null时都无法触发ResponseBodyAdvice的业务。开始的时候,在网上搜索了一些资料,发现有些人说是因为Controller的入参存在HttpServletResponse对象,有些人说是HttpServletRequest对象,导致Spring底层执行方法是走的路径与入参不存在HttpServletResponse对象不同,最终导致ResponseBodyAdvice无效。还有些人说这是Spring设计的失误,最终都是建议Controller的返回值不要存在null。

看完这些分析的文章,我开始困惑了。首先我这边的情况是,出现ResponseBodyAdvice无效情况的Controller入参并没有HttpServletResponse和HttpServletRequest对象,这就很奇怪了。其次,就算是Sping设计的失误,本着架构设计要尽量兼容和规范业务开发的规则,也不能仅仅建议Controller返回值不要为null就结束了,对业务开发不规范的部分没有做容错兼容,那也算不上一个合理的框架封装。为了解决我的困惑,绝对着手从底层源码对问题进行分析,走自己解决问题的道路。

由于并不是所有Controller返回值为null时都无法触发ResponseBodyAdvice的业务,对此情况做了一些模拟

    @GetMapping("/test/{value}")
    @Token(EXCEPT)
    public String test(@PathVariable("value") String value) {
        if ("null".equals(value)) {
            return null;
        }
        return value;
    }

    @GetMapping("/test1/{value}")
    @Token(EXCEPT)
    public Collection<?> test1(@PathVariable("value") String value) throws Exception {
        return null;
    }

    @GetMapping("/test1/{value}")
    @Token(EXCEPT)
    public Map<?, ?> test2(@PathVariable("value") String value) throws Exception {
        return null;
    }

    @GetMapping("/test3/{value}")
    @Token(EXCEPT)
    public Object test3(@PathVariable("value") String value) throws Exception {
        return null;
    }

对多种返回值类型的测试后发现,当返回值类型为java.lang.Object且返回值为null时,没有触发ResponseBodyAdvice的业务。

问题分析(不想看分析过程的可以直接跳过此章节)

在调试跟踪过程中,对Spring的ResponseBodyAdvice源码进行了分析
首先ResponseBodyAdvice执行时会进入AbstractMessageConverterMethodProcessor类,类的281行(下图红框部分),去执行自定义扩展ResponseBodyAdvice的beforeBodyWrite方法
在这里插入图片描述
通过debug调试,发现是因为if (selectedMediaType != null)条件为false,导致代码没有走到281行。那么继续分析selectedMediaType为null的原因
在这里插入图片描述
继续分析源码,发现selectedMediaType是根据mediaTypesToUse来设置的,因为mediaTypesToUse为空,所以selectedMediaType为null,继续分析mediaTypesToUse为空的原因
在这里插入图片描述
mediaTypesToUse是根据producibleTypes来设定的,那么再找producibleTypes的来源
在这里插入图片描述
一路跟踪源码,来到了230行,发现producibleTypes通过getProducibleMediaTypes方法获取,那么再看看getProducibleMediaTypes方法内容吧
在这里插入图片描述
还好方法内容并不多,通过阅读源码可见方法返回值是从this.messageConverters中获取的。那这个messageConverters是什么呢?
在这里插入图片描述

看到List<HttpMessageConverter<?>>东西,如果对Spring比较了解的人,应该能想到了,这个对象变量应该就是Http消息的序列化转换器,这东西是可以通过扩展WebMvcConfigurationSupport类的configureMessageConverters方法进行自定义的。再看getProducibleMediaTypes方法,可见converter.canWrite(valueClass, null)的判断结果影响到了方法的返回结果。通过方法命名可以大概猜到,Spring通过判断converter是否可以对返回值类型来执行序列化写入,来决定是否执行ResponseBodyAdvice的beforeBodyWrite方法。

这样一来问题就串起来了,底层是因为返回值类型为java.lang.Object,返回值为null时,converter不支持对其序列化,所以不执行ResponseBodyAdvice的beforeBodyWrite方法。

那么是否可以通过解决converter对java.lang.Object返回值null的序列化支持,让ResponseBodyAdvice的beforeBodyWrite方法愉快的运行起来呢?我们继续调试源码,发现返回值类型为java.lang.Object时使用的converter是MappingJackson2HttpMessageConverter,那我们就看看Jackson的canWrite是怎么写的
在这里插入图片描述
内容也不多,执行selectObjectMapper方法时,objectMapper并未为null,使用了defaultObjectMapper对象。但是再判断objectMapper.canSerialize(clazz, causeRef)时返回结果为false了,最终导致了canWrite方法判断结果为false。那么看看为什么objectMapper不能序列化java.lang.Object吧,继续看ObjectMapper类中源码
在这里插入图片描述
继续跟进到hasSerializerFor方法,发现问题的所在了
在这里插入图片描述
Jackson默认的ObjectMapper采用了SerializationFeature.FAIL_ON_EMPTY_BEANS特性,此特性的描述内容如下:

Feature that determines what happens when no accessors are found for a type (and there are no annotations to indicate it is meant to be serialized). If enabled (default), an exception is thrown to indicate these as non-serializable types; if disabled, they are serialized as empty Objects, i.e. without any properties.
Note that empty types that this feature has only effect on those “empty” beans that do not have any recognized annotations (like @JsonSerialize): ones that do have annotations do not result in an exception being thrown.
Feature is enabled by default.
翻译一下:
确定在找不到某个类型的访问器时会发生什么情况的功能(并且没有注释来指示要序列化该类型)。如果启用(默认),则会引发异常以指示这些类型为不可序列化类型;如果禁用,它们将序列化为空对象,即没有任何属性。
请注意,此功能仅对那些没有任何可识别注解的“空”bean 起作用(例如 @JsonSerialize):具有注解的空类型不会导致抛出异常。
默认情况下,该功能处于启用状态。

根据特性描述发现,这个特性是可以禁用了。那么好办了,在扩展messageConverters的地方把MappingJackson2HttpMessageConverter的FAIL_ON_EMPTY_BEANS特性禁用就可以了~

解决方案

根据问题的分析结果,我们来到系统中继承了WebMvcConfigurationSupport类并且重写configureMessageConverters方法的地方,对MappingJackson2HttpMessageConverter的ObjectMapper进行设置

@Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new StringHttpMessageConverter(StandardCharsets.UTF_8));
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
        converters.add(new MappingJackson2HttpMessageConverter(objectMapper));
        super.addDefaultHttpMessageConverters(converters);
    }

使用objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS),禁用FAIL_ON_EMPTY_BEANS特性。

然后回到上文中提到的测试类中,对多种返回值类型且返回值为null的情况进行测试,测试结构都是返回值经过统一处理以后的内容,完美执行了ResponseBodyAdvice的beforeBodyWrite方法。至此问题得到解决。

附录A ResponseBodyAdvice方法

@RestControllerAdvice
public class GlobalResponseAdvice implements ResponseBodyAdvice<Object> {

    /**
     * 全局返回值统一处理判定
     *
     * @param returnType    方法参数
     * @param converterType 转换器类型
     * @return boolean
     * @author shaoshen
     */
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        if (!returnType.hasMethodAnnotation(Token.class)) {
            return false;
        }
        Token token = returnType.getMethodAnnotation(Token.class);
        if (Objects.isNull(token)) {
            return false;
        }
        return token.responseAdvice();
    }

    /**
     * 全局返回值统一处理
     *
     * @param body          返回值对象
     * @param returnType    方法类型
     * @param contentType   媒体类型
     * @param converterType 转换器类型
     * @param request       请求
     * @param response      响应
     * @return java.lang.Object
     * @author shaoshen
     */
    @Override
    public Object beforeBodyWrite(Object body,
                                  MethodParameter returnType,
                                  MediaType contentType,
                                  Class<? extends HttpMessageConverter<?>> converterType,
                                  ServerHttpRequest request,
                                  ServerHttpResponse response) {
        if (converterType == StringHttpMessageConverter.class) {
            return ResponseUtil.renderResponseBody(StatusCode.OK.code(), null, body);
        }
        if (body instanceof ResponseBody value) {
            return value;
        } else if (this.isPageInfo()) {
            PageEntity page = (PageEntity) GlobalThreadLocal.getValue().get(SystemConstant.SYS_THREADLOCAL_PAGE_INFO);
            page.setRows((List<?>) body);
            return new ResponseBody(StatusCode.OK.code(), null, page);
        } else if (body instanceof Boolean value) {
            return new ResponseBody(value ? StatusCode.OK.code() : StatusCode.ERROR.code(), null, value);
        } else if (body instanceof StatusCode value) {
            return new ResponseBody(value.code(), null, value == StatusCode.OK);
        }
        return new ResponseBody(StatusCode.OK.code(), null, body);
    }

    /**
     * 判定是否为分页信息
     *
     * @return boolean
     * @author shaoshen
     */
    private boolean isPageInfo() {
        return GlobalThreadLocal.getValue().get(SystemConstant.SYS_THREADLOCAL_PAGE_INFO) != null;
    }

}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值