小白新手web开发简单总结(九)-ContextLoaderListener

目录

一  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的配置类

关系图如下:

最后通过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对象了,这个需要验证下

猜你喜欢

转载自blog.csdn.net/nihaomabmt/article/details/114259276