Spring容器啟動原始碼解析
1. 前言
最近搭建的工程都是基於SpringBoot,簡化配置的感覺真爽。但有個以前的專案還是用SpringMvc寫的,看到滿滿的配置xml檔案,卻有一種想去深入瞭解的衝動。折騰了好幾天,決心去寫這篇關於Spring啟動的部落格,自己是個剛入職的小白,技術水平有限,也是硬著頭皮看原始碼去Debug,很多不懂的地方還請諒解!
2. 概述
先給出幾個讓我頭皮發麻的概念:web容器,Spring容器,SpringMvc容器
容器就是管理物件的地方,例如web容器就是管理servlet的地方,Spring容器就是管理Service,dao等Bean的地方,SpringMvc就是管理Controller等bean的地方(下文會做解釋)。一個SpringMvc專案的啟動離不開上述三個容器。所以這就是這篇文章的講點,各個容器的啟動過程解析。
3. Web容器初始化過程
官方文件是對於Web容器初始化時是這樣描述的(英文不懂,已翻譯成中文)
1. 部署描述檔案(web.xml)中的<listener>標記的監聽器會被建立和初始化
2. 對於實現了ServletContextListener的監聽器,會執行它的初始化方法 contextInitialized()
3. 部署描述檔案中的<filter>標記的過濾器會被建立和初始化,呼叫其init()方法
4. 部署描述檔案中的<servlet>標記的servlet會根據<load-on-startup>中的序號建立和初始化,呼叫init()方法
大致流程瞭解之後,結合自己的SpringMvc專案一步步深入,先貼一下基本的web.xml檔案
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:web="http://java.sun.com/xml/ns/javaee" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5"> <display-name>dmpserver</display-name> <welcome-file-list> <welcome-file>login.jsp</welcome-file> </welcome-file-list> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring.xml</param-value> </context-param> <context-param> <param-name>log4jConfigLocation</param-name> <param-value>classpath:log4jConfig.xml</param-value> </context-param> <filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>utf-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <listener> <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class> </listener> <servlet> <description>spring mvc servlet</description> <servlet-name>rest</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> </web-app>
1. 容器會先解析<context-param>中的鍵值對(上述程式碼重點關注Spring配置檔案Spring.xml)
2. 容器建立一個application內建物件servletContext(可以理解為servlet上下文或web容器),用於全域性變數共享
3. 將解析的<context-param>鍵值對存放在application即servletContext中
4. 讀取<listener>中的監聽器,一般會使用ContextLoaderListener類,呼叫其contextInitialized方法,建立IOC容器(Spring容器)webApplicationContext。將webApplication容器放入application(servlet上下文)中作為根IOC容器,鍵名為WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE 注意的是,webApplicationContext是全域性唯一的,一個web應用只能有一個根IOC容器。因為這個根IOC容器是讀取<context-param>配置的鍵值對來建立Bean,這個根IOC容器只能訪問spring.xml中配置的Bean,我們在Spring.xml中一般配置的是service,dao等Bean。所以根IOC容器(Spring容器)只能管理service,dao等Bean
5. listener載入完畢後,載入filter過濾器
6. 載入servlet,一般springMvc專案中會優先載入 DispatcherServlet(現在開始載入SpringMvc容器了)
7. DispatcherServlet的父類FrameworkServlet重寫了其父類的initServletBean()方法,在初始化時呼叫initWebApplicationContext()方法和onRefresh()方法
8. initWebApplicationContext()方法會在servletContext(即當前servlet上下文)建立一個子IOC容器(即SpringMvc容器),如果存在上述的根IOC容器,就設定根IOC容器作為父容器,如果不存在,就將父容器設定為NULL
9. 讀取<servlet>標籤的<init-param>配置的xml檔案並載入相關Bean。此時載入的是Spring-mvc.xml配置檔案,管理的是Controller等Bean
10. onRefresh()載入其他元件
4. 啟動過程分析
4.1 listener初始化Spring容器
tomcat啟動後,<context-param>標籤的內容讀取後會被放進application中,做為Web應用的全域性變數使用,接下來建立listener時會使用到這個全域性變數,因此,Web應用在容器中部署後,進行初始化時會先讀取這個全域性變數,之後再進行上述講解的初始化啟動過程。
檢視ContextLoaderListener原始碼
public class ContextLoaderListener extends ContextLoader implements ServletContextListener { public ContextLoaderListener() { } public ContextLoaderListener(WebApplicationContext context) { super(context); } public void contextInitialized(ServletContextEvent event) { this.initWebApplicationContext(event.getServletContext()); } public void contextDestroyed(ServletContextEvent event) { this.closeWebApplicationContext(event.getServletContext()); ContextCleanupListener.cleanupAttributes(event.getServletContext()); } }
據官方文件說明,實現ServletContextListener介面,執行contextInitialized(),進入initWebApplicationContext方法。contextInitialized()和contextDestroyed()方法會在web容器啟動或銷燬時執行。網上查了下此處設計模式用到的是觀察者模式和代理模式,自己也不懂就不做詳解了
檢視ContextLoader.class中的initWebApplicationContext方法
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) { /* 首先通過WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE 這個String型別的靜態變數獲取一個根IoC容器,根IoC容器作為全域性變數 儲存在application物件中,如果存在則有且只能有一個 如果在初始化根WebApplicationContext即根IoC容器時發現已經存在 則直接丟擲異常,因此web.xml中只允許存在一個ContextLoader類或其子類的物件 */ 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!"); } else { 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) { // 建立一個根IOC容器 this.context = this.createWebApplicationContext(servletContext); } if (this.context instanceof ConfigurableWebApplicationContext) { ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext)this.context; if (!cwac.isActive()) { if (cwac.getParent() == null) { // 為根IOC容器設定一個父容器 ApplicationContext parent = this.loadParentContext(servletContext); cwac.setParent(parent); } this.configureAndRefreshWebApplicationContext(cwac, servletContext); } } //將建立好的IoC容器放入到application物件中,並設定key為WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE 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); } if (logger.isDebugEnabled()) { logger.debug("Published root WebApplicationContext as ServletContext attribute with name [" + WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]"); } if (logger.isInfoEnabled()) { long elapsedTime = System.currentTimeMillis() - startTime; logger.info("Root WebApplicationContext: initialization completed in " + elapsedTime + " ms"); } return this.context; } catch (RuntimeException var8) { logger.error("Context initialization failed", var8); servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, var8); throw var8; } catch (Error var9) { logger.error("Context initialization failed", var9); servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, var9); throw var9; } } }
initWebApplicationContext方法的主要目的是建立一個根IOC容器,並放入servlet上下文中。看上述原始碼可知,根IOC容器只能僅有一個,作為全域性變數儲存在servletContext中。將根IoC容器放入到application物件之前進行了IoC容器的配置和重新整理操作,呼叫了configureAndRefreshWebApplicationContext()方法,該方法原始碼如下:
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) { String configLocationParam; if (ObjectUtils.identityToString(wac).equals(wac.getId())) { configLocationParam = sc.getInitParameter("contextId"); if (configLocationParam != null) { wac.setId(configLocationParam); } else { wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + ObjectUtils.getDisplayString(sc.getContextPath())); } } wac.setServletContext(sc); /* 在容器啟動時,會把<context-param>中的內容放入servlet上下文的全域性變數中, 此時獲取key為contextConfigLocation的變數,及Spring.xml配置檔案 將其放入到webApplicationContext中 */ configLocationParam = sc.getInitParameter("contextConfigLocation"); if (configLocationParam != null) { wac.setConfigLocation(configLocationParam); } ConfigurableEnvironment env = wac.getEnvironment(); if (env instanceof ConfigurableWebEnvironment) { ((ConfigurableWebEnvironment)env).initPropertySources(sc, (ServletConfig)null); } this.customizeContext(sc, wac); wac.refresh(); }
configureAndRefreshWebApplicationContext方法比較重要的是把配置檔案資訊放入根IOC容器中。方法最後呼叫了refresh()方法,對配置檔案資訊(Bean)進行載入。因為refresh()這是個ConfigurableApplication-Context介面方法,想到了它的常用實現類ClassPathXmlApplicationContext,一層層進去找到了Abstract-ApplicationContext,實現了refresh(),見如下原始碼:
public void refresh() throws BeansException, IllegalStateException { Object var1 = this.startupShutdownMonitor; synchronized(this.startupShutdownMonitor) { this.prepareRefresh(); ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory(); this.prepareBeanFactory(beanFactory); try { this.postProcessBeanFactory(beanFactory); this.invokeBeanFactoryPostProcessors(beanFactory); this.registerBeanPostProcessors(beanFactory); this.initMessageSource(); this.initApplicationEventMulticaster(); this.onRefresh(); this.registerListeners(); this.finishBeanFactoryInitialization(beanFactory); this.finishRefresh(); } catch (BeansException var9) { if (this.logger.isWarnEnabled()) { this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var9); } this.destroyBeans(); this.cancelRefresh(var9); throw var9; } finally { this.resetCommonCaches(); } } }
該方法主要用於建立並初始化contextConfigLocation類配置的xml檔案中的Bean,因此,如果我們在配置Bean時出錯,在Web應用啟動時就會丟擲異常,而不是等到執行時才丟擲異常。因為技術能力有限加上此處方法太多,就不在一一解析了。到此為止,整個Spring容器載入完畢,下面開始載入SpringMVC容器
4.2 Filter初始化
因為Filter的操作沒有涉及IOC容器,此處不做詳解,上面web.xml中配置的是一個UTF8編碼過濾器
5. 總結
時間有限,只大致介紹了Spring容器的初始化,後面還沒來得及整理,對於springMvc容器的建立和初始化下篇文章見