考虑到多人协作与代码封装性,我将Controller方法的返回值和HTTP请求最终的返回值做了隔离,也就是在开发过程中在Controller的方法中返回都是具备业务含义的POJO,但是在最终通过HTTP返回的时候都会被包装成如下格式:
public class Response <T > { private int code; private String msg; private T data; }
其中的data就是实际的业务内容,要实现这个效果很简单,就是Spring的APO,通过自定义ResponseBodyAdvice,在beforeBodyWrite方法中包装内容,可以在supports中加一些过滤条件,例如如果有某个注解就不包装等,提示灵活性,统一的错误处理在handleTException方法中。
@RestControllerAdvice public class APIResponseAdvice implements ResponseBodyAdvice <Object > { private int codeOk = 0 ; private int codeErr = 1 ; @ExceptionHandler (Exception.class ) public MetadataResponse handleTException (HttpServletRequest request , TException ex ) { log.error("process url {} failed" , request.getRequestURL().toString(), ex); return Response.builder().code(codeErr).msg(ex.getMessage()).build(); } @Override public boolean supports (MethodParameter returnType, @NotNull Class converterType) { return returnType.getParameterType() != Response.class && AnnotationUtils .findAnnotation (Objects .requireNonNull (returnType .getMethod ()), NotWrapperResponse .class ) == null && AnnotationUtils.findAnnotation(returnType.getDeclaringClass(), NotWrapperResponse.class ) == null ; } @Override public Object beforeBodyWrite (Object body, @NotNull MethodParameter returnType, @NotNull MediaType selectedContentType, @NotNull Class<? extends HttpMessageConverter<?>> selectedConverterType, @NotNull ServerHttpRequest request, @NotNull ServerHttpResponse response) { return Response.builder() .code(codeOk).data(body).build(); } }
NotWrapperResponse就是一个标记,如果Controller的方法上出现这个注解则不进行包装。
@Target ({ElementType.METHOD, ElementType.TYPE})@Retention (RetentionPolicy.RUNTIME)public @interface NotWrapperResponse {}
看起来没有什么问题,不过实际在运行的时候出现一个问题,就是如果返回值是String会出现这样的问题:
java.lang.ClassCastException: com.xxx.Response cannot be cast to java.lang.String at org.springframework.http.converter.StringHttpMessageConverter.addDefaultHeaders(StringHttpMessageConverter.java:44 ) ~[spring-web-5.2 .5 .RELEASE.jar:5.2 .5 .RELEASE]
先说原因,在beforeBodyWrite方法中有一个参数是selectedConverterType,这个参数就是对内容解析的对象,正常来说如果是Pojo的话,他的解析对象是MappingJackson2HttpMessageConverter,但是恰巧如果是String返回值的话,默认生效的是org.springframework.http.converter.StringHttpMessageConverter,也就是返回值类型为字符串的时候,mvc框架就会将其跳转到路径为返回内容的地址。
如果将返回值类型改为int后,可以发现mvc框架选中的转换器也是org.springframework.http.converter.json.MappingJackson2HttpMessageConverter。 那么问题是找到了,返回字符串的时候采用的是特殊的StringHttpMessageConverter转换器,而其他格式则是采用MappingJackson2HttpMessageConverter转换器来解析的,StringHttpMessageConverter转换器将内容转为String字符串,而当前我们返回的则是一个具体的对象,这就导致了报错的根本原因也就是类型转换异常。
要解决这个问题可以直接把转换器的级别提升,也就是在创建WebMvcConfigurer对象的时候配置转换器,如下:
@Bean public WebMvcConfigurer createWebMvcConfigurer (@Autowired HandlerInterceptor[] interceptors) { return new WebMvcConfigurer() { public void addInterceptors (InterceptorRegistry registry) { for (HandlerInterceptor interceptor : interceptors) { registry.addInterceptor(interceptor); } } @Override public void configureMessageConverters (List<HttpMessageConverter<?>> converters) { converters.add(0 , new MappingJackson2HttpMessageConverter()); } }; }
将MappingJackson2HttpMessageConverter的优先级提高,当然也可以在AOP的时候重新选择,而不是直接这样写,不过都能搞定,重要的是理解为什么会出现这个问题就行。
但是,因为我是在一个庞大的遗留系统上开发,这样修改后,解决了返回值的问题,随之而以来又引入另一个问题,那就是如果Controller里面出现:
public XXXX cancelQuery (@RequestBody String id) { }
会导致出现Json解析错误,因为前面的修改方案会让所有字符串的解析都走org.springframework.http.converter.json.MappingJackson2HttpMessageConverter,则在RequestBody的时候也会使用Json的converter,而这里的id是个纯字符串,会出现json的解析错误。
所以最后是在beforeBodyWrite中,使用:
if (body instanceof String) { return JSON.toJSONString(Response.builder() .code(codeOk).data(body).build()); }
来判断,然后转成Json,这里也有一个潜在的不完美就是这里虽然toJson了,但是走的还是String的Convert,则会导致整个Json内容以字符串的形式返回给前端。
扫码手机观看或分享: