一个项目使用了SpringBoot,需要对Controller的返回值进行二次包装。包装类结构大致如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
import org.springframework.http.HttpStatus; public class Result { private int status = HttpStatus.OK.value(); private Object content; public Result(Object content) { this.content = content; } public Result(int status, String content) { this(content); this.content = content; } public int getStatus() { return status; } public void setStatus(int status) { this.status = status; } public Object getContent() { return content; } public void setContent(Object content) { this.content = content; } } |
通过查找资料,找到了两种封装方式。
方法一
第一种方式是替换掉RequestResponseBodyMethodProcessor,这需要使用一个MethodReturnValueHandler的装饰类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import org.springframework.core.MethodParameter; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodReturnValueHandler; import org.springframework.web.method.support.ModelAndViewContainer; public class ResponseBodyWrapHandler implements HandlerMethodReturnValueHandler { private final HandlerMethodReturnValueHandler delegate; public ResponseBodyWrapHandler(HandlerMethodReturnValueHandler delegate) { this.delegate = delegate; } @Override public boolean supportsReturnType(MethodParameter returnType) { return delegate.supportsReturnType(returnType); } @Override public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { Result result = new Result(returnValue); delegate.handleReturnValue(result, returnType, mavContainer, webRequest); } } |
在装饰类中使用一个Result类的实例替换了returnValue。而后在InitializingBean中基于原来的RequestResponseBodyMethodProcessor的实例创建一个ResponseBodyWrapHandler的实例来完成替换:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.method.support.HandlerMethodReturnValueHandler; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor; import java.util.ArrayList; import java.util.List; public class ResponseBodyWrapFactoryBean implements InitializingBean { @Autowired private RequestMappingHandlerAdapter adapter; @Override public void afterPropertiesSet() throws Exception { List<HandlerMethodReturnValueHandler> returnValueHandlers = adapter.getReturnValueHandlers(); List<HandlerMethodReturnValueHandler> handlers = new ArrayList(returnValueHandlers); decorateHandlers(handlers); adapter.setReturnValueHandlers(handlers); } private void decorateHandlers(List<HandlerMethodReturnValueHandler> handlers) { for (HandlerMethodReturnValueHandler handler : handlers) { if (handler instanceof RequestResponseBodyMethodProcessor) { ResponseBodyWrapHandler decorator = new ResponseBodyWrapHandler(handler); int index = handlers.indexOf(handler); handlers.set(index, decorator); break; } } } } |
使用ResponseBodyWrapFactoryBean,完成afterProperties方法的调用,只需要创建一个ResponseBodyWrapFactoryBean的实例即可:
1 2 3 4 |
@Bean public ResponseBodyWrapFactoryBean getResponseBodyWrap() { return new ResponseBodyWrapFactoryBean(); } |
这行代码可以放在启动类中。
方法二
第二种方式基于ControllerAdvice和HttpMessageConverter实现。
首先用一个ResponseBodyAdvice类的实现包装Controller的返回值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
import com.zhyea.spring.ext.Result; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; @ControllerAdvice public class ResponseAdvisor implements ResponseBodyAdvice<Object> { @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { return true; } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { String requestPath = request.getURI().getPath(); if (!requestPath.startsWith("/ug")) { return body; } if (body instanceof Result) { return body; } return new Result(body); } } |
如果Controller类的返回值没有String类型的,仅有上面这个类就够了。如果有String类型的返回值,就有可能遇到类型不匹配的问题。HttpMessageConverter是根据Controller的原始返回值类型进行处理的,而我们在ResponseAdvisor中改变了返回值的类型。如果HttpMessageConverter处理的目标类型是Object还好说,如果是其它类型就会出现问题,其中最容易出现问题的就是String类型,因为在所有的HttpMessageConverter实例集合中,StringHttpMessageConverter要比其它的Converter排得靠前一些。我们需要尝试将处理Object类型的HttpMessageConverter放得靠前一些,这可以在一个Configuration类中完成:
1 2 3 4 5 6 7 8 9 10 |
@Configuration public class WebConfig extends DelegatingWebMvcConfiguration { @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { converters.add(0, new MappingJackson2HttpMessageConverter()); super.configureMessageConverters(converters); } } |
在这个方案中,如需要对异常做些特别处理,还可以创建一个ExceptionAdvisor类来完成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; @ControllerAdvice public class ExceptionAdvisor { @ResponseBody @ExceptionHandler(value = Exception.class) @ResponseStatus public Result exceptionHandler(Exception e) { return new Result(HttpStatus.BAD_REQUEST.value(), e.getMessage()); } @ResponseBody @ExceptionHandler(value = RuntimeException.class) @ResponseStatus(code = HttpStatus.BAD_REQUEST) public Result formatCheckExceptionHandler(RuntimeException e) { return new Result(HttpStatus.BAD_REQUEST.value(), e.getMessage()); } } |
这样还可以根据异常类型来设置返回时的HttpStatus。
就这样。
有朋友在评论区指出问题了,做了些调整,也写了一份示例程序上传到CSDN。有兴趣的可以下载来看看。下载地址如下:点击此处下载。
这是许久之前刚用springboot时写的,现在适应springboot的最新版本存在一些问题。所以稍稍重新调整了下,并将之加入到了最近正在尝试进行的一个系列里面。
新版本的代码可以在GitHub / zhyea上看到。
##########
第一种方法,测试了发现不可以啊,是不是需要添加注解啊,还有一种利用@around来处理返回结果,和楼主的两种处理方式相比有什么优劣
第一种方法我反复测试过了,是没有问题的。如有报错,可以把错误信息贴一下。
@Around是Spring标准的AOP处理方案。也考虑过类似的思路,不过不太理想。
返回的类的数据必须要有getter,(可能需要有setter)
用第一种方式,一直报错,类型不匹配
java.lang.IllegalArgumentException: Unknown return value type
现在提供了示例代码。
No converter found for return value of type: class com.Test.Result
没有Result类的转换器
恩,偷懒来着。没有为Result设置get&set方法。现在已作了修正。
第二种方法lz试了吗
现在附上了示例代码,见文末
不清楚作者的版本,目前springboot2.0,两种方法均无法实现,最后spring均会对方法声明的返回值类型和实际返回值类型进行对比,不一致直接抛出异常
版本为spring-boot-starter-web:1.5.6.RELEASE。详情可以参考提供的示例代码。2.0没有试过,不妄言。
2.0 第二种可以用。
感谢,找了很久Stack Overflow 都没有结果,博主的解决最详细,原理描述清晰,感谢。
纠正一下:
使用 第 2 种方式的时候,其实是可以的。 并不需要 将 FastJsonMessageConvert 提前。 因为 FastJsonMessageConvert 一直都是在 String 转换器前面的。
如果你是用浏览器请求, content-type 会是 text/plain。 因此 spring 会认为你是要请求 一个 页面。 从而会找不到任何可以处理该返回值的处理。
如果你是使用接口,或者用 postMan 测试。 content-type 会是 application/json。那么这个时候 FastJson 是可以处理的。
第二种方法返回值为null的时候,楼主有没有测试过,根本就走不到beforeBodyWrite方法中