11.0、Spring源码学习 ——SpringMVC 的 ContextLoaderListener

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/bestcxx/article/details/99402140

前言

体能状态先于精神状态,习惯先于决心,聚焦先于喜好。

SpringMVC 和 web.xml

SpringMVC 的入口在 web.xml
在该文件中我们可以进行多项配置,比如全局变量、过滤器、 < listener> 和 Spring 的 DispatcherServlet 等等。

ServletContextListener

该接口的完整路径为 javax.servlet.ServletContextListener
从源码注释中我们可以知道:
1、一个类实现一个ServletContextListener 借口,这种实现可以有多个;
2、该接口有两个方法 contextInitialized 和 contextDestroyed,二者都可以直接访问 ServletContext,ServletContext 是整个web 项目启动阶段的核心所在
3、contextInitialized 会在 ServletContext 初始化阶段运行,且早于 filters 或 servlets 的配置运行,期间我们完全可以做一些前期准备工作,比如配置文件加载、健康检查、数据库密码解密等等
4、contextDestroyed 会在 ServletContext 停止阶段运行,且晚于 filters 或 servlets 的配置运行,其运行对一些资源进行销毁。

public interface ServletContextListener extends EventListener {

    /**
     * 在 web 应用初始化阶段会调用本方法;
     * 可以使用多个类实现该接口,即你可以在一个应用中拥有多个 ServletContextListener; 
     * ServletContextListener的contextInitialized方法会在 web.xml 中所有的 filters 或 servlets 配置前运行
     * 参数 sce the ServletContextEvent 包含正在初始化的 ServletContext,即你可以在该方法的实现中直接操作 ServletContext
     */
    public void contextInitialized(ServletContextEvent sce);

    /**
     * 当 ServletContext  将要停止时被调用
     *
     * 在web.xml所有的 servlets 和 filters 配置执行完毕并销毁后 contextDestroyed 方法才会被执行
     * 参数 sce 可以访问正在被销毁的 ServletContext 对象,即你可以在方法实现中操作 ServletContext 对象
     */
    public void contextDestroyed(ServletContextEvent sce);
}

自己实现一个 ServletContextListener

package com.bestcxx.cn.webrecord.listener;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

/**
 * 自定义一个 ServletContextListener
 * @Author jie.wu
 */
public class MyServletContextListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println(this.getClass().getName()+"输出了");
        System.out.println(this.getClass().getName()+"输出了");
        System.out.println(this.getClass().getName()+"输出了");
        System.out.println(this.getClass().getName()+"输出了");
        System.out.println(this.getClass().getName()+"输出了");

        //定义一般参数
        sce.getServletContext().setAttribute("name","jecket");
        //定义初始化类型参数,比如数据库连接的用户名密码解密等
        sce.getServletContext().setInitParameter("demo","demo");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        //todo
    }
}

在 web.xml 中使用 < listener> 配置 ServletContextListener

实现了 ServletContextListener 的类后还需增加配置才能生效
通过 web.xml < listener> 标签指定相关类,可以有多个< listener>标签并列存在:

<listener>
    <listener-class>com.bestcxx.cn.webrecord.listener.MyServletContextListener</listener-class>
</listener>

其中 < listener-class> 标签包围着一个类,该类实现了 ServletContextListener 接口。
这个类你也可以自己写,并且,一个web.xml 文件可以包含多个 < listener> 代码组合

SpringMVC 对 ServletContextListener 的实现 ContextLoaderListener

SpringlMVC 对 ServletContextListener 的实现是 ContextLoaderListener,其包含了更多的SpringMVC 自身的特性。
其最主要的功能就是加载 SpringMVC 相关的配置文件,进行初始化工作,核心逻辑是初始化 WebApplicationContext 实例并存放至 ServletContext 中。

<listener>
	<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

ContextLoaderListener 的 UML

在这里插入图片描述

org.springframework.web.context.ContextLoaderListener 处理逻辑 UML

在这里插入图片描述

ContextLoader 决定 WebApplicationContext 的具体实现类

如果你不指定,就会采用 Spring 自带的配置文件的类 XmlWebApplicationContext 进行实例化,除非你自己实现了个性的实例化类。
实例化后的 WebApplicationContext 会被放入到上下文中供应用使用。

扫描二维码关注公众号,回复: 7196413 查看本文章
仅允许加载一次 WebApplicationContext 对象

当加载完毕后被实例化的 WebApplicationContext 对象会被保存到 上下文中

//值为 org.springframework.web.context.WebApplicationContext.ROOT
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE= 
WebApplicationContext.class.getName() + ".ROOT" 
//···中间代码略
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

加载仅允许加载一次,即你只能在 web.xml 中配置一次 ContextLoaderListener

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!");
		}
