在軟體開發的中,如果某些特性的使用比較普遍,那麼這些特性往往可以作為平台特性來實現,通過對這些平台特性進行有效封裝,使其向其他應用開放。正是如此,Spring由於其IOC、AOP、交易處理、持久化驅動等特點,使得其起到了一個應用平台的作用。Spring MVC是Spring的一個重要的模組,其web應用的實現,是由Spring的來支撐的,Spring MVC的是實現也是依託再Spring平台提供的基礎特性的。本文主要是介紹Spring mvc容器初始化的過程,從中可以看出Spring MVC的對於Spring的依賴。 一、從Web應用角度看Spring MVC
在Servlet模型中,要求-回應的實現依賴於兩大元素的共同配合:
1. 配置Servlet及其映射關係(在web.xml中)
2. 在Servlet實作類別中完成響應邏輯
項目規模擴大之後,要求-回應的映射關係全部定義在web.xml中,將造成web.xml的不斷膨脹而變得難以維護。針對這個問題,SpringMVC提出的方案就是:提煉一個核心的Servlet覆蓋對所有Http請求的處理。這一被提煉出來的Servlet,通常被我們稱之為:核心分發器。在SpringMVC中,核心分發器就是org.springframework.web.servlet.DispatcherServlet。
核心分發器要解決的是下面兩個問題:
問題1:核心Servlet應該能夠建立起一整套完整的對所有Http請求進行正常化處理的流程。
問題2:核心Servlet應該能夠根據一定的規則對不同的Http請求分發到不同的Servlet對象上去進行處理。
針對上面的這個兩個問題,SpringMVC的解決方案是:將整個處理流程正常化,並把每一個處理步驟指派到不同的組件中進行處理。
處理流程正常化 :將處理流程劃分為若干個步驟(任務),並使用一條明確的邏輯主線將所有的步驟串聯起來
處理流程組件化 : 將處理流程中的每一個步驟(任務)都定義為介面,並為每個介面賦予不同的實現模式
處理流程正常化的首要內容就是考慮一個通用的Servlet響應程式大致應該包含的邏輯步驟: 對Http請求進行初步處理,尋找與之對應的Controller處理類(方法) 調用相應的Controller處理類(方法)完成商務邏輯 對Controller處理類(方法)調用時可能發生的異常進行處理 根據Controller處理類(方法)的調用結果,進行Http響應處理
所謂的組件化,實際上也就是使用程式設計語言將這些邏輯語義表達出來。在Java語言中,最適合表達邏輯處理語義的文法結構是介面,而介面可以有不同的實現,因此上述的四個流程也就被定義為了四個不同介面,它們分別是: HandlerMapping HandlerAdapter HandlerExceptionResolver ViewResolver 二、從Spring角度看Spring MVC
從上面可以看出,組件是核心分發器(DispatchServlet)的核心所在,它們是http請求處理的邏輯載體,DispatcherServlet是邏輯處理的調度中心,組件則是被調度的操作對象。而Spring容器在這裡所起到的作用,是協助DispatcherServlet更好地對組件進行管理。
我們知道,SpringMVC的組件是一個個的介面定義,當我們在SpringMVC的核心設定檔中定義一個組件時,使用的卻是組件的實作類別,用具體的實作類別來指定組件的行為模式,不同的實作類別代表了不同的行為模式,它們在Spring中是可以共存的。Spring容器對這些實作類別進行管理,具體如何使用,由應用程式本身來決定。
上圖是Spring官方reference中的一幅圖,DispatchServlet對外接收http的請求,而請求的處理的是依靠組件來完成的,組件的介面實現的是依靠Spring IOC容器(WebApplicationContext)來管理。從這個圖中我們可以看出,Spring MVC實現web應用是依賴與Spring提供的基礎特性(IOC等)。圖中的兩個WebApplicationContext的區別,留到下面再講。 三、Spring MVC 入口設定檔web.xml
Spring mvc 有哪些設定檔: 入口設定檔:web.xml;由web或應用伺服器為每個web項目載入的設定檔。 應用上下文:包括web架構特有設定檔:SpringMVC的${dispatcherServletName}-servlet.xml設定檔。和Spring的設定檔applicationContext.xml,applicationContext-*.xml。
遵循servlet規範,Spring MVC的web應用的入口設定檔也是web.xml。web容器的初始化首先是載入web.xml檔案,Jetty在啟動時首先載入此設定檔,並且對其中定義的listener和servlet等進行相應的載入和初始化。Jetty並不清楚(也不關心)其他設定檔的存在,因此,載入其他設定檔應該是你(架構)的事情。那麼怎麼載入呢。前面說過Jetty在啟動時會自動載入和初始化listener和servlet,那麼我們可以自訂一個listener(ContextLoaderListener)或servlet(DispatcherServlet),Jetty會根據web.xml載入和初始化他們,而他們則負責載入相應的設定檔。
在web.xml設定檔中,有兩個主要的配置:ContextLoaderListener和DispatcherServlet。同樣的關於spring設定檔的相關配置也有兩部分:context-param和DispatcherServlet中的init-param。那麼,這兩部分的分別表示是Spring 容器的初始化和web容器的初始化。DispatcherServlet和ContextLoaderListener提供了在Web容器中對spring的介面。ServletContext為Spring的IOC容器提供了一個宿主環境,在宿主環境中,Spring MVC建立起了一個IOC容器體系。
下面看一下Spring mvc 中web.xml檔案的相關配置內容:
<?xml version="1.0" encoding="UTF-8"?><web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"> <display-name>push-center-server</display-name> <description><span style="font-family: Arial, sans-serif;">test</span></description> <context-param> <param-name>webAppRootKey</param-name> <param-value>test</param-value> </context-param> <context-param> <param-name>contextConfigLocation</param-name> <param-value> classpath:applicationContext.xml </param-value> </context-param> <context-param> <param-name>log4jConfigLocation</param-name> <param-value>classpath:log4j2.xml</param-value> </context-param> <!-- 項目使用的設定檔位置.項目啟動自動讀取 --> <context-param> <param-name>propertiesConfigLocation</param-name> <param-value>/WEB-INF/conf/config.properties</param-value> </context-param> <!--jmonitor--> <context-param> <param-name>jmonitor-configfile</param-name> <param-value>jmonitor.properties</param-value> </context-param> <listener> <listener-class>com.meituan.jmonitor.servlet.ContextListener</listener-class> </listener> <listener> <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class> </listener> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <listener> <listener-class> org.springframework.web.context.request.RequestContextListener </listener-class> </listener> <!--jmonitor http collector--> <filter> <filter-name>JMonitorHttpMonitorFilter</filter-name> <filter-class>com.meituan.jmonitor.collector.http.HttpMonitorFilter</filter-class> </filter> <filter-mapping> <filter-name>JMonitorHttpMonitorFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <servlet> <servlet-name>appServlet</servlet-name> <servlet-class>com.sankuai.meituan.web.MtDispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:webmvc-config.xml</param-value> </init-param> <load-on-startup>2</load-on-startup> </servlet> <servlet-mapping> <servlet-name>appServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping></web-app>
四、Spring IOC容器(根上下文)的初始化
Spring Framework本身沒有Web功能,Spring MVC使用WebApplicationContext介面擴充ApplicationContext,使得擁有web功能,WebApplicationContext介面預設的實現是XmlWebApplicationContext。那麼,Spring MVC是如何在web環境中建立IoC容器呢。
先看一下WebApplicationContext的源碼,
WebApplicationContext
public interface WebApplicationContext extends ApplicationContext { //用於在ServletContext中存取根上下文 String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT"; String SCOPE_REQUEST = "request"; String SCOPE_SESSION = "session"; String SCOPE_GLOBAL_SESSION = "globalSession"; String SCOPE_APPLICATION = "application"; String SERVLET_CONTEXT_BEAN_NAME = "servletContext"; String CONTEXT_PARAMETERS_BEAN_NAME = "contextParameters"; String CONTEXT_ATTRIBUTES_BEAN_NAME = "contextAttributes"; //取得當前web容器的ServletContext ServletContext getServletContext();}
在Spring MVC中,Spring Context是以父子的繼承結構存在的。Web環境(ServletContext)中存在一個根上下文,這個Context是整個應用的根上下文,是其他context的雙親Context。同時Spring MVC也對應的持有一個獨立的Context,它是根內容相關的子上下文。
由ContextLoaderListener首先啟動的上下文為根上下文,該上下文是與ServletContext相伴而生的,在根內容相關的基礎上,Spring MVC對應持有的一個用來管理控制器需要的對象的子上下文。下圖是ContextLoaderListener繼承關係,它實現了ServletContextListener介面,這個介面提供了與Servlet生命週期結合的回調,比如contextInitialized和contextDestroyed方法。建立WebApplicationContext的過程是在contextInitialized的介面實現中完成的,具體的載入IOC容器的過程是由ContextLoader來完成的。
ContextLoaderListener
public class ContextLoaderListener extends ContextLoader implements ServletContextListener { private ContextLoader contextLoader; public ContextLoaderListener() { } public ContextLoaderListener(WebApplicationContext context) { super(context); } public void contextInitialized(ServletContextEvent event) { this.contextLoader = createContextLoader(); if (this.contextLoader == null) { this.contextLoader = this; } this.contextLoader.initWebApplicationContext(event.getServletContext()); } @Deprecated protected ContextLoader createContextLoader() { return null; } @Deprecated public ContextLoader getContextLoader() { return this.contextLoader; } public void contextDestroyed(ServletContextEvent event) { if (this.contextLoader != null) { this.contextLoader.closeWebApplicationContext(event.getServletContext()); } ContextCleanupListener.cleanupAttributes(event.getServletContext()); }
從ContextLoaderListener源碼可以看出,實現的是ServletContextListener介面,這個介面裡的函數會結合Web容器的生命週期被調用。因為ServletContextListener是ServletContext的監聽者,當servletContext啟動或者停止的時候,會觸發響應的事件,監聽器ServletContextListener會接收到事件,會做出相應的響應,比如這裡的contextInitialized方法和contextDestroyed方法。Spring IOC容器(根上下文)的產生與銷毀就是通過這個兩個方法的,所以根上下文是與ServletContext相伴而生的。
所以當Web應用啟動時,contextInitialized方法會執行載入根上下文(IOC容器),具體過程是首先從Servlet事件中得到ServletContext,然後以ServletContext為宿主環境,載入根上下文(IOC容器),具體的載入過程是由ContextLoader處理的。
下圖所示為根內容相關的載入過程,下面將結合源碼來看一下這個過程是如何?的。
根內容相關的載入過程:
ROOT Context是在ContextLoaderListener中配置的,ContextLoaderListener讀取context-param中的contextConfigLocation指定的設定檔,建立ROOT Context。下面看一下ContextLoaderListener中建立context的源碼:
ContextLoader載入根內容相關的源碼
//這裡開始對WebApplicationContext進行初始化public WebApplicationContext initWebApplicationContext(ServletContext servletContext) { //在整個web應用中,只能有一個根上下文,判斷ServletContext中是否已經有根上下文 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 { // Store context in local instance variable, to guarantee that // it is available on ServletContext shutdown. if (this.context == null) {// 執行了建立WebApplicationContext的操作 this.context = createWebApplicationContext(servletContext); } if (this.context instanceof ConfigurableWebApplicationContext) { ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context; if (!cwac.isActive()) { // The context has not yet been refreshed -> provide services such as // setting the parent context, setting the application context id, etc if (cwac.getParent() == null) { // The context instance was injected without an explicit parent -> // determine parent for root web application context, if any.//載入根內容相關的雙親上下文 ApplicationContext parent = loadParentContext(servletContext); cwac.setParent(parent); } configureAndRefreshWebApplicationContext(cwac, servletContext); } } //將根上下文放置在servletContext中 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 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; } } protected WebApplicationContext createWebApplicationContext(ServletContext sc) {// 根據web.xml中的配置,決定用那種WebApplicationContext,預設用XmlWebApplicationContext Class<?> contextClass = determineContextClass(sc);//判斷contextClass是否繼承ConfigurableWebApplicationContext或者是其介面實現 if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) { throw new ApplicationContextException("Custom context class [" + contextClass.getName() + "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]"); }//直接執行個體化需要產生的IOC容器 return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass); } //設定IOC容器各個參數,然後通過refresh啟動容器的初始化protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {//設定application context ID if (ObjectUtils.identityToString(wac).equals(wac.getId())) { // The application context id is still set to its original default value // -> assign a more useful id based on available information String idParam = sc.getInitParameter(CONTEXT_ID_PARAM); if (idParam != null) { wac.setId(idParam); } else { // Generate default id... if (sc.getMajorVersion() == 2 && sc.getMinorVersion() < 5) { // Servlet <= 2.4: resort to name specified in web.xml, if any. wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + ObjectUtils.getDisplayString(sc.getServletContextName())); } else { wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + ObjectUtils.getDisplayString(sc.getContextPath())); } } }//設定ServletContext wac.setServletContext(sc);//設定設定檔的位置參數 String initParameter = sc.getInitParameter(CONFIG_LOCATION_PARAM); if (initParameter != null) { wac.setConfigLocation(initParameter); }//在refresh之前,根據配置的config locations重新定製(Customize)ConfigurableWebApplicationContext customizeContext(sc, wac);//啟動容器的初始化 wac.refresh(); }
從上面的源碼中可以看出來,IOC容器在web容器中的啟動過程,與在應用中啟動IOC容器的方式相似,不同的是這裡需要考慮web容器的環境的特點,比如各種參數的設定,IOC容器與web容器ServletContext的結合等。在初始化上下文以後,該上下文被儲存再ServletContext中,這樣就建立了一個全域的關於整個應用的上下文,即所謂的根上下文。同時在啟動Spring MVC的時候,DispatchServlet在進行自己持有的內容相關的初始化時,是將此根上下文設定為自己的雙親內容相關的。
http://blog.csdn.net/and1kaney/article/details/51214193 這篇文章可以看到根容器初始化過程的整個的call hierarchy。 五、Spring MVC容器(子上下文)的初始化
以上是web容器中根內容相關的載入與初始化,在完成對ContextLoaderListener的初始化以後,web容器開始初始化DispatchServlet,DispatchServlet會建立自己的上下文來管理Spring MVC的bean對象。在建立這個自己持有的內容相關的時候,會從ServletContext中得到根上下文作為DispatchServlet持有的內容相關的雙親上下文,再對自己持有的上下文進行初始化,最後把自己持有的這個上下文也儲存到ServletContext中。
我們先看下DispatchServlet的繼承關係,如下圖。DispatchServlet通過繼承FrameworkServlet和HttpServletBean而繼承了HttpServlet。HttpServletBean是Spring對於Servlet最低層次的抽象。在這一層抽象中,Spring會將這個Servlet視作是一個Spring的bean,並將web入口設定檔web.xml中DispatchServlet定義的init-param參數中的值作為bean的屬性注入進來。
DispatcherServlet也是一個Servlet,根據Servlet規範的定義,Servlet中的兩大核心方法init方法和service方法:
1. init方法
在整個系統啟動時運行,且只運行一次。因此,在init方法中我們往往會對整個應用程式進行初始化操作。這些初始化操作可能包括對容器(WebApplicationContext)的初始化、組件和外部資源的初始化等等。
2. service方法
在整個系統啟動並執行過程中處於偵聽模式,偵聽並處理所有的Web請求。因此,在service及其相關方法中,我們看到的則是對Http請求的處理流程。
這篇文章主要是介紹Spring mvc 容器的初始化,所以主要是介紹iDisipatchServlet的init方法。
HttpServletBean
@Override public final void init() throws ServletException { if (logger.isDebugEnabled()) { logger.debug("Initializing servlet '" + getServletName() + "'"); } // Set bean properties from init parameters. try {//讀取web.xml中DispatchServlet定義中的<init-param>,對Bean屬性進行配置 Prop