在現在愈來愈崇尚開箱即用的階段,不少細節被隱藏在了身後,特別是開始使用SpringBoot以後,更多的自動配置,讓咱們方便的同時,不少時候也讓咱們更加深刻的思考。html
本篇文章就來了解一下遇到比較多的ResponseBody流程相關的問題。前端
Spring處理ResponseBody流程相關的類和接口主要有下面7個:java
RequestMappingHandlerMapping RequestMappingHandlerAdapter HandlerMethodArgumentResolver HandlerMethodReturnValueHandler RequestResponseBodyMethodProcessor AbstractMessageConverterMethodProcessor HttpMessageConverter
RequestMappingHandlerMapping是一個HandlerMapping,簡單的來講它幫咱們找到和請求URL匹配的Controller中@RequestMapping註解的方法。web
RequestMappingHandlerAdapter能夠看做是Controller中@RequestMapping註解的方法的一個適配器,處理了一些請求和返回的細節,例如參數注入,返回值處理。spring
HandlerMethodArgumentResolver用於處理Controller中@RequestMapping註解的方法參數 HandlerMethodReturnValueHandler用於處理Controller中@RequestMapping註解的方法返回值json
RequestResponseBodyMethodProcessor實現了HandlerMethodArgumentResolver和HandlerMethodReturnValueHandler,用於處理RequestBody、ResponseBody註解。後端
@RestController這個類註解會自動爲方法添加上@ResponseBody,因此在@RestController註解的Controller中不須要再顯示的添加@ResponseBody註解。app
HttpMessageConverter用於request請求到Controller方法的參數類型,Controller方法的返回值到response之間的轉換。ide
例如前端的請求參數轉換爲RequestBody註解的參數,後端Controller方法的返回的類轉換爲前端須要的json、xml等實際就是經過HttpMessageConverter完成的。函數
能夠經過下面的方法看一下大體Spring容器中有哪些類,檢查有沒有出現上面提到的類:
import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; @Component public class ApplicationHolder implements ApplicationContextAware { @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { String[] names = applicationContext.getBeanDefinitionNames(); for(String name : names){ System.out.println(name); } } }
知道了前面的內容,咱們如今重點關注一下HttpMessageConverter就能夠了。 HttpMessageConverter是RequestMappingHandlerAdapter建立的,有興趣能夠看一下RequestMappingHandlerAdapter的構造函數。
AnnotationDrivenBeanDefinitionParser解析xml配置文件的message-converters也會添加。
關於HttpMessageConverter添加有興趣能夠本身看一下相關源碼,這裏不詳細介紹,咱們要介紹的是在SpringBoot中要如何定製HttpMessageConverter。
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.List; @Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private HttpMessageConverters messageConverters; @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { List<HttpMessageConverter<?>> httpMessageConverters = this.messageConverters.getConverters(); httpMessageConverters.forEach(System.out::println); converters.addAll(httpMessageConverters); // FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter(); // FastJsonConfig fastJsonConfig = new FastJsonConfig(); // fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat); // List<MediaType> fastMediaTypes = new ArrayList<>(); // fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8); // fastConverter.setSupportedMediaTypes(fastMediaTypes); // fastConverter.setFastJsonConfig(fastJsonConfig); // converters.add(fastConverter); } }
經過WebMvcConfigurer的configureMessageConverters方法,咱們就能夠自由的查看有那些HttpMessageConverter,也能夠很容易添加很刪除。
例如咱們不想使用默認的MappingJackson2HttpMessageConverter,咱們就能夠把它刪除了,而後添加一個FastJsonHttpMessageConverter。
若是還不滿意,甚至能夠本身建立一個HttpMessageConverters,替換掉默認的:
@Bean public HttpMessageConverters customConverters(){ StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter(); stringHttpMessageConverter.setWriteAcceptCharset(false); ByteArrayHttpMessageConverter byteArrayHttpMessageConverter = new ByteArrayHttpMessageConverter(); SourceHttpMessageConverter<Source> sourceSourceHttpMessageConverter = new SourceHttpMessageConverter<>(); AllEncompassingFormHttpMessageConverter allEncompassingFormHttpMessageConverter = new AllEncompassingFormHttpMessageConverter(); return new HttpMessageConverters(stringHttpMessageConverter ,byteArrayHttpMessageConverter,sourceSourceHttpMessageConverter,allEncompassingFormHttpMessageConverter); }
或者咱們懶得改,只想配置一下默認的Jackson的行爲:
@Bean public ObjectMapper jsonMapper(Jackson2ObjectMapperBuilder builder) { ObjectMapper mapper = builder.createXmlMapper(false).build(); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); return mapper; }
下面的類放在SpringBoot工程中,執行最後的測試類。
import javax.xml.bind.annotation.XmlRootElement; import java.io.Serializable; @XmlRootElement public class Result<T> implements Serializable { private static final long serialVersionUID = -1; /** * 成功與否,客戶端快速判斷 */ private boolean status = false; /** * 狀態碼,方便快速定位問題 */ private int code; /** * 提示信息 */ private String msg; /** * 數據 */ private T data; public Result() { } public Result(boolean status, int code, String msg) { this.status = status; this.code = code; this.msg = msg; } public Result(boolean status, int code, String msg, T data) { this.status = status; this.code = code; this.msg = msg; this.data = data; } public boolean isStatus() { return status; } public void setStatus(boolean status) { this.status = status; } public int getCode() { return code; } public void setCode(int code) { this.code = code; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public T getData() { return data; } public void setData(T data) { this.data = data; } }
public final class ResultHelper { private static final boolean DEFAULT_SUCCESS_STATUS = true; private static final boolean DEFAULT_FAIL_STATUS = false; private static final int DEFAULT_SUCCESS_CODE = 200; private static final int PARAM_ERROR_CODE = 400; private static final int DEFAULT_ERROR_CODE = 600; private static final String DEFAULT_SUCCESS_MESSAGE = "success"; private static final String DEFAULT_ERROR_MESSAGE = "fail"; private static final String PARAM_ERROR_MESSAGE = "參數錯誤"; public static <T> Result getDefaultSuccessResult(T data){ return new Result(DEFAULT_SUCCESS_STATUS,DEFAULT_SUCCESS_CODE,DEFAULT_SUCCESS_MESSAGE,data); } public static Result getDefaultErrorResult(){ return new Result(DEFAULT_FAIL_STATUS, DEFAULT_ERROR_CODE, DEFAULT_ERROR_MESSAGE); } public static Result getParamErrorResult(){ return new Result(DEFAULT_FAIL_STATUS,PARAM_ERROR_CODE,PARAM_ERROR_MESSAGE); } public static Result getErrorResult(int code,String message){ return new Result(DEFAULT_FAIL_STATUS, code, message); } }
import org.curitis.common.Result; import org.curitis.common.ResultHelper; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/rrb") public class RRBController { @RequestMapping("/hello") public Result hello(){ return ResultHelper.getParamErrorResult(); } }
import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.HttpHeaders; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultHandler; import org.springframework.web.context.WebApplicationContext; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; @RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @SpringBootTest public class RRBControllerTest { @Autowired protected WebApplicationContext wac; protected MockMvc mockMvc; @Before public void setUp(){ this.mockMvc = webAppContextSetup(this.wac).build(); } @Test public void json() throws Exception { mockMvc.perform(post("/rrb/hello")) .andExpect(status().isOk()) .andDo(print()) .andReturn(); } @Test public void xml() throws Exception { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.add(HttpHeaders.ACCEPT, "application/xml;charset=UTF-8"); mockMvc.perform(post("/rrb/hello") .headers(httpHeaders) .param("page","1") .param("pageSize","20") ) .andDo(new ResultHandler() { @Override public void handle(MvcResult result) throws Exception { System.out.println(result.getResponse().getContentAsString()); } }) .andReturn(); } }
執行上面的測試類,咱們能夠看到咱們可以獲得不一樣的輸出,json會獲得json字符串,xml會獲得xml字符串。
只是由於咱們請求頭的accept不一樣。
注意,要使用Jaxb2RootElementHttpMessageConverter須要在返回的實體類上添加@XmlRootElement註解。
具體的代碼就不詳細說了,說一下流程和關鍵代碼,感興趣的朋友能夠本身調試。
在Controller中的方法return以後,若是方法有@ResponseBody註解,那麼就會來到RequestResponseBodyMethodProcessor的handleReturnValue。
handleReturnValue會調用AbstractMessageConverterMethodProcessor的writeWithMessageConverters方法。
斷點打到writeWithMessageConverters方法中,須要跟蹤那個看那個,不用被其餘的類和流程繞暈了。
writeWithMessageConverters方法中很重要的邏輯就是找MediaType,由於要根據MediaType來找HttpMessageConverter。
首先從Response的Content-Type判斷,若是有,說明服務端是很是清楚要返回什麼類型,因此就能夠直接肯定。
若是沒有,就會根據request去找,若是沒有特殊配置就是看Header中的Accept,涉及的類是HeaderContentNegotiationStrategy。
找到客戶端須要什麼還不算完,由於重要的不是你要什麼,而是要看我有什麼。
因此還要找應用支持哪些MediaType,基本就是去HttpMessageConverters中的遍歷全部HttpMessageConverter支持的類型。
找到支持的MediaType會排序,怎麼排序呢?
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
咱們看請求頭,常常會看到上面的內容,q=0.9的部分就能夠能夠看做是權重,值越大優先級越高。 例如上面application/xml;q=0.9,/;q=0.8就表示xml比all類型優先級要高。
若是全部的HttpMessageConverter支持的MediaType都不匹配,那麼就會獲得下面的異常:
HttpMessageNotWritableException:No converter found for return value of type
SpringBoot默認會使用MappingJackson2HttpMessageConverter,若是咱們不想使用其餘json converter,那麼咱們能夠經過添加一個ObjectMapper來定製Jackson。
@Bean public ObjectMapper jsonMapper(Jackson2ObjectMapperBuilder builder) { ObjectMapper mapper = builder.createXmlMapper(false).build(); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); return mapper; }
關於Jackson的配置,能夠參考:Jackson最經常使用配置與註解