一、簡介
Spring MVC是一個基於Java的實現了MVC設計模式的請求驅動類型的輕量級Web框架,通過把Model,View,Controller分離,將web層進行職責解耦,把複雜的web應用分成邏輯清晰的幾部分,簡化開發,減少出錯,方便組內開發人員之間的配合。
1. Springmvc的優點:
-
可以支持各種視圖技術,而不僅僅侷限於JSP;
-
與Spring框架集成(如IoC容器、AOP等);
-
清晰的角色分配:前端控制器(dispatcherServlet) , 請求到處理器映射(handlerMapping), 處理器適配器(HandlerAdapter), 視圖解析器(ViewResolver)。
-
支持各種請求資源的映射策略。
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#lookupHandlerMethod
的org.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