一般我们在写服务的时候,会有统一的返回格式,在这篇文章中用 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
。具体可见 RequestResponseBodyAdviceChain
的 processBody
方法。既然是统一,这里都返回true了。 beforeBodyWrite
方法用于改写响应。 然后就大功告成了,so easy。
上面的其实有一点小坑,下面详细的分析。
如果接口返回的是 String
,比如
@PostMapping(value = "test") public String test() { return "test"; }
就会报错
从我们定义的 RespMsg
强转为 String
报错,从异常信息信息可以看出报错发生在 StringHttpMessageConverter
的 getContentLength
方法,跟了代码发现是在 AbstractHttpMessageConverter
的 addDefaultHeaders
中,调用了 getContentLength
方法,而这个方法被 StringHttpMessageConverter
方法重写了,在 AbstractHttpMessageConverter
的 getContentLength
声明如下:
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
写之前。
具体代码可以参考 AbstractMessageConverterMethodProcessor
的 writeWithMessageConverters
方法。
等等,为什么是 StringHttpMessageConverter
,而不是 MappingJackson2HttpMessageConverter
还是 AbstractMessageConverterMethodProcessor
的 writeWithMessageConverters
方法中有如下代码:
会按 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
的顺序,可以通过实现 WebMvcConfigurer
的 configureMessageConverters
方法,调整list中的元素位置。我试了一下,调整完之后不会报错了,但是会不会引起别的问题还不确定,毕竟这是一个较大的改动。
第二种方案就是在Controller层中包装为 RespMsg
,这样就避免了匹配到 StringHttpMessageConverter
。这种方案可能更加安全。
注意到 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
),就会报错了。
ResponseBodyAdvice
搭配 @ControllerAdvice
来统一数据的返回格式。(配合统一异常处理) HttpMessageConverter
,这时可以通过修改返回类型避免这种错误。 ResponseBodyAdvice
接口支持泛型,需要严格的判断类型是否符合,否则可能会因为强转不成功报错。