默认加载配置文件 ContextLoader.properties

注意UML中的 ContextLoader,该类的静态方法加载了一个默认的配置文件 ContextLoader.properties

在这里插入图片描述

static {
		// Load default strategy implementations from properties file.
		// This is currently strictly internal and not meant to be customized
		// by application developers.
		try {
			ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, ContextLoader.class);
			defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
		}
		catch (IOException ex) {
			throw new IllegalStateException("Could not load 'ContextLoader.properties': " + ex.getMessage());
		}
	}

ContextLoader.properties 文件内容只有一行,即默认情况下 WebApplicationContext 的实现类为 XmlWebApplicationContext

org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext
你可以自定义 WebApplicationContext 的实现类

对于 WebApplicationContext 的具体实现类,你可以通过自定义的ServletContextListener 或者全局变量提前将变量放入上下文中,名称为 contextClass,
它就会使用你提供的类为 WebApplicationContext 提供一个实例化对象。
当然,你没必要自己写,如果仅仅作为实验的话——看看报错信息也可以,如果不想看报错信息就直接将value 写成 org.springframework.web.context.support.XmlWebApplicationContext

  • 自定义 ServletContextListener

注意是 setInitParameter

@Override
public void contextInitialized(ServletContextEvent sce) {
	//定义一般参数
	sce.getServletContext().setAttribute("name","jecket");
	//定义初始化类型参数-指定WebApplicationContext 的具体实例化类
	//org.springframework.web.context.support.XmlWebApplicationContext 是默认的类,在 ContextLoader.properties 中有指定
	sce.getServletContext().setInitParameter("contextClass","com.bestcxx.cn.webrecord.listener.MyWebApplicationContext");
 }
  • 通过全局变量 在web.xml 中
 <context-param>
        <param-name>contextClass</param-name>
        <param-value>com.bestcxx.cn.webrecord.listener.MyWebApplicationContext</param-value>
    </context-param>
  • Spring 源码通过反射机制为 WebApplicationContext 生成实例化对象

如果你指定了具体实例化类,就用你指定的,否则就用默认的

public static final String CONTEXT_CLASS_PARAM = "contextClass";
//···中间代码略
protected Class<?> determineContextClass(ServletContext servletContext) {
		String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
		if (contextClassName != null) {
			try {
				return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
			}
			catch (ClassNotFoundException ex) {
				throw new ApplicationContextException(
						"Failed to load custom context class [" + contextClassName + "]", ex);
			}
		}
		else {
			contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
			try {
				return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
			}
			catch (ClassNotFoundException ex) {
				throw new ApplicationContextException(
						"Failed to load default context class [" + contextClassName + "]", ex);
			}
		}
	}

ContextLoaderListener 默认加载配置文件路径:/WEB-INF/applicationContext.xml

默认情况下,WebApplicationContext 由 XmlWebApplicationContext 实例化后,就会加载配置文件
,XmlWebApplicationContext 默认配置文件路径为 /WEB-INF/applicationContext.xml 文件,路径和名字都必须一模一样。
applicationContext.xml 内应该包含的内容是 Spring 容器基础能力相关的内容-比如数据库、业务、事务等功能bean,而不应该包含 controller 和页面的内容.

public class XmlWebApplicationContext extends AbstractRefreshableWebApplicationContext {

