SpringMVC 源碼解析

作為一個javaer,Spring、SpringMVC目前是一項必不可少的技能了。SpringMVC在struts2之後異軍突起,現如今已經完全超過了struts2,成為了javaweb開發的主流框架。今天我們基於Spring5.0對SpringMVC容器初始化過程、DispatcherServlet初始話過程、DispatcherServlet處理請求流程三個方面進行分析。

WebApplicationContext

WebApplicationContext結構

我們可以看到WebApplicationContext繼承自ApplicationContext,也就是我們web開發中的IOC容器,我們一般用到的是它的實現類 XmlWebApplicationContext。

WebApplicationContext初始化過程

開發web的時候,我們都需要在web.xml中配置Spring的監聽器、Spring配置文件

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-context.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

ContextLoaderListener

我們從ContextLoaderListener入手,講解WebApplicationContext的創建流程。

ContextLoaderListener繼承了ContextLoader,同時實現了ServletContextListener介面,通過實現contextInitialized來初始化IOC容器,而主要的方法都在ContextLoader裡面實現。

@Override
public void contextInitialized(ServletContextEvent event) {
initWebApplicationContext(event.getServletContext());
}

我們來看看initWebApplicationContext方法的具體實現:

//1.先判斷ServletContext中是否已經存在WebApplicationContext,比如我們不小心多配置了一個ContextLoaderListener的時候,就會導致重複的初始化
if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
throw new IllegalStateException(
"Cannot initialize context because there is already a root application context present - " +
"check whether you have multiple ContextLoader* definitions in your web.xml!");
}

Log logger = LogFactory.getLog(ContextLoader.class);
servletContext.log("Initializing Spring root WebApplicationContext");
if (logger.isInfoEnabled()) {
logger.info("Root WebApplicationContext: initialization started");
}
long startTime = System.currentTimeMillis();

try {
if (this.context == null) {
//2.創建WebApplicationContext對象
this.context = createWebApplicationContext(servletContext);
}
if (this.context instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
if (!cwac.isActive()) {
if (cwac.getParent() == null) {
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
}
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
//3.把創建好的IOC容器放到Serverlet容器中
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

ClassLoader ccl = Thread.currentThread().getContextClassLoader();
if (ccl == ContextLoader.class.getClassLoader()) {
currentContext = this.context;
}
else if (ccl != null) {
currentContextPerThread.put(ccl, this.context);
}

return this.context;
}
catch (RuntimeException ex) {
logger.error("Context initialization failed", ex);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
throw ex;
}
catch (Error err) {
logger.error("Context initialization failed", err);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, err);
throw err;
}

我們這裡就不展示過多的源碼細節。

圖片地址:oscimg.oschina.net/oscn

從上面的圖我們可以知道,WebApplicationContext的初始化,確切的說是XmlWebApplicationContext對象的創建過程主要包括以下幾個步驟:

  1. 調用determineContextClass()方法獲取具體的實現類。首先會獲取用戶配置的WebApplicationContext,如果沒有,會使用默認的配置,具體的配置在spring-context模塊下的ContextLoader.properties,裡面配置的默認使用XmlWebApplicationContext; org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext 如果我們需要使用自己的自定義容器,我們需要在web.xml中配置 <context-param> <param-name>contextClass</param-name> <param-value>org.springframework.web.context.support.XmlWebApplicationContext</param-value> </context-param>
  2. 調用ContextLoader的initWebApplicationContext()方法,通過反射創建具體的WebApplicationContext對象。
  3. 設置WebApplication的id,configLocation(也就是我們在web.xml中配置的contextConfigLocation)。
  4. 設置WebApplication的環境變數,設置web的環境參數servletContextInitParams 值為當前的ServletContext 5.customizeContext(),我們需要在容器初始化之前進行一些自己的操作,可以實現ApplicationContextInitializer介面對我們的容器進行一些修改或者操作,只需要在web.xml中配置contextInitializerClasses。 6.調用refresh()方法刷新容器,IOC容器載入配置,解析BeanDefintion初始化bean,初始化配置,運行Bean的後置處理器等等一系列操作都在這個方法裏完成。

@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
//1.準備刷新容器
prepareRefresh();
//2.獲取beanFactory,這一步會調用XmlWebApplication的loadBeanDefinitions()方法,從我們的配置文件載入所有的bean定義
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
//3.準備刷新beanFactory
prepareBeanFactory(beanFactory);
try {
//4.設置beanFactory的後置處理器
postProcessBeanFactory(beanFactory);
//5.運行beanFactory的後置處理器
invokeBeanFactoryPostProcessors(beanFactory);
//6.註冊bean的後置處理器,這一步主要是獲取4 5兩步中設置的BeanPostProcessor進行分類、排序等處理,以確定BeanPostProcessor的調用順序
registerBeanPostProcessors(beanFactory);
//7.初始化MessageSource,用來處理國際化
initMessageSource();
//8.註冊事件廣播器
initApplicationEventMulticaster();
//9.再次初始化其它的bean
onRefresh();
//10.註冊監聽器
registerListeners();
//11.完成對BeanFactory的初始化,初始化所有非non-lazy-init的bean,這一步會創建bean實例、運行bean的後置處理器、載入我們配置的properties文件等
finishBeanFactoryInitialization(beanFactory);
//12.完成刷新,這裡會發布一個ContextRefreshedEvent的事件
finishRefresh();
} catch (BeansException ex) {
destroyBeans();
cancelRefresh(ex);
throw ex;
} finally {
resetCommonCaches();
}
}

