1. 程式人生 > >Spring MVC原理之Spring應用上下文(IoC容器)在Web容器中的啟動分析

Spring MVC原理之Spring應用上下文(IoC容器)在Web容器中的啟動分析

Spring IoC是一個獨立的模組,它並不是直接在Web容器中發揮作用的。如果要在Web環境中使用IoC容器,需要Spring為IoC設計一個啟動過程,把IoC容器導人,並在Web容器中建立起來。具體說來,這個啟動過程是和Web容器的啟動過程整合在一起的。在這個過程中,一方面處理Web容器的啟動,另一方面通過設計特定的Web容器攔截器,將IoC容器載入到Web環境中來.並將其初始化。在這個過程建立完成以後, IoC容器才能正常工作,而SpringMVC是建立在IoC容器的基礎上的,這樣才能建立起MVC框架的執行機制,從而響應從Web容器傳遞的HTTP請求。

下面以Tomcat容器為例子進行分析。在Tomcat中,web.xml

是應用的部署描述檔案。在web.xm.中常常看到與Spring相關的部署描述如下:

    <!-- 載入spring容器 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring/applicationContext-*.xml</param-value>
    </context-param>
    <listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!-- springmvc的前端控制器 --> <servlet> <servlet-name>tt-manager</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class
>
<!-- contextConfigLocation不是必須的, 如果不配置contextConfigLocation, springmvc的配置檔案預設在:WEB-INF/servlet的name+"-servlet.xml" --> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/springmvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>tt-manager</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>

這裡看到的部署描述是Spring MVCTomcat的介面部分。在這個部署描述檔案中。首先定義了一個Servlet物件,它是Spring MVCDispatcherServlet。這個DispatcherServletMVC中很重要的一個類,起著分發請求的作用。同時,在部署描述中,為這個DispatcherServlet定義了應的URL對映,這些URL對映為這個Servlet指定了需要處理的HTTP請求。

context-param引數的配裡用來指定Spring IoC容器讀取Bean定義的XML檔案的路徑,在這裡。這個配且檔案被定義為classpath:spring/applicationContext-*.xml下的檔案中,可以看到Spring應用的Bean配置。最後,作為Spring MVC的啟動類,Contex tLoaderListener被定義為一個監聽器,這個監聽器是與Web伺服器的生命週期相關聯的。由ContextLoaderListener監聽器負責完成IoC容器在Web環魂中的啟動工作.

IoC容器啟動的基本過程

IOC容器的啟動過程就是建立上下文的過程,該上下文是與SerVietContext相伴而生的,同時也是IoC容器在Web應用環境中的具體表現之一。由ConteztLoaderListener啟動的上下文為根上下文。在根上下文的基礎上.還有一個與Web MVC相關的上下文用來儲存控制器(DispatcherServlet)需要的MVC物件,作為根上下文的子上下文,構成一個層次化的上下文體系。

在Web容器中啟動Spring應用程式時.首先建立根上下文.然後建立這個上下文體系的,這個上下文體系的建立是由ContextLoder來完成的。具體如下圖:

web.xml中,已經配了ContextLoaderListener,這個ConteztLoaderListenerSpring提供的類,是為在Web容器中建立IoC容器服務的。它實現了ServletContextListener介面。這個介面是在Servlet API中定義的,提供了與Servlet生命週期結合的回撥,比如contextlnitialized方法和contextDestroyed方法。
而在Web容器中,建立WebApplicationContext的過程,是在contextlnitialized的介面實現中完成的。具體的載入IoC容器的過程是由ContextLoaderListener交由ContextLoader來完成的,而ContextLoader本身就是ContextLoaderListener的基類.它們之間的類關係如下圖:

總之,ContextLoaderListener是SpringMVC的入口,通過父類ContextLoader來實現IoC容器的初始化,通過實現ServletContextListener介面,通過監聽來建立或銷燬WebApplicationContext(IoC容器)。

Web容器中的上下文設計

為了方便在Web環境中使用IoC容器,Spring為Web應用提供了上下文的擴充套件介面WebApplicationContext來滿足啟動過程的需要。

在這個類繼承關係中,可以從熟悉的XmlWebApplicationContext入手來了解它的介面實現.在介面設計中,最後是通過ApplicationContex介面與BeanFactory介面對接的,而對於具體的功能實現,很多都是封裝在其基類AbstractRefreshableWebApplicationContext中完成的。
WebApplicationContextXmlWebApplicationContext中定義了很多常量,如下:

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_APPLICATION = "application";
    String SERVLET_CONTEXT_BEAN_NAME = "servletContext";
    String CONTEXT_PARAMETERS_BEAN_NAME = "contextParameters";
    String CONTEXT_ATTRIBUTES_BEAN_NAME = "contextAttributes";

    @Nullable
    ServletContext getServletContext();   //可以取得Web容器的ServletContext
}
public class XmlWebApplicationContext extends AbstractRefreshableWebApplicationContext {
    //預設的Bean資訊路徑
    public static final String DEFAULT_CONFIG_LOCATION = "/WEB-INF/applicationContext.xml";
    //預設的配置檔案位裡在/WEB-INF/目錄下
    public static final String DEFAULT_CONFIG_LOCATION_PREFIX = "/WEB-INF/";
    //預設的配置檔案字尾名.xml檔案
    public static final String DEFAULT_CONFIG_LOCATION_SUFFIX = ".xml";