	/** Default config location for the root context */
	public static final String DEFAULT_CONFIG_LOCATION = "/WEB-INF/applicationContext.xml";

自定义 配置文件路径:web.xml增加一个全局参数配置 context-param:contextConfigLocation

表达式支持1,2 挥着 * 通配符,即可以配置多个文件

<!--自定义SpringMVC 配置文件的位置
 1、contextConfigLocation 名字不可变
    2、可以使用通配符加载多个文件,如 classpath:*-applicationContext.xml-->
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:applicationContext.xml</param-value>
</context-param>
<listener>
	<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

ContextLoaderListener 的配置可以省略,但不建议这么做

如果你的 web.xml 没有配置 ContextLoaderListener 的 < listener>,你会发现你的 SpringMVC 项目应用并不受什么影响。因为 SpringMVC 的 DispatcherServlet 也可以指定加载多个配置文件,但是不建议这么做,因为正常来说 ContextLoaderListener 负责加载基础功能的bean,会提前准备好,DispatcherServlet 加载 servlet相关的Bean,比如Controller 拦截器、视图处理器等.
关于 DispatcherServlet 的配置请移步:SpringMVC 的 DispatcherServet

源码跟踪

入口 ContextLoaderListener.contextInitialized(

web应用启动阶段会自动运行 web.xml 中配置的 < listener>
org.springframework.web.context.ContextLoaderListener.contextInitialized
注意 initWebApplicationContext(event.getServletContext());

/**
 * 初始化web应用的根上下文
 */
@Override
public void contextInitialized(ServletContextEvent event) {
	//初始化 webApplicationContext
	initWebApplicationContext(event.getServletContext());
}

ContextLoader.initWebApplicationContext(

org.springframework.web.context.ContextLoader.initWebApplicationContext(
为给定的 servlet 上下文对象 初始化 Spring webAppictionContext对象,该webAppictionContext 可以在构造函数中提供,也可以创建一个新的对象,依据位于
CONTEXT_CLASS_PARAM 和 contextConfigLocation 这两个全局配置。
注意 createWebApplicationContext(servletContext); 这一句
注意 configureAndRefreshWebApplicationContext(cwac, servletContext);这一句

  • 实例化 WebApplicationContext 对象,默认是 XmlWebApplicationContext
  • XmlWebApplicationContext 间接实现了 ConfigurableWebApplicationContext ,如果该对象尚未 refreshed,则判断是否设置了 根web上下文对象,如果webApplicationContext 对象没有明确指定 父上下文对象,则确定一个根web上下文对象——只要符合标准即可
/**
	 * @param servletContext current servlet context
	 * @return the new WebApplicationContext
	 * @see #ContextLoader(WebApplicationContext)
	 * @see #CONTEXT_CLASS_PARAM
	 * @see #CONFIG_LOCATION_PARAM
	 */
	public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
		//以 org.springframework.web.context.WebApplicationContext.ROOT 作为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!");
		}

		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 = createWebApplicationContext(servletContext);
			}
			if (this.context instanceof ConfigurableWebApplicationContext) {
				ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
				if (!cwac.isActive()) {
					// 如果 appliactionContext 对象还没有被 refreshed (refreshed 会提供父上下文对象,设置上下文id等配置),该值在  AbstractApplicationContext.refresh()的prepareRefresh()中修改为true
					if (cwac.getParent() == null) {
						//webApplicationContext 对象没有明确指定 父上下文对象,则确定一个根web上下文对象——只要符合标准即可
						ApplicationContext parent = loadParentContext(servletContext);
						//debug 发现,默认情况下这个值为null
						cwac.setParent(parent);
					}
					//进行 webApplicationContext 的 配置和refresh 工作
					configureAndRefreshWebApplicationContext(cwac, servletContext);
				}
			}
	//初始化完毕后,放入到 servletContext 对象中,之后的Servlet请求就可以使用了
	// key 为 org.springframework.web.context.WebApplicationContext.ROOT
	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;
		}
	}

ContextLoader.createWebApplicationContext(

org.springframework.web.context.ContextLoader.createWebApplicationContext(
为这个加载器初始化 根 WebApplicationContext 对象,也可以是 默认的或者指定的上下文类 。
指定的类被期望为 ConfigurableWebApplicationContext 的子类,并且该方法不可被覆盖重写。
此外,在 refreshing the context 这个webApplicaionContext 对象之前,允许子类提前调用 customizeContext()方法,增加一些定制化的定义。

  • 该方法内部将进行判断,如果你没有指定特殊的 webApplicationContext 的实例化类,则将使用Spring 默认的实例化类 org.springframework.web.context.support.XmlWebApplicationContext 来进行实例化,方式为 ClassUtils.forName
  • 需要注意的是,实例化的类必须(间接)实现了 ConfigurableWebApplicationContext 的接口,默认的XmlWebApplicationContext 这个类是没有问题的,如果自己指定的话千万注意

在这里插入图片描述

/**
	 * @param sc current servlet context
	 * @return the root WebApplicationContext
	 * @see ConfigurableWebApplicationContext
	 */
	protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
	//该方法内部将进行判断,如果你没有指定特殊的 webApplicationContext 的实例化类,则将使用Spring 默认的实例化类 org.springframework.web.context.support.XmlWebApplicationContext 来进行实例化,方式为 ClassUtils.forName 
		Class<?> contextClass = determineContextClass(sc);
		//需要注意的是,实例化的类必须是 ConfigurableWebApplicationContext 的子类
		if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
			throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
					"] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
		}
		return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
	}

