當一個http請求來臨時,SpringMVC究竟偷偷幫你做了什麼?

一、簡介

Spring MVC是一個基於Java的實現了MVC設計模式的請求驅動類型的輕量級Web框架,通過把Model,View,Controller分離,將web層進行職責解耦,把複雜的web應用分成邏輯清晰的幾部分,簡化開發,減少出錯,方便組內開發人員之間的配合。

1. Springmvc的優點:

  1. 可以支持各種視圖技術,而不僅僅侷限於JSP;

  2. 與Spring框架集成(如IoC容器、AOP等);

  3. 清晰的角色分配:前端控制器(dispatcherServlet) , 請求到處理器映射(handlerMapping), 處理器適配器(HandlerAdapter), 視圖解析器(ViewResolver)。

  4. 支持各種請求資源的映射策略。

2. 請求映射器源碼解析

這些優秀的特性使得他在企業級開發中使用率超過98%,如此優秀的框架,你是否疑惑過,在一個請求到達後,是如何被SpringMvc攔截到並處理的?

 

 

 

相信大家對上面的流程圖都很熟悉,或多或少無論是在準備面試的時候,還是自己學習的時候,都會接觸到這個流程圖,我見過很多的人,對着這個圖死記硬背!我也面試過一些技術人員,問到這塊知識,仰着頭閉着眼(誇張一下)把這塊知識說出來,再往深了問一點就懵逼,歸根到底就是對框架理解不夠深刻。

I. SpringMVC是如何感知到每個方法對應的url路徑的?

org.springframework.web.servlet.handler.AbstractHandlerMethodMapping 實現 org.springframework.beans.factory.InitializingBean 覆蓋 afterPropertiesSet方法,這個方法會在Spring容器初始化的時候回調該方法

 

 

 

該方法類定義爲

@Override
public void afterPropertiesSet() {
    initHandlerMethods();
}

調用initHandlerMethods方法,那麼initHandlerMethods裏面幹了什麼事情呢?對該方法逐步分析!

/**
 * Scan beans in the ApplicationContext, detect and register handler methods.
 * @see #getCandidateBeanNames()
 * @see #processCandidateBean
 * @see #handlerMethodsInitialized
 */
protected void initHandlerMethods() {
    for (String beanName : getCandidateBeanNames()) {
        if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
            processCandidateBean(beanName);
        }
    }
    handlerMethodsInitialized(getHandlerMethods());
}

首先 getCandidateBeanNames() 方法,我們看它的定義

/**
 * Determine the names of candidate beans in the application context.
 * @since 5.1
 * @see #setDetectHandlerMethodsInAncestorContexts
 * @see BeanFactoryUtils#beanNamesForTypeIncludingAncestors
 */
protected String[] getCandidateBeanNames() {
    return (this.detectHandlerMethodsInAncestorContexts ?
            BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class) :
            obtainApplicationContext().getBeanNamesForType(Object.class));
}
  • 這個方法本質的目的就是爲了從bean容器中,獲取所有的bean,爲什麼是獲取全部的 因爲他是基於Object.class類型來獲取的類,故而是全部的類,但是這個方法其實深究起來,知識點很多,因爲它涉及到Spring父子容器的知識點,所以我決定,後面花一篇文檔單獨去講,這裏你只需要知道,這個方法可以獲取Spring容器裏面所有的bean然後返回!

initHandlerMethods() 獲取到所有的bean之後然後循環遍歷,我們將目光聚集在循環體內部的processCandidateBean方法

protected void processCandidateBean(String beanName) {
    Class<?> beanType = null;
    try {
        beanType = obtainApplicationContext().getType(beanName);
    }
    catch (Throwable ex) {
        // An unresolvable bean type, probably from a lazy bean - let's ignore it.
        if (logger.isTraceEnabled()) {
            logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
        }
    }
    if (beanType != null && isHandler(beanType)) {
        detectHandlerMethods(beanName);
    }
}
  • beanType = obtainApplicationContext().getType(beanName);

    • 這個方法是基於bean名稱獲取該類的Class對象
  • isHandler(beanType)

    • 這個方法是判斷該類是是加註了Controller註解或者RequestMapping
