天天看點

小白新手web開發簡單總結(九)-ContextLoaderListener一  ContextLoaderListener二 ContextLoader#initWebApplicationContext三 ContextLoader#closeWebApplicationContext四 總結

目錄

一  ContextLoaderListener

二 ContextLoader#initWebApplicationContext

1.讀取之前儲存的ApplicationContext

2.建立新的ApplicationContext

(1)determineContextClass(sc):擷取ApplicationContext對應的類

(2)(ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass)

3.配置ConfigurableWebApplicationContext屬性

4.儲存ApplicationContext

三 ContextLoader#closeWebApplicationContext

四 總結

之前在小白新手web開發簡單總結(二)-什麼是web.xml也提過在一個web應用中通常需要在web.xml中配置ContextLoaderListener,那麼ContextLoaderListener到底作用是什麼呢。

<!--用來配置Spring的配置檔案-->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:config/application-context.xml</param-value>
    </context-param>
    <!--用來建立Spring IoC容器-->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
           

我們知道在小白新手web開發簡單總結(六)-Spring的IoC容器知道 Spring就是核心功能就是IoC容器,就是為了加載和管理所有的JavaBean的生命周期,在這篇總結中也提到通過代碼怎麼執行個體化一個IoC容器,怎麼從IoC容器中取出對應的JavaBean對象,而這個ContextLoaderListener是怎麼實作這個過程呢?從源碼的角度來分析下這個過程。

一  ContextLoaderListener

本身繼承了ContextLoader,實作了ServletContextListener接口。那麼在web應用啟動的時候,會調用到對應的contextInitialized()方法來完成IoC容器的初始化;在web應用關閉的時候,來釋放資源等。

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

    public void contextDestroyed(ServletContextEvent event) {
        this.closeWebApplicationContext(event.getServletContext());
        ContextCleanupListener.cleanupAttributes(event.getServletContext());
    }
           

 單純從源碼的方法命名,就是來建立了IoC容器的WebApplicationContext對象(就是在小白新手web開發簡單總結(六)-Spring的IoC容器提到的這個WebApplicationContext)以及釋放資源等,而具體該Listener的邏輯在ContextLoader中完成。後面就詳細的看下裡面幾個重點的方法的作用。

二 ContextLoader#initWebApplicationContext

下面的initWebApplicationContext()代碼為源碼中的大部分代碼,為了友善描述整個流程,去掉了一些非重點的代碼。

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
//=== 1.讀取之前儲存的ApplicationContext
        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 {

            try {
//=== 2.否則就直接建立新的servletContext
                if (this.context == null) {
                    this.context = this.createWebApplicationContext(servletContext);
                }
//=== 3.配置ROOT application和讀取配置資訊
                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);
                    }
                }

  //=== 4.将建立的servletContext寫入到key中 
  servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
//===  5.将建立的servletContext寫入到 Map<ClassLoader, WebApplicationContext>中
                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 (Error | RuntimeException var8) {
                throw var8;
            }
        }
    }
           

從代碼中可以看出主要就是分為下面幾個步驟:

1.讀取之前儲存的ApplicationContext

代碼剛開始會先從 WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUT中讀取之前儲存的ApplicationContext,如果讀取不到才會向下執行。

這裡補充一個Tomcat和Spring幾個Context的知識點:

一般Tomcat和Spring的項目最起碼要包括三個配置檔案:web.xml、applicationcontext.xml、xxx-servlet.xml。

Tomcat啟動web應用的時候,會為每個web應用建立一個ServletContext對象,這個個ServletContext對象就可以了解為Servlet容器。那麼Tomcat首先會讀取web.xml檔案的配置内容來設定web應用的基本資訊;一般在項目中為了能夠啟動Spring中的IoC容器,通過會在web.xml中配置ContextLoaderListener,而這個ContextLoaderListener會在web應用啟動過程中建立IoC容器,那麼也就是建立一個ApplicationContext,準确的說是WebApplicationContext。該ApplicationContext通常加載的是除web層的其他後端的中間層和資料層的元件,可以讓任何web架構內建。