ContextLoader.configureAndRefreshWebApplicationContext(

org.springframework.web.context.ContextLoader.configureAndRefreshWebApplicationContext(
对于 新创建的 webApplicationContext 对象进行配置和刷新操纵-设置 context的id、初始化 servletContext的环境变量、refresh 等等。

  • initPropertySources(
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
		if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
		//为 servletContext 设置一个更有用的名字——而不是默认的名字,我理解的就是名字更通俗易懂
		    //从统一配置获取-如果你没有配置,这个就是null了
			String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
			if (idParam != null) {
				wac.setId(idParam);
			}
			else {
				// 默认代码会走这里,最终 servletContext 名字和 applicationContext 对象有一定关联
				wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
						ObjectUtils.getDisplayString(sc.getContextPath()));
			}
		}
        //为webApplicationContext 对象 设置 servletContext 对象
		wac.setServletContext(sc);
		//从统一配置获取待加载的配置文件路径,比如本文配置为 classpath:applicationContext.xml,如果没有配置,默认就是  WEB-INF/applicationContext.xml
		String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
		if (configLocationParam != null) {
			wac.setConfigLocation(configLocationParam);
		}
		
		// 当上下文环境刷新时,webApplicationContext environment 的初始化 配置资源会被请求;这里使用卫语句判断是为了确保任何 servlet 配置资源在 refresh 之前的操作生效
		ConfigurableEnvironment env = wac.getEnvironment();
		if (env instanceof ConfigurableWebEnvironment) {
			//提前加载一切资源,比如操作系统信息,JVM信息等等
			//为 sevletContext 初始化环境变量 servletConfigInitParams,servletContextInitParams,jndiProperties,systemProperties,systemEnvironment
			((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
		}
		//初始化所有的上下文对象,debug发现,默认的上下文对象数量为0,即该方法代码默认情况下没有起作用
		customizeContext(sc, wac);
		//最重要的一个方法,刷新上下文,这里是方法模板,由子类具体实现
		wac.refresh();
	}

StandardServletEnvironment.initPropertySources(

org.springframework.web.context.support.StandardServletEnvironment.initPropertySources(
从 getPropertySources() 可以粗略的看出加载的配置信息
[servletConfigInitParams,servletContextInitParams,jndiProperties,systemProperties,systemEnvironment]

@Override
	public void initPropertySources(ServletContext servletContext, ServletConfig servletConfig) {
		WebApplicationContextUtils.initServletPropertySources(getPropertySources(), servletContext, servletConfig);
	}
servletConfigInitParams

在这里插入图片描述

servletContextInitParams

在这里插入图片描述

jndiProperties

在这里插入图片描述

systemProperties

在这里插入图片描述

systemEnvironment

在这里插入图片描述

ContextLoader.customizeContext(

org.springframework.web.context.ContextLoader.customizeContext(
在 上下文 refrash 之前,在上下文定义了本地配置信息后,自定义 被这个 ContextLoader 创建的ConfigurableWebApplicationContext对象(webApplicationContext 对象)
上下文初始化类可以通过全局配置参数 CONTEXT_INITIALIZER_CLASSES_PARAM 指定,默认通过这个方法获得 determineContextInitializerClasses(ServletContext),并且通过 ApplicationContextInitializer 初始化每一个被提供的 web 应用上下文。
任何 ApplicationContextInitializers 实现了 org.springframework.core.Ordered Ordered 或者被org.springframework.core.annotation.Order Order 注解标注的类会被适当的排序。

/**
	 * @param sc the current servlet context
	 * @param wac the newly created application context
	 * @see #CONTEXT_INITIALIZER_CLASSES_PARAM
	 * @see ApplicationContextInitializer#initialize(ConfigurableApplicationContext)
	 */
	protected void customizeContext(ServletContext sc, ConfigurableWebApplicationContext wac) {
//获取上下文初始化类,,debug发现默认为0
List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>> initializerClasses =
				determineContextInitializerClasses(sc);

		for (Class<ApplicationContextInitializer<ConfigurableApplicationContext>> initializerClass : initializerClasses) {
		
			Class<?> initializerContextClass =
					GenericTypeResolver.resolveTypeArgument(initializerClass, ApplicationContextInitializer.class);
			if (initializerContextClass != null && !initializerContextClass.isInstance(wac)) {
				throw new ApplicationContextException(String.format(
						"Could not apply context initializer [%s] since its generic parameter [%s] " +
						"is not assignable from the type of application context used by this " +
						"context loader: [%s]", initializerClass.getName(), initializerContextClass.getName(),
						wac.getClass().getName()));
			}
			this.contextInitializers.add(BeanUtils.instantiateClass(initializerClass));
		}

		AnnotationAwareOrderComparator.sort(this.contextInitializers);
		for (ApplicationContextInitializer<ConfigurableApplicationContext> initializer : this.contextInitializers) {
			initializer.initialize(wac);
		}
	}

AbstractApplicationContext.refresh(

org.springframework.context.support.AbstractApplicationContext.refresh
这个方法太重要了,单独写一篇文章介绍。
认识 AbstractApplicationContext.refresh()

猜你喜欢

转载自blog.csdn.net/bestcxx/article/details/99402140