@Override
protected boolean isHandler(Class<?> beanType) {
    return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
    AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}
  • detectHandlerMethods(Object handler)
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,(MethodIntrospector.MetadataLookup<T>) method -> {
    try {
    return getMappingForMethod(method, userType);
    }
    catch (Throwable ex) {
        throw new IllegalStateException("Invalid mapping on handler class [" +
                                        userType.getName() + "]: " + method, ex);
    }
  });

內部該段邏輯可以遍歷某個類下所有的方法

  • getMappingForMethod(method, userType); 這個方法的內部做了什麼呢?

    該i方內部讀取所有的映射方法的所有定義,具體的邏輯如下

 

 

 

 設置了該方法 的映射路徑,方法對象,方法參數,設置的方法請求頭,消費類型,可接受類型,映射名稱等信息封裝成RequestMappingInfo對象返回!
  • getPathPrefix(handlerType);

    該方法是處理方法前綴,如果存在和前者方法級別的合併

  • 最終返回一個方法與方法描述信息的map映射集合(Map<Method, RequestMappingInfo>),循環遍歷該集合!

    • Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);找到該方法的代理方法!
    • registerHandlerMethod(handler, invocableMethod, mapping);註冊該方法!
      • 我們深入該方法摒棄其他與本文無關的代碼,會發現這麼一段代碼!

 

 

 

會發現,我們方法上標註的 url會和前面讀取的該方法的定義綁定在一個叫做 urlLookup的方法裏面,請大家記住這個方法,這個方法對我們理解SpringMvc的處理邏輯有大用處!

3.請求獲取邏輯源碼解析

現在,整個工程所有對應的@requestMapping的方法已經被緩存,以該方法爲例子!

@RestController
public class TestController {

    @RequestMapping("test")
    public String test(){
        return "success";
    }
}

現在在urlLookup屬性裏面就有一個 key爲test,value爲test()方法詳細定義的 k:v鍵值對:v:

我們看下下面這個類圖,DispatcherServlet這個關鍵的中央類,實際上是Servlet的子類,熟悉Servlet的同學都知道,之前在做Servlet開發的時候,所有的請求經過配置後都會被內部的doget和dopost方法攔截,至此SpringMvc爲什麼能夠攔截URL也就不難分析了,攔截到url後,進入如下的流程調用鏈!

 

 

 

請求經由 org.springframework.web.servlet.FrameworkServlet#doGet捕獲,委託給org.springframework.web.servlet.FrameworkServlet#processRequest方法,最後在調用org.springframework.web.servlet.DispatcherServlet#doService來處理真正的邏輯!

我們看一下這個方法裏面的一些主要邏輯吧!

org.springframework.web.servlet.DispatcherServlet#doDispatch調用org.springframework.web.servlet.DispatcherServlet#getHandler方法,再次調用org.springframework.web.servlet.handler.AbstractHandlerMapping#getHandler經由org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#getHandlerInternal方法的org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#lookupHandlerMethodorg.springframework.web.servlet.handler.AbstractHandlerMethodMapping.MappingRegistry#getMappingsByUrl講過這麼長的調用鏈是不是懵了,此時我們終於看到了正主!

/**
 * Return matches for the given URL path. Not thread-safe.
 * @see #acquireReadLock()
 */
@Nullable
public List<T> getMappingsByUrl(String urlPath) {
    return this.urlLookup.get(urlPath);
}

這段代碼是不是熟悉?這就是我們Spring容器在初始化的時候將url和方法定義放置的那個屬性,現在Spring容器經由DispatcherServlet攔截請求後又重新找到該方法,並且返回!

此時就完成了MVC流程圖裏面的HandlerMapping處理映射器的部分!

本章關於請求映射器的源碼分析到這也就結束了,後續作者會將處理適配器處理器,視圖解析器一一講明白,其實後續的邏輯也就很簡單了,簡單來說,拿到方法後反射執行該方法(不一定,一般場景是這樣),然後拿到返回值,判斷是否有@responseBody註解,判斷是否需要轉換成json,在通過write寫回到頁面!大致流程就是這樣,詳細過程作者後續會寫!

經過今天的流程分析,你能否基於Servlet寫一個屬於自己的SpringMvc呢?


作者:JAVA程序狗
鏈接:https://juejin.im/post/5eeac8eef265da02f31def54
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章