    public XmlWebApplicationContext() {
    }
    //refresh()時啟動
    protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
        XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);
        beanDefinitionReader.setEnvironment(this.getEnvironment());
        beanDefinitionReader.setResourceLoader(this);
        beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));
        this.initBeanDefinitionReader(beanDefinitionReader);
        this.loadBeanDefinitions(beanDefinitionReader);
    }

    protected void initBeanDefinitionReader(XmlBeanDefinitionReader beanDefinitionReader) {
    }
    //這個初始化過程是由refreshBeanFactory方法來完成的.這裡只是負責載入BeanDefinition
    protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws IOException {
        String[] configLocations = this.getConfigLocations();
        if (configLocations != null) {
            String[] var3 = configLocations;
            int var4 = configLocations.length;

            for(int var5 = 0; var5 < var4; ++var5) {
                String configLocation = var3[var5];
                reader.loadBeanDefinitions(configLocation);
            }
        }

    }

    //這裡是取得Resource位置的地方.使用了設定的預設配置位置!預設的配置位置是/WEB-INF/applicationContext.xml
    protected String[] getDefaultConfigLocations() {
        return this.getNamespace() != null ? new String[]{"/WEB-INF/" + this.getNamespace() + ".xml"} : new String[]{"/WEB-INF/applicationContext.xml"};
    }

}

從程式碼中可以看到,在XmlWebApplicationContext中,基本的上下文功能都已經通過類的繼承獲得,這裡需要處理的是,如何獲取Bean定義資訊,在這裡,就轉化為如何在Web容器環境如這裡指定的/WEB -INF/applicationContext.xml中獲得Bean定義資訊。這就解釋了為什麼我們開發的時候,web.xml檔案一般放在/WEB -INF/下。

ContextLoader的設計與實現

對於Spring承載的Web應用而言,可以指定在Web應用程式啟動時載人IoC容器(或者稱為WebAppl icationCon text)。這個功能是由ContextLoaderListener這樣的類來完成的,它是在Web容器中配置的監聽器。這個ContextLoaderListener通過使用ContextLoader來完成實際的WebApplicationContext,也就是IoC容器的初始化工作。這個ContextLoader就像Spring應用程式在Web容器中的啟動器。這個啟動過程是在Web容器中發生的,所以需要根據Web容器部署的要求來定義ContextLoader。

下面分析具體的根上下文的載入過程。在ContextLoaderListener中,實現的是ServletContextListener介面,這個接口裡的函式會結合Web容器的生命週期被呼叫。因為ServletContextListener是ServletContext的監聽者,如果ServletContext發生變化.會觸發出相應的事件,而監聽器一直在對這些事件進行監聽,如果接收到了監聽的事件,就會做出預先設計好的響應動作。

由於ServletContext的變化而觸發的監聽器的響應具體包括:在伺服器啟動時,ServletContext被建立。伺服器關閉時,ServletContext將被銷燬等。對應這些事件及Web容器狀態的變化,在監聽器中定義了對應的事件響應的回撥方法。比如在伺服器啟動時,ServletContextListener的contextlnitialized()方法被呼叫,伺服器將要關閉時,ServletContextListener的contextDestroyed()方法被呼叫。

public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
    public ContextLoaderListener() {
    }

    public ContextLoaderListener(WebApplicationContext context) {
        super(context);
    }
    //在伺服器啟動時,ServletContext被建立
    public void contextInitialized(ServletContextEvent event) {
        this.initWebApplicationContext(event.getServletContext()); //具體的初始化工作交給ContextLoader來完成
    }
    //伺服器關閉時,ServletContext將被銷燬
    public void contextDestroyed(ServletContextEvent event) {
        this.closeWebApplicationContext(event.getServletContext());
        ContextCleanupListener.cleanupAttributes(event.getServletContext());
    }
}

看看ContextLoader中的建立方法:

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
        //判斷是否已經有上下文存在,key就是之前WebApplicationContext定義的
        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) {
                    //建立根上下文
                    this.context = this.createWebApplicationContext(servletContext);
                }

                if (this.context instanceof ConfigurableWebApplicationContext) {
                    ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext)this.context;
                    if (!cwac.isActive()) {
                        if (cwac.getParent() == null) {
                            ApplicationContext parent = this.loadParentContext(servletContext);
                            //這裡載入根上下文的雙親上下文
                            cwac.setParent(parent);
                        }

                        this.configureAndRefreshWebApplicationContext(cwac, servletContext);
                    }
                }
                //將根上下文儲存到servletContext中,key是之前WebApplicationContext中定義的
                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;
            }
        }
    }

    //建立根上下文的方法
   protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
       //這裡判斷使用什麼樣的類在Web容界中作為IoC容界
        Class<?> contextClass = this.determineContextClass(sc);
        if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
            throw new ApplicationContextException("Custom context class [" + contextClass.getName() + "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
        } else {
        //直接例項化要產生的IOC容器
            return (ConfigurableWebApplicationContext)BeanUtils.instantiateClass(contextClass);
        }
    }

在初始化這個上下文以後,該上下文會被儲存到SevletContext中,這樣就建立了一個全域性的關於整個應用的上下文。同時,在啟動Spring MVC時.我們還會看到這個上下文被以後的DispatcherServlet在進行自己持有的上下文的初始化時,設定為DispatcherServlet自帶的上下文的雙親上下文。