上一次說到了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來處理,同樣需要注意注入順序。