ContextLoaderListener的作用就是启动Web容器时,自动装配ApplicationContext的配置信息。因为它实现了ServletContextListener接口,在web.xml配置这个监听器,启动容器时就会默认执行它实现的方法,使用ServletContextListener接口,开发者能够在为客户端请求提供服务之前向ServletContext中添加任意的对象。这个对象在ServletContext启动的时候被初始化,然后在ServletContext整个运行期间都是可见的。
每个web应用都有一个ServletContext与之关联。ServletContext对象在应用启动时被创建,在应用关闭时被销毁。ServletContext在全局范围内有效,类似于应用中的一个全局变量。在ServletContextListener中的核心逻辑便是初始化WebApplicationContext实例,并存放至ServletContext中。
ContextLoaderListener源码如下:
/**
* Bootstrap listener to start up and shut down Spring's root {@link WebApplicationContext}.
* Simply delegates to {@link ContextLoader} as well as to {@link ContextCleanupListener}.
*
* {@code ContextLoaderListener} supports injecting the root web application
* context via the {@link #ContextLoaderListener(WebApplicationContext)}
* constructor, allowing for programmatic configuration in Servlet initializers.
* See {@link org.springframework.web.WebApplicationInitializer} for usage examples.
*
* @author Juergen Hoeller
* @author Chris Beams
* @since 17.02.2003
* @see #setContextInitializers
* @see org.springframework.web.WebApplicationInitializer
*/
public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
/**
* Create a new {@code ContextLoaderListener} that will create a web application
* context based on the "contextClass" and "contextConfigLocation" servlet
* context-params. See {@link ContextLoader} superclass documentation for details on
* default values for each.
*
This constructor is typically used when declaring {@code ContextLoaderListener}
* as a {@code } within {@code web.xml}, where a no-arg constructor is
* required.
* The created application context will be registered into the ServletContext under
* the attribute name {@link WebApplicationContext#ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE}
* and the Spring application context will be closed when the {@link #contextDestroyed}
* lifecycle method is invoked on this listener.
* @see ContextLoader
* @see #ContextLoaderListener(WebApplicationContext)
* @see #contextInitialized(ServletContextEvent)
* @see #contextDestroyed(ServletContextEvent)
*/
public ContextLoaderListener() {
}
/**
* Create a new {@code ContextLoaderListener} with the given application context. This
* constructor is useful in Servlet initializers where instance-based registration of
* listeners is possible through the {@link jakarta.servlet.ServletContext#addListener} API.
*
The context may or may not yet be {@linkplain
* org.springframework.context.ConfigurableApplicationContext#refresh() refreshed}. If it
* (a) is an implementation of {@link ConfigurableWebApplicationContext} and
* (b) has not already been refreshed (the recommended approach),
* then the following will occur:
*
* - If the given context has not already been assigned an {@linkplain
* org.springframework.context.ConfigurableApplicationContext#setId id}, one will be assigned to it
* - {@code ServletContext} and {@code ServletConfig} objects will be delegated to
* the application context
* - {@link #customizeContext} will be called
* - Any {@link org.springframework.context.ApplicationContextInitializer ApplicationContextInitializer org.springframework.context.ApplicationContextInitializer ApplicationContextInitializers}
* specified through the "contextInitializerClasses" init-param will be applied.
* - {@link org.springframework.context.ConfigurableApplicationContext#refresh refresh()} will be called
*
* If the context has already been refreshed or does not implement
* {@code ConfigurableWebApplicationContext}, none of the above will occur under the
* assumption that the user has performed these actions (or not) per his or her
* specific needs.
* See {@link org.springframework.web.WebApplicationInitializer} for usage examples.
*
In any case, the given application context will be registered into the
* ServletContext under the attribute name {@link
* WebApplicationContext#ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE} and the Spring
* application context will be closed when the {@link #contextDestroyed} lifecycle
* method is invoked on this listener.
* @param context the application context to manage
* @see #contextInitialized(ServletContextEvent)
* @see #contextDestroyed(ServletContextEvent)
*/
public ContextLoaderListener(WebApplicationContext context) {
super(context);
}
/**
* Initialize the root web application context.
*/
@Override
public void contextInitialized(ServletContextEvent event) {
initWebApplicationContext(event.getServletContext());
}
/**
* Close the root web application context.
*/
@Override
public void contextDestroyed(ServletContextEvent event) {
closeWebApplicationContext(event.getServletContext());
ContextCleanupListener.cleanupAttributes(event.getServletContext());
}
}
这段代码是一个Java类,名为ContextLoaderListener。它继承自ContextLoader并实现了ServletContextListener接口。ContextLoaderListener用于在Web应用中初始化和销毁Spring的根WebApplicationContext。通过setContextInitializers方法可以注入根Web应用上下文。它支持通过无参构造函数在Servlet初始化器中进行程序化配置。在contextInitialized方法中初始化根Web应用上下文,在contextDestroyed方法中关闭根Web应用上下文,并通过ContextCleanupListener清理Servlet上下文中的属性。
首先我们创建ServletContextListener,目标是在系统启动时添加自定义的属性,以便于在全局范围内可以随时调用。系统启动的时候会调用ServletContextListener实现类的contextInitialized方法,所以需要在这个方法中实现我们初始化的逻辑。
public class MyDataContextListener implements ServletContextListener {
private ServletContext context;
@Override
public void contextInitialized(ServletContextEvent event) {
context = event.getServletContext();
context.setAttribute("myData", "1234");
}
@Override
public void contextDestroyed(ServletContextEvent event) {
context = null;
}
}
一旦Web应用启动的时候,我们就能在任意的Servlet或者JSP中通过下面的方式获取我们初始化的参数:
String myDate = (String)getServletContext().getAttribute("myDate");
ServletContext启动之后会调用ServletContextListener的contextInitialized方法,首先从这个方法开始进行分析:
/**
* Initialize the root web application context.
*/
@Override
public void contextInitialized(ServletContextEvent event) {
initWebApplicationContext(event.getServletContext());
}
这里涉及到了一个常用类WebApplicationContext:在Web应用中,我们会用到WebApplicationContext,WebApplicationContext继承自ApplicationContext,在WebApplicationContext的基础上下文增加了一些特定于Web的操作及属性,非常类似于我们通过编程方式使用Spring时使用的ClassPathXmlApplicationContext类提供的功能。initWebApplicationContext源码如下:
/**
* 使用给定的ServletContext初始化Spring的Web应用程序上下文,使用提供的构造函数中的ApplicationContext,或者根据"{@link #CONTEXT_CLASS_PARAM contextClass}"
* 和"{@link #CONFIG_LOCATION_PARAM contextConfigLocation}"上下文参数创建一个新的ApplicationContext。
* @param servletContext 当前的servlet上下文
* @return 新的WebApplicationContext
* @see #ContextLoader(WebApplicationContext)
* @see #CONTEXT_CLASS_PARAM
* @see #CONFIG_LOCATION_PARAM
*/
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
throw new IllegalStateException(
"无法初始化上下文,因为已经存在根应用程序上下文 - 检查您的web.xml中是否有多个ContextLoader*定义!");
}
servletContext.log("正在初始化Spring根WebApplicationContext");
Log logger = LogFactory.getLog(ContextLoader.class);
if (logger.isInfoEnabled()) {
logger.info("根WebApplicationContext:初始化开始");
}
long startTime = System.currentTimeMillis();
try {
// 将上下文存储在本地实例变量中,以保证在Servlet上下文关闭时仍可访问。
if (this.context == null) {
this.context = createWebApplicationContext(servletContext);
}
if (this.context instanceof ConfigurableWebApplicationContext cwac && !cwac.isActive()) {
// 上下文尚未刷新 -> 提供服务,例如设置父上下文、设置ApplicationContext ID等
if (cwac.getParent() == null) {
// 未显式设置父上下文 -> 如果存在,则为根Web应用程序上下文设置父上下文
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
}
configureAndRefreshWebApplicationContext(cwac, 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.isInfoEnabled()) {
long elapsedTime = System.currentTimeMillis() - startTime;
logger.info("根WebApplicationContext在" + elapsedTime + " ms内初始化完成");
}
return this.context;
}
catch (RuntimeException | Error ex) {
logger.error("上下文初始化失败", ex);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
throw ex;
}
}
这个函数用于初始化Spring的Web应用上下文,根据给定的servlet上下文、上下文类参数和配置位置参数创建一个新的Web应用上下文,并将其设置为根Web应用上下文。如果已经存在一个根应用上下文,则抛出异常。最后,将上下文存储在servlet上下文中,并返回创建的Web应用上下文。
在配置中只允许声明一次ServletContextListener,多次声明会扰乱Spring的执行逻辑,所以这里首先做的就是对此进行验证,在Spring中如果创建WebApplicationContext实例会记录在ServletContext中以方便全局调用,而使用的key就是WebApplicationContext的ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,所以验证的方式就是查看ServletContext实例中是否有对应的Key的属性。
如果通过验证,则Spring将创建WebApplicationContext实例的工作委托给了createWebApplicationContext函数,源码如下:
/**
* 创建用于该加载器的根WebApplicationContext,即默认的上下文类或指定的自定义上下文类。
* 该实现期望自定义上下文实现ConfigurableWebApplicationContext
接口。
* 可在子类中重写。
*
在刷新上下文之前,会调用customizeContext
方法对上下文进行自定义修改。
* @param sc 当前的servlet上下文
* @return 根WebApplicationContext
* @see ConfigurableWebApplicationContext
*/
protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
Class> contextClass = determineContextClass(sc);
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException("自定义上下文类[" + contextClass.getName() +
"]不是类型[" + ConfigurableWebApplicationContext.class.getName() + "]");
}
return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}
这个Java函数用于创建Web应用程序的根WebApplicationContext。它要么使用默认的上下文类,要么如果指定的话,使用自定义的上下文类。该函数会检查自定义上下文类是否实现了ConfigurableWebApplicationContext接口,并使用BeanUtils类的instantiateClass方法来实例化上下文。在实例化之前,它还会调用customizeContext函数对上下文进行自定义修改。determineContextClass用于获取WebApplicationContext类信息,源码如下:
/**
* 返回要使用的WebApplicationContext实现类,要么是默认的XmlWebApplicationContext,
* 要么是指定的自定义上下文类。
* @param servletContext 当前的servlet上下文
* @return 要使用的WebApplicationContext实现类
* @see #CONTEXT_CLASS_PARAM
* @see org.springframework.web.context.support.XmlWebApplicationContext
*/
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 {
if (defaultStrategies == null) {
// 从属性文件中加载默认策略实现。
// 这是目前严格内部的,不打算供应用程序开发人员定制。
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());
}
}
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);
}
}
}
这个Java函数根据当前的servlet上下文来确定要使用的WebApplicationContext实现类,可以是默认的XmlWebApplicationContext或自定义的上下文类。如果在servlet上下文中指定了上下文类名,则返回指定的上下文类;否则返回默认的上下文类。
函数configureAndRefreshWebApplicationContext用于配置和刷新WebApplicationContext,源码如下:
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
// 如果applicationContext的identityString仍然设置为其默认值
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
// 将id参数设置为更有用的值
String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
if (idParam != null) {
wac.setId(idParam);
} else {
// 生成默认的id
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
ObjectUtils.getDisplayString(sc.getContextPath()));
}
}
wac.setServletContext(sc);
String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
if (configLocationParam != null) {
wac.setConfigLocation(configLocationParam);
}
// 在上下文刷新时,wac环境的#initPropertySources方法将被调用;在这里提前执行它以确保在下面进行的任何后处理或初始化中可以使用servlet属性源
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment cwe) {
cwe.initPropertySources(sc, null);
}
customizeContext(sc, wac);
wac.refresh();
}
这个函数用于配置和刷新Web应用上下文。它首先根据可用信息为上下文分配一个更有用的id。然后设置上下文的servlet上下文和配置位置。接下来,它通过调用环境的initPropertySources方法来确保在刷新上下文之前可以使用servlet属性源进行后处理或初始化。最后,它自定义上下文并刷新上下文。自定义上下文的源码如下:
/**
* 在将上下文刷新之前,自定义由 this ContextLoader 创建的 {@link ConfigurableWebApplicationContext}。
* 默认实现通过 {@linkplain #determineContextInitializerClasses(ServletContext)} 确定了通过
* {@linkplain #CONTEXT_INITIALIZER_CLASSES_PARAM 上下文初始化参数} 和
* {@linkplain ApplicationContextInitializer#initialize(ConfigurableApplicationContext)} 方法给出的上下文初始化器类。
*
实现了 {@link org.springframework.core.Ordered Ordered} 或被标记了 @{@link org.springframework.core.annotation.Order Order}
* 的任何 {@code ApplicationContextInitializers} 将被适当排序。
* @param sc 当前的 servlet 上下文
* @param wac 新创建的上下文
* @see #CONTEXT_INITIALIZER_CLASSES_PARAM
* @see ApplicationContextInitializer#initialize(ConfigurableApplicationContext)
*/
protected void customizeContext(ServletContext sc, ConfigurableWebApplicationContext wac) {
List>> initializerClasses =
determineContextInitializerClasses(sc);
for (Class> initializerClass : initializerClasses) {
Class> initializerContextClass =
GenericTypeResolver.resolveTypeArgument(initializerClass, ApplicationContextInitializer.class);
if (initializerContextClass != null && !initializerContextClass.isInstance(wac)) {
throw new ApplicationContextException(String.format(
"无法应用上下文初始化器 [%s],因为它的泛型参数 [%s] 无法赋值给该上下文加载器使用的应用程序上下文类型:[%s]",
initializerClass.getName(), initializerContextClass.getName(), wac.getClass().getName()));
}
this.contextInitializers.add(BeanUtils.instantiateClass(initializerClass));
}
AnnotationAwareOrderComparator.sort(this.contextInitializers);
for (ApplicationContextInitializer initializer : this.contextInitializers) {
initializer.initialize(wac);
}
}
这个函数用于自定义由ContextLoader创建的ConfigurableWebApplicationContext。它首先确定通过上下文初始化参数指定的上下文初始化器类,然后使用每个初始化器对给定的Web应用程序上下文进行初始化。任何实现org.springframework.core.Ordered接口或被标记with org.springframework.core.annotation.Order注解的ApplicationContextInitializers都将按照适当的顺序进行排序。
其中AbstractApplicationContext的refresh方法用于刷新Web应用上下文的,参考Spring源码解析——容器的功能扩展