ContextLoaderListener建立的第一個WebApplicationContext成為ROOT ApplicationContext(讀取的是applicationcontext.xml的配置資訊,可以有contextConfigLocation來指定,如果未指定,則讀取的是/WEB-INF/applicationContext.xml),該ROOT ApplicationContext會存放到ServletContext的WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUT中,其他的Context都會作為子節點或子孫節點進行關聯。也就是一個web應用可以建立多個ApplicationContext。(對應的代碼邏輯為org.springframework.web.context.ContextLoader#createWebApplicationContext,可在下面介紹中具體看下這個邏輯)

Tomcat在生成Servlet的時候,通常還需要在web.xml中配置DispatcherServlet,那麼DispatcherServlet讀取的就是xxx-servlet.xml(xxx為該Servlet的配置的名字)檔案,而此時DispatcherServlet又會在這個過程中又初始化一個xxx相關的WebApplicationContext,也會将WebApplicationContext儲存到ServletContext中。該但是WebApplicationContext會基于上面的ROOT ApplicationContext,并設定上面的ROOT ApplicationContext為parent,并儲存到ServletContext中。該ApplicationContext建立的是web元件的bean,如控制器、視圖解析器、以及處理器映射(對應代碼邏輯為org.springframework.web.servlet.DispatcherServlet#initWebApplicationContext,這個後面在具體的去分析)(遺留問題1:DispatcherServlet)

那麼我們可以看到這三個Context之間的關系簡單描述如下:

  • (1)ROOT ApplicationContext和 xxx ApplicationContext和ServletContext相關綁定;
  • (2)ROOT ApplicationContext為xxx ApplicationContext的父節點

2.建立新的ApplicationContext

如果第一步失敗之後,則會通過createWebApplicationContext(servletContext)來建立新的ServletContext。進入到createWebApplicationContext()的源碼中看下邏輯:

protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
        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 {
            return (ConfigurableWebApplicationContext)BeanUtils.instantiateClass(contextClass);
        }
    }
           

這個裡面有兩個重要的兩行代碼:一個是 this.determineContextClass(sc);另外一個是 return (ConfigurableWebApplicationContext)BeanUtils.instantiateClass(contextClass);

(1)determineContextClass(sc):擷取ApplicationContext對應的類

該方法的作用就是從ServletContext(Servlet容器會為每一個web應用建立唯一全局的ServletContext對象)中找到ApplicationContext(IoC容器:用來執行個體化和管理所有JavaBean)。

protected Class<?> determineContextClass(ServletContext servletContext) {
//我們傳入的這個ServletContext就是Tomcat為我們web應用建立的一個Servlet容器的全局對象
//1.首先會從配置中讀取contextClass
        String contextClassName = servletContext.getInitParameter("contextClass");
        if (contextClassName != null) {
            try {
                return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
            } catch (ClassNotFoundException var4) {
                throw new ApplicationContextException("Failed to load custom context class [" + contextClassName + "]", var4);
            }
        } else {
//2.否則就直接讀取預設配置的
            contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());

            try {
                return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
            } catch (ClassNotFoundException var5) {
                throw new ApplicationContextException("Failed to load default context class [" + contextClassName + "]", var5);
            }
        }
    }
           

這個地方的邏輯很簡單:

  • 首先會讀取配置檔案中"contextClass"配置的類

也就是如果在web.xml檔案中通過下面的代碼配置了contextClass,那麼此時這個傳回的就是自定義的ApplicationContext類;

<context-param>
        <param-name>contextClass</param-name>
<!--自己定義的ApplicationContext對應的類名-->
        <param-value>com.wj.hsqldb.SpringApplicationContext</param-value>
    </context-param>
           

通常不會去配置這個類,都會下面采用預設的類。

  • 否則就讀取了ContextLoader.properties(該檔案在org\springframework\web\context\ContextLoader.properties目錄下)中配置的org.springframework.web.context.WebApplicationContext的内容(預設值為org.springframework.web.context.support.XmlWebApplicationContext)

這裡補充一個關于ApplicationContext的點:

Tomcat伺服器在啟動web應用的時候,都會為每個web應用建立一個ServletContext,是以這個ServletContext就可以認為是Servlet容器的引用,而ApplicationContext就是Spring的IoC容器,是以ApplicationContext就可以看作是一個IoC容器,每個web應用可以建立多個ApplicationContext。

常見的ApplicationContext又分為兩個子接口:ConfigurableApplicationContext(可通過配置檔案設定)和WebApplicationContext(專門用于Web開發),而ConfigurableWebApplicationContext又是繼承了WebApplicationContext子接口,即可配置的WebApplicationContext。

常見的ConfigurableApplicationContext實作類有:

  • ClassPathXmlApplicationContext
  • FileSystemXmlApplicationContext
  • AnnotationConfigApplicationContext

