Spring web請求controller puzzle

遇到一個(gè)非常奇怪的問題, 在瀏覽器中和PostMan中向后端發(fā)送請求, 得到的請求結(jié)果是不一樣的

后端代碼如下:

@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把返回?cái)?shù)據(jù)格式化,

  • 在瀏覽器中請求得到的響應(yīng)是:
<Map xmlns="">
    <a>1000</a>
    <b>1</b>
</Map>
  • 在postman中請求得到的響應(yīng)是:
{
    "a": "1000",
    "b": "5"
}

看到兩者請求返回?cái)?shù)據(jù)的格式是不一樣的, 需要對此進(jìn)行探究一下

一. 首先從SpringMVC處理請求的流程說起

  1. Web服務(wù)啟動(dòng)的時(shí)候, 需要在sprinf-mvc context配置文中配置mvc:annotation-driven
<mvc:annotation-driven>
        <mvc:message-converters register-defaults="true">
            <!-- 將StringHttpMessageConverter的默認(rèn)編碼設(shè)為UTF-8 -->
            <bean class="org.springframework.http.converter.StringHttpMessageConverter">
                <constructor-arg value="UTF-8" />
            </bean>
            <!-- 將Jackson2HttpMessageConverter的默認(rèn)格式化輸出設(shè)為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());
        ........
    }
}

進(jìn)入到AnnotationDrivenBeanDefinitionParser中會發(fā)現(xiàn)默認(rèn)向bean容器注冊了一個(gè)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加載的時(shí)候會調(diào)用preInitializationSingletons, 會初始化singleton的bean對象, 這時(shí)候會初始化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會判斷有沒有相關(guān)jar包依賴決定要不要加載對應(yīng)的messageConverter

  1. web服務(wù)收到外部請求, 會將該請求轉(zhuǎn)發(fā)給DispatchServlet, DispatchServlet是一個(gè)實(shí)現(xiàn)Servlet接口的類, 是SpringMVC的前端分發(fā)servlet.

    外部請求被引導(dǎo)到processRequest -> doService -> doDispatch, doDispatch中的邏輯相對復(fù)雜

    • step1: 判斷是不是multiPart類型的請求,
    • step2: 根據(jù)request信息尋找對應(yīng)的handler, 如果找不到handler, response返回錯(cuò)誤信息
    • step3: 尋找handlerAdapter
    • step4: 判斷對Last-Modified的支持
    • step5: 調(diào)用攔截器的preHandle方法
    • step6: 調(diào)用handler.handle方法
    • step7: 執(zhí)行攔截器的postHandle方法
    • step8: 處理頁面跳轉(zhuǎn)
  2. 具體分下一下2所述的流程

    • 外部請求被引導(dǎo)到processRequest函數(shù)中執(zhí)行
      • 有個(gè)threadLocal用于配置localContext, 雖然我暫時(shí)也不知道這個(gè)玩意是干啥的
      • 調(diào)用doService
      • reset local context, 恢復(fù)為之前的localContext
    • doService -> doDispatch
    • doDispatch
      • 檢查是不是multiPart的請求
      • getHandler, 獲得HandlerExecutionChain
      • 調(diào)用getHandlerAdapter, 獲得HandlerAdapter ha
      • 調(diào)用handler攔截器的preProcess
      • 調(diào)用ha.handler執(zhí)行實(shí)際操作
      • 調(diào)用handler攔截器的postProcess
      • 處理dispatch結(jié)果
    • 獲得HandlerExecutionChain流程
      • 根據(jù)系統(tǒng)獲得的handlerMappings(一般有兩個(gè): RequestMappingHandlerMapping和BeanNameUrlHandlerMapping), 調(diào)用每個(gè)handler的getHandler方法
      • 調(diào)用HandlerMapping的getHandler方法獲得HandlerExecutionChain
        • 獲得HandlerMethod, 調(diào)用 lookUpHandlerMethod
          • 先根據(jù)請求路徑(絕對路徑)查詢直接匹配的元素, 如果每一個(gè)RequestMappingInfo的各個(gè)條件(例如method條件, header條件等)都滿足, 則添加到臨時(shí)容器中
          • 若沒有命中的, 則嘗試查詢所有的RequestMappingInfo最終嘗試獲取滿足條件的RequestMappingInfo
          • 如果有多個(gè)滿足條件的, 則會進(jìn)行排序比較找到最滿足條件的
      • 獲得HandlerExecutionChain, 遍歷adaptedInterceptors找到匹配請求路徑的攔截器(攔截器可以指定excludePath和includePath, 如果請求路徑match excludePath則不匹配, 若請求路徑匹配任一includePath, 則匹配)
    • 獲得HandlerAdapter的過程: 遍歷handlerAdapter, 找到第一個(gè)能support handler的handlerAdapter
    • 調(diào)用HandlerAdapter的handle方法
      • 首先進(jìn)行一些基礎(chǔ)校驗(yàn)check, 以及是否需要session等一些校驗(yàn)
      • 調(diào)用invokeHandlerMethod
        • 調(diào)用invokeAndHandle: 首先反射調(diào)用函數(shù), 獲得請求結(jié)果
        • 調(diào)用returnValueHandlers對計(jì)算結(jié)果進(jìn)行處理
          • selectHandler, 這個(gè)實(shí)際上是遍歷系統(tǒng)的returnvalueHandlers, 然后判斷returnValueHandler是不是支持返回結(jié)果的returnType, 最終會匹配RequestResponseBodyMethodProcessor(判斷邏輯是外圍類是不是有ResponseBody注解或者返回值所在的method有沒有ResponseBody注解)
          • 調(diào)用上述返回的HandlerMethodReturnValueHandler的handleReturnValue函數(shù), 該函數(shù)中會調(diào)用writeWithMessageConverters函數(shù), 在這個(gè)函數(shù)中, 會根據(jù)請求的header獲得mediaType, 然后不同的mediaType會匹配到不同的messageConverter上, 寫的格式也不一樣
        • 還記得上述的RequestMappingHandlerAdapter初始化中加載的一堆messageConverter?