到這裡,我們WebAp初始化就已經完成了,但是我們不免有個疑問,我們的Controller,HandlerInterceptor這些沒有在我們設置的contextConfigLocation裡面,那他們又是怎麼初始化的呢,下面我們繼續對DispatcherServlet的初始化過程進行分析。

DispatcherServlet初始化

web.xml配置

<!-- spring mvc servlet -->
<servlet>
<servlet-name>mvc-dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>mvc-dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>

這裡我們又配置了一個contextConfigLocation,這個就是SpringMVC容器的配置文件,我們一開始也配置了一個contextConfigLocation,他們之間沒有任何的區別,我們第一個配置的contextConfigLocation是作為一個父容器,現在配置的是作為一個子容器,我們只能有一個父容器,但是可以配置多個子容器,也就是我們可以在一個項目中配置多個DispatcherServlet。

DispatcherServlet初始化過程

  • DispatcherServlet本質還是一個Servlet,Servlet的初始化要求我們實現init()方法,DispatcherServlet的init()方法是在HttpServletBean中實現的。

[@Override](https://my.oschina.net/u/1162528)
public final void init() throws ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Initializing servlet " + getServletName() + "");
}

PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {

throw ex;
}
}

//最主要的操作都在這裡面了
initServletBean();
}

從上面的圖我們可以知道,SpringMVC子容器的初始是在DispatcherServlet初始化的時候完成的,主要包括下面幾個方面:

  1. 執行init()方法。
  2. 執行initServletBean(),這個方法具體實現在FrameworkServlet裡面。
  3. 執行initWebApplicationContext(),這個方法和我們前面講過的初始化父IOC容器的流程基本一致,我們可以對比著看看,一些比較大的區別就是現在創建的子容器會一個parent contetx,也就是我們之前創建的WepApplicationContext,我們怎麼獲取到這個parent context呢,就是從ServletContext中獲取,我們之前創建好父容器之後把它放到了ServletContext中。 ServletContext#getAttribute()
  4. 設置自容器的環境變數,設置web的環境參數servletContextInitParams 值為當前的ServletContext,設置namespace,用來區別不同的子容器,添加一個監聽器,監聽我們在父容器發布的事件,這裡監聽刷新容器,調用了refresh()方法。
  5. applyInitializers(),我們需要在容器初始化之前進行一些自己的操作,可以實現ApplicationContextInitializer介面對我們的容器進行一些修改或者操作,只需要在web.xml中配置contextInitializerClasses。
  6. 調用refresh()方法刷新容器,IOC容器載入配置,解析BeanDefintion初始化bean,初始化配置,運行Bean的後置處理器等等一系列操作都在這個方法裏完成。關於refresh()方法,前面已經具體分析過了,我們就不在分析了。
  7. onfresh(),上面幾部已經把我們的子容器初始化完成了,繼續調用DispatcherServlet的onfresh()方法,驗證和初始化HandleMapping、HandlerAdapter、ViewResolvers、ExceptionHandler等一系列前端控制器策略。