這幾個的使用方式可以參照小白新手web開發簡單總結(六)-Spring的IoC容器。

常見的ConfigurableWebApplicationContext實作類有:

  • XmlWebApplicationContext(該類預設讀取的是/WEB-INF/applicationContext.xml下配置内容,當然也可以通過contextConfigLocation來指定配置檔案
  • AnnotationConfigWebApplicationContext:讀取的是@Configuration的配置類
關系圖如下:
小白新手web開發簡單總結(九)-ContextLoaderListener一  ContextLoaderListener二 ContextLoader#initWebApplicationContext三 ContextLoader#closeWebApplicationContext四 總結

最後通過determineContextClass(sc)擷取到ApplicationContext的類名為XmlWebApplicationContext,即一個可以通過配置檔案來配置的WebApplicationContext。

(2)(ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass)

這段代碼最後很明顯,就是最後将建立的WebApplicationContext對象強轉成了ConfigurableWebApplicationContext對象,從我們總結的ApplicationContext幾個接口之間的關系,那麼我們也就是說我們在去通過"contextClass"來配置一個ApplicationContext的時候,必須要是一個ConfigurableWebApplicationContext的實作類。

3.配置ConfigurableWebApplicationContext屬性

由于第二步已經将WebApplicationContext強轉為ConfigurableWebApplicationContext,是以這部分的代碼一定都會執行。

代碼的邏輯基本上就是就要設定ConfigurableWebApplicationContext的各個屬性,包括讀取contextConfigLocation配置的資訊來進行設定屬性。裡面比較關鍵的方法configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc)。不在貼出代碼,可根據源碼進行檢視。

4.儲存ApplicationContext

從代碼中可以看出,就是将最後建立的WebApplicationContext儲存到第一步提到的WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUT這個key中以及本身儲存的WebApplicationContext的currentContextPerThread這個map中。

通過上面四步,已經完成了WebApplicationContext的建立過程,那麼我們就可以使用Spring 的IoC容器來管理JavaBean。在小白新手web開發簡單總結(六)-Spring的IoC容器提到的注解的方式來管理JavaBean應該是不在使用,隻能通過配置檔案的方式來。

(遺留問題2:而之前在公司項目中看到的那些@Component的方式又是怎麼實作的呢??)

解答:是可以通過注解的方式來加載JavaBean的。需要有以下操作:

在contextConfigLocation對應的配置檔案中添加

<beans> 
    <context:annotation-config/>
    <context:component-scan base-package="com.wj"/>
</beans> 
           
注意這個context:component-scan base-packag就是要掃描注解的包,這裡一定不能用“*”,否則會抛出以下異常:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.context.annotation.internalAsyncAnnotationProcessor' defined in org.springframework.scheduling.annotation.ProxyAsyncConfiguration: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.scheduling.annotation.AsyncAnnotationBeanPostProcessor]: Factory method 'asyncAdvisor' threw exception; nested exception is java.lang.IllegalArgumentException: @EnableAsync annotation metadata was not injected
		at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:656)
		at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:484)
		at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1338)
		at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1177)
		at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:557)
           

一定要使用對應所需要掃描的路徑,這樣就可以使用Spring的注解方式來加載JavaBean了(可參照小白新手web開發簡單總結(六)-Spring的IoC容器)。

但是當在項目中添加了HttpServlet類的時候,這裡又會引入另外一個問題:你會發現在HttpServlet的子類中使用@Autowired來引入JavaBean執行個體的時候,會抛出該JavaBean執行個體NullPointException,這個原因是因為HttpServlet是在Servlet容器,而這些執行個體化的JavaBean對象是在Spring的IoC容器,這些JavaBean無法在Servlet容器得到,但是spring-web也提供了一種解決方案,那就是複寫HttpServlet的init(config)方法中添加如下代碼:

@Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        ServletContext application = this.getServletContext();     
SpringBeanAutowiringSupport.processInjectionBasedOnServletContext(this,application);
    }
           
具體的一些代碼實作參見小白新手web開發簡單總結(十)-資料庫HSQLDB執行個體問題總結

綜上,準确的說ContextLoaderListener是在web應用啟動的時候,讀取contextConfigLocation中指定的xml檔案,自動裝配ApplicationContext的配置資訊,并建立WebApplicationContext對象,即IoC容器,因為Servlet是在Servlet容器(Tomcat),而Spring執行個體化的JavaBean在IoC容器,為了使在Servlet容器中可以使用這些JavaBean,是以還将建立的WebApplicationContext對象和ServletContext做了綁定,那麼就可以通過Servlet來通路到WebApplicationContext對象,并利用這個對象來通路到JavaBean。

 (遺留問題3:這裡需要驗證下我的這個結論是不是正确!!!!)