HandlerAdapter是什么?
在HandlerMapping返回處理請求的Controller實(shí)例后,需要一個(gè)幫助定位具體請求方法的處理類,這個(gè)類就是HandlerAdapter.

HandlerAdapter是處理器適配器,Spring MVC通過HandlerAdapter來實(shí)際調(diào)用處理函數(shù)。例如Spring MVC自動(dòng)注冊的AnnotationMethodHandlerAdpater.

HandlerAdapter定義了如何處理請求的策略,通過請求url、請求Method和處理器的requestMapping定義,最終確定使用處理類的哪個(gè)方法來處理請求,并檢查處理類相應(yīng)處理方法的參數(shù)以及相關(guān)的Annotation配置,確定如何轉(zhuǎn)換需要的參數(shù)傳入調(diào)用方法,并最終調(diào)用返回ModelAndView。

DispatcherServlet中根據(jù)HandlerMapping找到對應(yīng)的handler method后,首先檢查當(dāng)前工程中注冊的所有可用的handlerAdapter,根據(jù)handlerAdapter中的supports方法找到可以使用的handlerAdapter。
通過調(diào)用handlerAdapter中的handler方法來處理及準(zhǔn)備handler method的參數(shù)及annotation(這就是spring mvc如何將request中的參數(shù)變成handle method中的輸入?yún)?shù)的地方),最終調(diào)用實(shí)際的handler method。

最終問題明確了, 瀏覽器請求的時(shí)候會默認(rèn)添加一個(gè)content-type的header, 而postman并不會帶上, 這樣會導(dǎo)致最終根據(jù)header匹配到不同的messageConverter, 從而返回不同的格式

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容