转载

Spring MVC 统一响应格式(源码分析以及问题解决)

快速上手

一般我们在写服务的时候,会有统一的返回格式,在这篇文章中用 RespMsg 表示。

比如根据用户的id要获取用户的信息。

第一版代码如下

@PostMapping(value = "user/{id}")
public RespMsg getUserById(@PathVariable Long id) {
    try {
        return RespMsg.success(userService.getUserById(id));
    } catch (BusinessException e) {
        log.error(e.getMessage(), e);
        return RespMsg.failed(e.getErrCode(), e.getMessage());
    }
}

看了上一篇 你真的了解spring boot全局异常处理吗 ,就可以改写为第二版

@PostMapping(value = "user/{id}")
public RespMsg getUserById(@PathVariable Long id) throws BusinessException {
    return RespMsg.success(userService.getUserById(id));
}

看了这篇,就可以改写为第三版

@PostMapping(value = "user/{id}")
public User getUserById(@PathVariable Long id) throws BusinessException {
    return userService.getUserById(id);
}

实现方式也很简单,通过实现 ResponseBodyAdvice 搭配 @ControllerAdvice 即可

@ControllerAdvice
public class ResponseWrapperAdvice implements ResponseBodyAdvice {

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        //如果已经是RespMsg类型的则直接返回
        if (body instanceof RespMsg) {
            return body;
        }
        //如果不是,则封装
        return RespMsg.success(body);
    }
}
  • supports 方法用于判断是否支持,如果不支持则不会调用 beforeBodyWrite 。具体可见 RequestResponseBodyAdviceChainprocessBody 方法。既然是统一,这里都返回true了。
  • beforeBodyWrite 方法用于改写响应。

然后就大功告成了,so easy。

问题分析

上面的其实有一点小坑,下面详细的分析。

匹配到错误的HttpMessageConverter

如果接口返回的是 String ,比如

@PostMapping(value = "test")
public String test() {
    return "test";
}

就会报错

Spring MVC 统一响应格式(源码分析以及问题解决)

从我们定义的 RespMsg 强转为 String 报错,从异常信息信息可以看出报错发生在 StringHttpMessageConvertergetContentLength 方法,跟了代码发现是在 AbstractHttpMessageConverteraddDefaultHeaders 中,调用了 getContentLength 方法,而这个方法被 StringHttpMessageConverter 方法重写了,在 AbstractHttpMessageConvertergetContentLength 声明如下:

protected Long getContentLength(T t, @Nullable MediaType contentType) throws IOException

StringHttpMessageConverter 中重写时的声明变为了:

protected Long getContentLength(String str, @Nullable MediaType contentType)

注意第一个类型变量发生了变化,报错也是因为这里有个隐式的类型转换,跟代码时这里第一个变量其实已经被封装为 RespMsg ,强转String肯定会报错。

这个顺序在 ResponseBodyAdvice 的注释中有提到

Allows customizing the response after the execution of an @ResponseBody or a ResponseEntity controller method but before the body is written with an HttpMessageConverter.

允许在被 @ResponseBody 注解或者返回 ResponseEntity 的Controller方法执行后自定义响应,但是是在 HttpMessageConverter 写之前。

具体代码可以参考 AbstractMessageConverterMethodProcessorwriteWithMessageConverters 方法。

等等,为什么是 StringHttpMessageConverter ,而不是 MappingJackson2HttpMessageConverter

还是 AbstractMessageConverterMethodProcessorwriteWithMessageConverters 方法中有如下代码:

Spring MVC 统一响应格式(源码分析以及问题解决)

会按 messageConverters 顺序检测是否 canWrite ,至于为什么 StringHttpMessageConverter 能匹配上,因为Controller中的方法返回的是String类型。

首先想到的解决办法是改变一下 messageConverters 里面元素的顺序,找到了加载 messageConverters 代码,具体可见 RequestMappingHandlerAdapter 的构造方法。

public RequestMappingHandlerAdapter() {
    StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
    stringHttpMessageConverter.setWriteAcceptCharset(false);  // see SPR-7316

    this.messageConverters = new ArrayList<>(4);
    this.messageConverters.add(new ByteArrayHttpMessageConverter());
    this.messageConverters.add(stringHttpMessageConverter);
    try {
        this.messageConverters.add(new SourceHttpMessageConverter<>());
    }
    catch (Error err) {
        // Ignore when no TransformerFactory implementation is available
    }
    this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
}

可以看到 StringHttpMessageConverter 是第二个元素。

想改变 messageConverters 的顺序,可以通过实现 WebMvcConfigurerconfigureMessageConverters 方法,调整list中的元素位置。我试了一下,调整完之后不会报错了,但是会不会引起别的问题还不确定,毕竟这是一个较大的改动。

第二种方案就是在Controller层中包装为 RespMsg ,这样就避免了匹配到 StringHttpMessageConverter 。这种方案可能更加安全。

ResponseBodyAdvice接口的泛型处理

注意到 ResponseBodyAdvice 是支持泛型的,刚才为了统一返回,所以写了 Object ,但是如果你只想用于某些类型,就可以直接指定类型,比如

@ControllerAdvice
public class UserWrapperAdvice implements ResponseBodyAdvice<User> {

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return false;
    }

    @Override
    public User beforeBodyWrite(User body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        return null;
    }
}

这时需要注意的是一定要在 supports 方法中判断什么类型支持,什么类型不支持,spring并不会根据你指定的类型判断调不调用 beforeBodyWrite 方法,只会根据 supports 的返回判断,比如说我返回了一个 RespMsg ,而 supports 返回 true ,然后就会调用 beforeBodyWrite ,这时会发生强制转换( RespMsg -> User ),就会报错了。

总结

  1. 可以通过实现 ResponseBodyAdvice 搭配 @ControllerAdvice 来统一数据的返回格式。(配合统一异常处理)
  2. 根据方法的返回类型,可能导致匹配到错误的 HttpMessageConverter ,这时可以通过修改返回类型避免这种错误。
  3. ResponseBodyAdvice 接口支持泛型,需要严格的判断类型是否符合,否则可能会因为强转不成功报错。
原文  https://segmentfault.com/a/1190000020891122
正文到此结束
Loading...