是可以直接擷取到WebApplicationContext來通路這些JavaBean,但是并不需要這麼麻煩。可以通過注解的方式添加這些JavaBean的依賴之後,隻需要複寫HttpServlet的init(config)方法,添加SpringBeanAutowiringSupport.processInjectionBasedOnServletContext(),即可通過注解來通路對應的JavaBean,具體可參見小白新手web開發簡單總結(十)-資料庫HSQLDB執行個體問題總結

三 ContextLoader#closeWebApplicationContext

代碼邏輯應該比較清晰,就是釋放資源:将ServletContext和本地Map中裡面儲存的内容清空。

public void closeWebApplicationContext(ServletContext servletContext) {
        servletContext.log("Closing Spring root WebApplicationContext");
        boolean var6 = false;

        try {
            var6 = true;
            if (this.context instanceof ConfigurableWebApplicationContext) {
                ((ConfigurableWebApplicationContext)this.context).close();
                var6 = false;
            } else {
                var6 = false;
            }
        } finally {
            if (var6) {
                ClassLoader ccl = Thread.currentThread().getContextClassLoader();
                if (ccl == ContextLoader.class.getClassLoader()) {
                    currentContext = null;
                } else if (ccl != null) {
                    currentContextPerThread.remove(ccl);
                }
  //清空Servlet裡面的内容          
servletContext.removeAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
            }
        }

        ClassLoader ccl = Thread.currentThread().getContextClassLoader();
        if (ccl == ContextLoader.class.getClassLoader()) {
            currentContext = null;
        } else if (ccl != null) {
 //清空Map裡面的内容  
            currentContextPerThread.remove(ccl);
        }
  //清空Servlet裡面的内容  servletContext.removeAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
    }
           

四 總結

通過對ContextLoaderListener的源碼解讀,自己終于明白了之前在總結小白新手web開發簡單總結(二)-什麼是web.xml的時候,為什麼要在web.xml中配置ContextLoaderListener,并且在小白新手web開發簡單總結(十)-資料庫HSQLDB執行個體問題總結中遇到的使用注解方式在HttpServlet中執行個體化的bookManagerService為空指針的原因。

1.ContextLoaderListener在web應用啟動的時候,會建立Root WebApplicationContext;

2.一個WebApplicationContext就是一個Spring的IoC容器,用來管理JavaBean;

3.ContextLoaderListener會首先讀取之前儲存在ServletContext中的WebApplicationContext,如果沒有的話,則重新建立;

4.ContextLoaderListener在建立一個WebApplicationContext的時候,會根據web.xml中是否配置"contextClass"來設定傳回的WebApplicationContext;如果配置,則傳回配置的類;如果沒有配置,則讀取預設的XmlWebApplicationContext;

5.ContextLoaderListener建立的WebApplicationContext都會轉換成ConfigurableWebApplicationContext對象,是以如果要通過web.xml配置"contextClass",則該類必須是ConfigurableWebApplicationContext的實作類;

6.一個ApplicationContext接口分為ConfigurableApplicationContext和WebApplicationContext,而WebApplicationContext為web應用專用的,并且為了可配置,特意又産生了一個ConfigurableWebApplicationContext的子接口,用來可配置的web應用的WebApplicationContext;

7.當産生WebApplicationContext對象的時候,再就是去配置裡面的各個屬性,即讀取web.xml裡面的“contextConfigLocation”配置的檔案進行配置屬性;

8.會WebApplicationContext儲存到将ServletContext,是以就可以在ServletContext中使用WebApplicationContext對象;

9.ContextLoaderListener在web應用關閉的時候,會将儲存在ServletContext裡面的内容清空,并且釋放本地Map裡面的内容。

當然也有幾個遺留問題:

1.DispatcherServlet源碼的分析;

2.為什麼在Spring MVC中可以使用@Controller等這些對象來标記一個JavaBean,因為我現在了解的傳回的是ApplicationContext是一個XmlWebApplicationContext,隻能通過配置檔案來管理JavaBean;

3.由于ContextLoaderListener的加載,那就可以在Servlet中使用JavaBean對象了,這個需要驗證下