上一次说到了springboot中对于参数处理的扩展,当然主要是说到如何针对特有的请求数据进行转换,并非站在spring的基础上看spring如何完成这一逻辑,而关于spring内部处理或者说内置的处理将在sping源码中详细去理解。
今天将要说到的是springboot中如何处理方法返回值,使用spring较多的人会看到,对于方法的返回一般会有两种模式,一种是页面,一种是数据,分别对应了ModelAndView和被@ResponseBody标注的方法。下面简单的看下这两种方式的实现过程,同时如何扩展自己的实现。
对于springmvc,所有的请求入口则是通过DispatcherServlet.doDispatch处理,不妨从这里开始看下处理过程,对于此次不关注的点回直接略过。
首先定义一个简单的Controller,然后在doDispatch打个断点
主要关注ha,在spring中有很多的HandlerAdapter,我们用的比较多的当然是RequestMappingHandlerAdapter,相应的进入到handleInternal方法中,
下一步则会调用invokeHandlerMethod,内容比较多,但是不用关注太多,
这里可以看到,使用了一个invocableMethod,看代码可以看到其类型为ServletInvocableHandlerMethod,
这里关注下mavContainer.setRequestHandled(true),这也就是表明返回的ModelAndView是否会被视图解析器解析。returnValueHandlers也就是HandlerMethodReturnValueHandlerComposite,
此时会找到对应的returnValueHandler去处理,调试会发现RequestResponseBodyMethodProcessor,
这里将结果交由MessageConverter处理,由于方法writeWithMessageConverters篇幅比较长,看下关键点
最终就会定位到我们熟悉的StringHttpMessageConverter、MappingJackson2HttpMessageConverter。
其实通过调试基本可以看到主要的逻辑流程。今天说到的返回值处理主要如何通过HandlerMethodReturnValueHandler接口进行扩展,通过上面的调试过程,我们可以看到,对改接口的使用时在RequestMappingHandlerAdapter出现 ,具体spring对该接口有哪些实现,这些实现是如何注入到RequestMappingHandlerAdapter中,这个不做深入。上面说到这里会选择 RequestResponseBodyMethodProcessor,可以看下大致的实现,方便之后的扩展。
这也解释了为什么通过@ResponseBody标记的类或方法能够被处理了。
上面看到的,所以我们在扩展的时候也是一个道理。
下面看一个示例,将请求结果写到文件中并返回:
定义一个注解,类似@ResponseBody,ResponseFile :
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
public @interface ResponseFile {
String type() default "txt";
}
定义HandlerMethodReturnValueHandler实现类FileHandlerMethodReturnValueHandler:
public class FileHandlerMethodReturnValueHandler implements HandlerMethodReturnValueHandler,Ordered {
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseFile.class) ||
returnType.hasMethodAnnotation(ResponseFile.class));
}
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
mavContainer.setRequestHandled(true);
ResponseFile methodResponseFile = returnType.getMethodAnnotation(ResponseFile.class);
if(methodResponseFile == null){
methodResponseFile = returnType.getContainingClass().getAnnotation(ResponseFile.class);
}
Assert.notNull(methodResponseFile,"no ResponseFile at method :"+returnType.getMethod().getName());
String fileType = methodResponseFile.type();
HttpServletResponse response = getServletResponse(webRequest);
response.setHeader("content-type", "application/octet-stream");
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment;filename="+UUID.randomUUID().toString()+"."+fileType);
ServletOutputStream out = response.getOutputStream();
if(returnValue instanceof ResponseFileEntity){
FileCopyUtils.copy(((ResponseFileEntity) returnValue).getIn(),out);
}else{
FileCopyUtils.copy(new ByteArrayInputStream("non".getBytes()),out);
}
}
public HttpServletResponse getServletResponse(NativeWebRequest webRequest){
HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
return response;
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE-10;
}
}
将HandlerMethodReturnValueHandler注册到RequestMappingHandlerAdapter中:
@Configuration
public class WebMvcConfiguration extends WebMvcConfigurerAdapter {
@Override
public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) {
super.addReturnValueHandlers(returnValueHandlers);
returnValueHandlers.add(new FileHandlerMethodReturnValueHandler());
}
}
,根据源码你会发现,这个会注册到RequestMappingHandlerAdapter的customReturnValueHandlers中,但是一定要注意,默认情况下会有很多HandlerMethodReturnValueHandler,而具体使用哪一个是根据supportsReturnType进行适配,一段发现有匹配的处理,则会调用,看下面的代码你会发现,自定义的处理器位置较后,如果默认的处理器有能够适配的,那么我们定义的就不会执行了,所有要注意。
最后就是测试了:
@ResponseFile
@GetMapping("/file")
public ResponseFileEntity toFile(){
ResponseFileEntity responseFileEntity = new ResponseFileEntity();
responseFileEntity.setIn(new ByteArrayInputStream("这是一段测试文字".getBytes()));
return responseFileEntity;
}
当发送对应请求时,会返回一个文件。
前面说到,我们一般情况下的返回值主要都是通过@ResponseBody处理,最后将返回值转换成json类型并返回,所有处理逻辑在RequestResponseBodyMethodProcessor中完成,前面也说到,主要是通过HttpMessageConverter完成,这个东西比较眼熟,同样,在spring中默认有很多HttpMessageConverter,比如常见的StringHttpMessageConverter、MappingJackson2HttpMessageConverter等。
下面通过自定义一个HttpMessageConverter来针对@ResponseBody进行扩展:
public class XmlHttpMessageConverter extends AbstractHttpMessageConverter<Message> {
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
public XmlHttpMessageConverter(){
this(DEFAULT_CHARSET);
}
public XmlHttpMessageConverter(Charset charset){
super(charset,MediaType.ALL);
}
@Override
protected boolean supports(Class<?> clazz) {
return clazz == Message.class;
}
@Override
protected Message readInternal(Class<? extends Message> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}
@Override
protected void writeInternal(Message message, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
XStream xStream = new XStream();
xStream.alias("message",Message.class);
// StreamUtils.copy(xStream.toXML(message), DEFAULT_CHARSET, outputMessage.getBody());
xStream.toXML(message,outputMessage.getBody());
}
}
注册到容器:
@Configuration
public class WebMvcConfiguration extends WebMvcConfigurerAdapter {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
super.configureMessageConverters(converters);
}
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
super.extendMessageConverters(converters);
converters.add(0,new XmlHttpMessageConverter());
}
}
写个测试:
@ResponseBody
@GetMapping("/message")
public Message message(){
Message message = new Message();
message.setId(UUID.randomUUID().toString());
message.setDate(new Date());
message.setInfo("消息");
return message;
}
总结:
1、主要了解了springboot处理返回值的简单逻辑,返回对象最后都转换为ModelAndView,但是是否由视图解析器处理需要根据
ModelAndViewContainer.RequestHandled判断
2、通过实现 HandlerMethodReturnValueHandler自定义返回,可以将返回值转换成指定的类型,但要注意与默认配置的顺序,防止自定义的被覆盖
3、在@ResponseBody的基础上进行扩展,通过实现自定义的HttpMessageConverter来处理,同样需要注意注入顺序。