@Override
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}

protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}

至此,我們已經分析了在web項目中父容器的初始化流程和自容器的初始化流程,下面的內容我們來分析DispatcherServlet處理請求的流程。

DispatcherServlet處理請求

我們先回憶下在沒有MVC框架的時候,我們是怎麼處理請求的。在我們還在通過繼承Servlet介面的遠古時代,我們的請求到達Servelt的時候,會調用service()方法,我們通過重寫doPost()、doGet()等方法,自己操作Request、Response處理請求,當然我們需要在web.xml中配置一大堆的servelt。

圖片地址:oscimg.oschina.net/oscn

從上面的圖中我們可以看到DispatcherServlet處理請求主要包括以下幾個方面:

  • 用戶發起請求,DispatcherServlet攔截到請求。
  1. 調用父類FrameworkServlet的sercice()方法。
  2. FrameworkServlet重寫了HttpServlet的service()方法doGet()、doPost()等doXX()方法,通過不同的請求類型,調用不同的doXX()方法處理請求。
  3. 而重寫的這些doXX()方法都是調用了FrameworkServlet的processRequest()方法進行處理。
  • processRequest()處理請求。
  1. 調用buildRequestAttributes()重新構建請求參數。
  2. initContextHolders()把當前請求參數設置到RequestContextHolder()中,通過ThreadLocal讓請求與當前線程綁定。
  3. 調用doService()方法處理請求。
  4. 最後發布一個ServletRequestHandledEvent事件。
  • doService()方法。doService()方法是由DispatcherServlet實現的,首先會把當前子容器對象、LocaleResolver、ThemeResolver等設置到當前請求中,然後調用doDispatch()方法
  • doDispatch()方法。這個方法就是DispatcherServlet處理請求的主要方法。
  1. 首先獲取到HandlerExecutionChain,HandlerExecutionChain結構很簡單,包括一個我們具體處理請求的Controller層方法的對象handler,以及攔截器interceptors、interceptorList。如果沒有獲取到相應的HandlerExecutionChain,則會返回404。
  2. 然後找到合適的HandlerAdapter。順序調用攔截器的preHandle()方法,如果不符合攔截器的攔截規則則會同時調用afterCompletion()方法。
  3. 調用HandlerAdapter的handle()方法,返回一個ModelAndView對象。handle()方法會通過反射調用我們具體的Controller層方法,完成業務邏輯處理。
  4. 調用攔截器的postHandle()。
  5. 調用processDispatchResult()對請求結果進行處理。如果業務處理過程中有異常拋出,調用processHandlerException()對異常進行處理,這裡會調用我們自定義的HandlerExceptionResolvers進行處理,然後返回一個異常處理結果的ModelAndView對象。
  6. DisPatcherServlet的render()方法。調用resolveViewName()獲取到具體的View對象,比如RedirectView進行重定向、其它擴展的如FastJsonJsonView、MappingJackson2JsonView進行json處理、MappingJackson2XmlView進行xml處理等。
  7. View的render()方法。根據不同的View對象實例,對返回結果做不同的對象,重定向、返回json、返回xml、返回頁面等。
  8. 調用攔截器afterCompletion()方法。

至此,我們對SpringMVC WebApplicationContext初始化、DispatcherServlet初始化、DispatcherServlet請求處理流程進行了大概的分析。總體來說,分析的還是比較粗糙,很多細節的方面沒有深入分析,建議大家多去看看源碼內容,會有不一樣的理解和感受。文章中可能有表達不準確、理解不正確的地方,歡迎隨時指正,一起探討。


推薦閱讀:
相關文章