遇到一個非常奇怪的問題, 在瀏覽器中和PostMan中向后端發送請求, 得到的請求結果是不一樣的
后端代碼如下:
@Controller
@RequestMapping("/test")
@Slf4j
public class TestController {
@RequestMapping(value = "/test", method = RequestMethod.GET)
@ResponseBody
public Map<String, String> testMethod(@RequestParam("id") long id) {
Map<String, String> rst = new HashMap<String, String>();
rst.put("a", String.valueOf(id));
rst.put("b", String.valueOf(RandomUtils.nextInt(20)));
return rst;
}
目的是希望通過jackson把返回數據格式化,
- 在瀏覽器中請求得到的響應是:
<Map xmlns="">
<a>1000</a>
<b>1</b>
</Map>
- 在postman中請求得到的響應是:
{
"a": "1000",
"b": "5"
}
看到兩者請求返回數據的格式是不一樣的, 需要對此進行探究一下
一. 首先從SpringMVC處理請求的流程說起
- Web服務啟動的時候, 需要在sprinf-mvc context配置文中配置mvc:annotation-driven
<mvc:annotation-driven>
<mvc:message-converters register-defaults="true">
<!-- 將StringHttpMessageConverter的默認編碼設為UTF-8 -->
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<constructor-arg value="UTF-8" />
</bean>
<!-- 將Jackson2HttpMessageConverter的默認格式化輸出設為true -->
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="prettyPrint" value="false"/>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
SpringMVC中自帶了自定義命名空間解析器
public class MvcNamespaceHandler extends NamespaceHandlerSupport {
@Override
public void init() {
registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenBeanDefinitionParser());
........
}
}
進入到AnnotationDrivenBeanDefinitionParser中會發現默認向bean容器注冊了一個RequestMappingHandlerAdapter的beanDefinition
@Override
public BeanDefinition parse(Element element, ParserContext parserContext) {
...........
RootBeanDefinition handlerAdapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class);
handlerAdapterDef.setSource(source);
handlerAdapterDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
handlerAdapterDef.getPropertyValues().add("contentNegotiationManager", contentNegotiationManager);
handlerAdapterDef.getPropertyValues().add("webBindingInitializer", bindingDef);
handlerAdapterDef.getPropertyValues().add("messageConverters", messageConverters);
addRequestBodyAdvice(handlerAdapterDef);
addResponseBodyAdvice(handlerAdapterDef);
if (element.hasAttribute("ignore-default-model-on-redirect")) {
Boolean ignoreDefaultModel = Boolean.valueOf(element.getAttribute("ignore-default-model-on-redirect"));
handlerAdapterDef.getPropertyValues().add("ignoreDefaultModelOnRedirect", ignoreDefaultModel);
}
else if (element.hasAttribute("ignoreDefaultModelOnRedirect")) {
// "ignoreDefaultModelOnRedirect" spelling is deprecated
Boolean ignoreDefaultModel = Boolean.valueOf(element.getAttribute("ignoreDefaultModelOnRedirect"));
handlerAdapterDef.getPropertyValues().add("ignoreDefaultModelOnRedirect", ignoreDefaultModel);
}
if (argumentResolvers != null) {
handlerAdapterDef.getPropertyValues().add("customArgumentResolvers", argumentResolvers);
}
if (returnValueHandlers != null) {
handlerAdapterDef.getPropertyValues().add("customReturnValueHandlers", returnValueHandlers);
}
if (asyncTimeout != null) {
handlerAdapterDef.getPropertyValues().add("asyncRequestTimeout", asyncTimeout);
}
if (asyncExecutor != null) {
handlerAdapterDef.getPropertyValues().add("taskExecutor", asyncExecutor);
}
handlerAdapterDef.getPropertyValues().add("callableInterceptors", callableInterceptors);
handlerAdapterDef.getPropertyValues().add("deferredResultInterceptors", deferredResultInterceptors);
readerContext.getRegistry().registerBeanDefinition(HANDLER_ADAPTER_BEAN_NAME , handlerAdapterDef);
........
}
springMVC的context加載的時候會調用preInitializationSingletons, 會初始化singleton的bean對象, 這時候會初始化RequestMappingHandlerAdapter, RequestMappingHandlerAdapter的初始化中會加載一些MessageConverter.
public RequestMappingHandlerAdapter() {
StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
stringHttpMessageConverter.setWriteAcceptCharset(false); // see SPR-7316
this.messageConverters = new ArrayList<HttpMessageConverter<?>>(4);
this.messageConverters.add(new ByteArrayHttpMessageConverter());
this.messageConverters.add(stringHttpMessageConverter);
this.messageConverters.add(new SourceHttpMessageConverter<Source>());
this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
}
在AllEncompassingFormHttpMessageConverter中我們可以看到這樣的邏輯
private static final boolean jaxb2Present =
ClassUtils.isPresent("javax.xml.bind.Binder", AllEncompassingFormHttpMessageConverter.class.getClassLoader());
private static final boolean jackson2Present =
ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", AllEncompassingFormHttpMessageConverter.class.getClassLoader()) &&
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", AllEncompassingFormHttpMessageConverter.class.getClassLoader());
private static final boolean jackson2XmlPresent =
ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", AllEncompassingFormHttpMessageConverter.class.getClassLoader());
private static final boolean gsonPresent =
ClassUtils.isPresent("com.google.gson.Gson", AllEncompassingFormHttpMessageConverter.class.getClassLoader());
可以看到, spring會判斷有沒有相關jar包依賴決定要不要加載對應的messageConverter
-
web服務收到外部請求, 會將該請求轉發給DispatchServlet, DispatchServlet是一個實現Servlet接口的類, 是SpringMVC的前端分發servlet.
外部請求被引導到processRequest -> doService -> doDispatch, doDispatch中的邏輯相對復雜
- step1: 判斷是不是multiPart類型的請求,
- step2: 根據request信息尋找對應的handler, 如果找不到handler, response返回錯誤信息
- step3: 尋找handlerAdapter
- step4: 判斷對Last-Modified的支持
- step5: 調用攔截器的preHandle方法
- step6: 調用handler.handle方法
- step7: 執行攔截器的postHandle方法
- step8: 處理頁面跳轉
-
具體分下一下2所述的流程
- 外部請求被引導到processRequest函數中執行
- 有個threadLocal用于配置localContext, 雖然我暫時也不知道這個玩意是干啥的
- 調用doService
- reset local context, 恢復為之前的localContext
- doService -> doDispatch
- doDispatch
- 檢查是不是multiPart的請求
- getHandler, 獲得HandlerExecutionChain
- 調用getHandlerAdapter, 獲得HandlerAdapter ha
- 調用handler攔截器的preProcess
- 調用ha.handler執行實際操作
- 調用handler攔截器的postProcess
- 處理dispatch結果
- 獲得HandlerExecutionChain流程
- 根據系統獲得的handlerMappings(一般有兩個: RequestMappingHandlerMapping和BeanNameUrlHandlerMapping), 調用每個handler的getHandler方法
- 調用HandlerMapping的getHandler方法獲得HandlerExecutionChain
- 獲得HandlerMethod, 調用 lookUpHandlerMethod
- 先根據請求路徑(絕對路徑)查詢直接匹配的元素, 如果每一個RequestMappingInfo的各個條件(例如method條件, header條件等)都滿足, 則添加到臨時容器中
- 若沒有命中的, 則嘗試查詢所有的RequestMappingInfo最終嘗試獲取滿足條件的RequestMappingInfo
- 如果有多個滿足條件的, 則會進行排序比較找到最滿足條件的
- 獲得HandlerMethod, 調用 lookUpHandlerMethod
- 獲得HandlerExecutionChain, 遍歷adaptedInterceptors找到匹配請求路徑的攔截器(攔截器可以指定excludePath和includePath, 如果請求路徑match excludePath則不匹配, 若請求路徑匹配任一includePath, 則匹配)
- 獲得HandlerAdapter的過程: 遍歷handlerAdapter, 找到第一個能support handler的handlerAdapter
- 調用HandlerAdapter的handle方法
- 首先進行一些基礎校驗check, 以及是否需要session等一些校驗
- 調用invokeHandlerMethod
- 調用invokeAndHandle: 首先反射調用函數, 獲得請求結果
- 調用returnValueHandlers對計算結果進行處理
- selectHandler, 這個實際上是遍歷系統的returnvalueHandlers, 然后判斷returnValueHandler是不是支持返回結果的returnType, 最終會匹配RequestResponseBodyMethodProcessor(判斷邏輯是外圍類是不是有ResponseBody注解或者返回值所在的method有沒有ResponseBody注解)
- 調用上述返回的HandlerMethodReturnValueHandler的handleReturnValue函數, 該函數中會調用writeWithMessageConverters函數, 在這個函數中, 會根據請求的header獲得mediaType, 然后不同的mediaType會匹配到不同的messageConverter上, 寫的格式也不一樣
- 還記得上述的RequestMappingHandlerAdapter初始化中加載的一堆messageConverter?
- 外部請求被引導到processRequest函數中執行
HandlerAdapter是什么?
在HandlerMapping返回處理請求的Controller實例后,需要一個幫助定位具體請求方法的處理類,這個類就是HandlerAdapter.
HandlerAdapter是處理器適配器,Spring MVC通過HandlerAdapter來實際調用處理函數。例如Spring MVC自動注冊的AnnotationMethodHandlerAdpater.
HandlerAdapter定義了如何處理請求的策略,通過請求url、請求Method和處理器的requestMapping定義,最終確定使用處理類的哪個方法來處理請求,并檢查處理類相應處理方法的參數以及相關的Annotation配置,確定如何轉換需要的參數傳入調用方法,并最終調用返回ModelAndView。
DispatcherServlet中根據HandlerMapping找到對應的handler method后,首先檢查當前工程中注冊的所有可用的handlerAdapter,根據handlerAdapter中的supports方法找到可以使用的handlerAdapter。
通過調用handlerAdapter中的handler方法來處理及準備handler method的參數及annotation(這就是spring mvc如何將request中的參數變成handle method中的輸入參數的地方),最終調用實際的handler method。
最終問題明確了, 瀏覽器請求的時候會默認添加一個content-type的header, 而postman并不會帶上, 這樣會導致最終根據header匹配到不同的messageConverter, 從而返回不同的格式