一、ApplicationContext
Spring框架可以应用于web环境和非web环境。通常情况下,非web环境下ApplicationContext接口常用的是其实现子类ClassPathXmlApplicationContext或FileSystemXmlApplicationContext;Spring应用于web环境下,需要额外的导入web相关的jar包,spring-web-x.x.x.RELEASE.jar,通过ApplicationContext的子接口WebApplicationContext(常用子类XmlWebApplicationContext)来和web容器整合。这三个applicationContext,分别允许从类路径、文件系统和web根目录下来加载配置文件完成容器初始化工作。
二、整合思想
1. 整合面临的问题
非web环境下,我们使用spring IOC容器是通过new一个applicationContext,也就是spring容器是需要我们自己创建出来才能使用。但是在web环境下,要想启动spring核心容器,该怎么来做?当然可以手动创建spring容器applicationContext,你可以在需要用到applicationContext的地方手动创建出来,直接new XmlWebApplicationContext();,然后再setConfigLocation(String location);即可,或者直接将applicationConext.xml放置在WEB-INF目录下,spring实现的源码如下。手动创建spring容器,你可以在servlet的doXXX()方法中来实例化applicationContext,也就是哪里用到spring容器就在哪里实例化该对象,并且API也支持。
但是这种方式有一个最大的问题,实例化spring容器太多次。
public class XmlWebApplicationContext extends AbstractRefreshableWebApplicationContext {
/** Default config location for the root context 默认读取配置文件的路径 */
public static final String DEFAULT_CONFIG_LOCATION = "/WEB-INF/applicationContext.xml";
/** Default constructor 默认构造函数 */
public XmlWebApplicationContext(){}
/**
* Set the config locations for this application context in init-param style,
* i.e. with distinct locations separated by commas, semicolons or whitespace.
* <p>If not set, the implementation may use a default as appropriate.
* 该方法在抽象父类中实现
*/
public void setConfigLocation(String location) {
setConfigLocations(StringUtils.tokenizeToStringArray(location, CONFIG_LOCATION_DELIMITERS));
}
}
实际上applicationContext在整个应用中实际只需要一个对象初始化一次就够了,没必要为了获得托管的bean对象而每次访问的时候都实例化该对象一次。所以问题转变成,web环境下在哪里实例化该对象一次。web环境下只创建一次的有listener、filter、servlet,filter本质上和servlet差不多,用于创建applicationContext逻辑上稍显麻烦(请求和响应都需要经过),spring提供web整合的jar包早就考虑到这个问题,并且spring提供了listener或servlet两种方式(选其一)来进行整合。问题迎刃而解,在listener或servlet的init方法中进行applicationContext的初始化,这样还解决了另一个大问题,applicationContext会随着web容器的启动而完成实例化操作,做完这一切之后你只需要在web.xml中配置好监听器或servlet。当然,如果你采用了servlet方式,那么必须考虑的问题是一般情况下servlet会在第一次被访问的时候才会被实例化,那每次应用上线之前都需要人为访问一次,这样太愚蠢了。servlet在配置的时候有一个配置项<load-on-startup></load-on-startup>,值是一个正整数,用于告诉web容器在启动的时候就加载该servlet(实例化+init)。如果你才用的是侦听器的方式,在JSP/SERVLET规范中提供的8个监听器中你需要选择一个合适的侦听器来帮你完成这项工作,这个侦听器能够侦听到web容器ServletContext的创建和消亡,显然ServletContextListener再适合不过了。
忙完这一切,还不够,你还有最后一项工作需要完成。在listener或servlet中创建好的applicationContext怎么能被应用共享呢?listener的init方法传入一个对象ServletContextEvent,servlet的init方法则是传入ServletConfig对象,这两个对象可以获得ServletContext,而ServletContext对象全应用共享,所以整合的最后一步就是将创建出来的applicationContext放入ServletContext中。
至此,整合思想完成。庆幸的是,你不用感觉到麻烦,因为spring-web-x.x.x.RELEASE.jar已经全部帮你完成了,你只需要导入jar包,然后选择是listener方式还是servlet方式整合,然后配置一下web.xml即可。
2. 核心问题梳理
Problem 1: spring容器需要随着web容器的加载而完成初始化
Problem 2: 初始化完成的spring容器需要放置在整个应用共享范围
这也是spring和web整合的核心问题。
三、源码节选
1. 侦听器方式:ContextLoaderListener
在配置好web.xml的<listener></listener>后,容器启动会加载该侦听器并且调用其init方法。
/*
* Copyright 2002-2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.context;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
/**
* Bootstrap listener to start up and shut down Spring's root {@link WebApplicationContext}.
* Simply delegates to {@link ContextLoader} as well as to {@link ContextCleanupListener}.
*
* <p>This listener should be registered after
* {@link org.springframework.web.util.Log4jConfigListener}
* in <code>web.xml</code>, if the latter is used.
*
* <p>As of Spring 3.1, {@code ContextLoaderListener} supports injecting the root web
* application context via the {@link #ContextLoaderListener(WebApplicationContext)}
* constructor, allowing for programmatic configuration in Servlet 3.0+ environments. See
* {@link org.springframework.web.WebApplicationInitializer} for usage examples.
*
* @author Juergen Hoeller
* @author Chris Beams
* @since 17.02.2003
* @see org.springframework.web.WebApplicationInitializer
* @see org.springframework.web.util.Log4jConfigListener
*/
public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
private ContextLoader contextLoader;
/**
* Initialize the root web application context.
*/
public void contextInitialized(ServletContextEvent event) {
this.contextLoader = createContextLoader();
if (this.contextLoader == null) {
this.contextLoader = this;//转成父类型
}
this.contextLoader.initWebApplicationContext(event.getServletContext());//调用父类中初始化WebApplicationContext<span style="font-family: 'Arial Black';">法</span><span style="font-family: 'Arial Black';">
</span>
}
/**
* Create the ContextLoader to use. Can be overridden in subclasses.
* @return the new ContextLoader
* @deprecated in favor of simply subclassing ContextLoaderListener itself
* (which extends ContextLoader, as of Spring 3.0)
*/
@Deprecated
protected ContextLoader createContextLoader() {
return null;//返回值为null
}
/**
* Close the root web application context.
*/
public void contextDestroyed(ServletContextEvent event) {
if (this.contextLoader != null) {
this.contextLoader.closeWebApplicationContext(event.getServletContext());
}
ContextCleanupListener.cleanupAttributes(event.getServletContext());
}
}
先将ContextLoaderListener转成父类,然后调用父类真正初始化WebApplicationContext的方法initWebApplicationContext(ServletContext)。查看父类方法中的源码。
/**
* Performs the actual initialization work for the root application context.
* Called by {@link ContextLoaderListener}.
*
* <p>Looks for a {@link #CONTEXT_CLASS_PARAM "contextClass"} parameter
* at the <code>web.xml</code> context-param level to specify the context
* class type, falling back to the default of
* {@link org.springframework.web.context.support.XmlWebApplicationContext}
* if not found. With the default ContextLoader implementation, any context class
* specified needs to implement the ConfigurableWebApplicationContext interface.
*
* <p>Processes a {@link #CONFIG_LOCATION_PARAM "contextConfigLocation"}
* context-param and passes its value to the context instance, parsing it into
* potentially multiple file paths which can be separated by any number of
* commas and spaces, e.g. "WEB-INF/applicationContext1.xml,
* WEB-INF/applicationContext2.xml". Ant-style path patterns are supported as well,
* e.g. "WEB-INF/*Context.xml,WEB-INF/spring*.xml" or "WEB-INF/**/*Context.xml".
* If not explicitly specified, the context implementation is supposed to use a
* default location (with XmlWebApplicationContext: "/WEB-INF/applicationContext.xml").
*
* <p>Note: In case of multiple config locations, later bean definitions will
* override ones defined in previously loaded files, at least when using one of
* Spring's default ApplicationContext implementations. This can be leveraged
* to deliberately override certain bean definitions via an extra XML file.
*
* <p>Above and beyond loading the root application context, this class
* can optionally load or obtain and hook up a shared parent context to
* the root application context. See the
* {@link #loadParentContext(ServletContext)} method for more information.
*
* <p>As of Spring 3.1, {@code ContextLoader} supports injecting the root web
* application context via the {@link #ContextLoader(WebApplicationContext)}
* constructor, allowing for programmatic configuration in Servlet 3.0+ environments. See
* {@link org.springframework.web.WebApplicationInitializer} for usage examples.
*
* @author Juergen Hoeller
* @author Colin Sampaleanu
* @author Sam Brannen
* @since 17.02.2003
* @see ContextLoaderListener
* @see ConfigurableWebApplicationContext
* @see org.springframework.web.context.support.XmlWebApplicationContext
*/
public class ContextLoader {
/**
* Name of servlet context parameter (i.e., {@value}) that can specify the
* config location for the root context, falling back to the implementation's
* default otherwise.
* @see org.springframework.web.context.support.XmlWebApplicationContext#DEFAULT_CONFIG_LOCATION
*/
public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation";//applicationContext.xml文件位置
/**
* Initialize Spring's web application context for the given servlet context,
* using the application context provided at construction time, or creating a new one
* according to the "{@link #CONTEXT_CLASS_PARAM contextClass}" and
* "{@link #CONFIG_LOCATION_PARAM contextConfigLocation}" context-params.
* @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) {
/**
* The root WebApplicationContext instance that this loader manages.
*/
private WebApplicationContext context;
//String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT";
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 {
// Store context in local instance variable, to guarantee that
// it is available on ServletContext shutdown.
if (this.context == null) {
this.context = createWebApplicationContext(servletContext);//创建WebApplicationContext
}
if (this.context instanceof ConfigurableWebApplicationContext) {
configureAndRefreshWebApplicationContext((ConfigurableWebApplicationContext)this.context, servletContext);
}
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);//将创建出来的WebApplicationContext对象放入ServletContext中
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;
}
}
}
鉴于篇幅问题,本处只展示了部分代码,详情请查看spring整合web的jar包:
spring-web-x.x.x.RELEASE.jar。
2. servlet方式:ContextLoaderServlet
在spring2.4或更高版本中已经移除了该方式,实际上该方式的配置和ContextLoaderListener一样,只需要加多一个配置项load-on-startup,本处不再讨论,第四部分提供实现配置源码。
Note that this class has been deprecated for containers implementing Servlet API 2.4 or higher, in favor of ContextLoaderListener.
四、整合实现
1. listener方式
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
id="WebApp_ID" version="3.0">
<display-name>ss01</display-name>
<!--
1. context-param中的param会以KV的方式存入ServletContext
2. 如果applicationContext.xml没有放在默认路径/WEB-INF/applicationContext.xml(定义在XmlWebApplicationContext中),
则需要配置contextConfigLocation(定义在ContextLoader中)
3. 其value是配置文件applicationContext.xml的存放路径,如果不是web根目录下,则需要加classpath修饰
-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<!--
配置上下文加载侦听器:spring容器随着web容器的启动而加载
-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
2. servlet方式
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
id="WebApp_ID" version="3.0">
<display-name>ss01</display-name>
<!--
1. context-param中的param会以KV的方式存入ServletContext
2. 如果applicationContext.xml没有放在默认路径/WEB-INF/applicationContext.xml(定义在XmlWebApplicationContext中),
则需要配置contextConfigLocation(定义在ContextLoader中)
3. 其value是配置文件applicationContext.xml的存放路径,如果不是web根目录下,则需要加classpath修饰
-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<!--
配置Servlet:servlet随着web容器的加载而加载,加载的同时完成spring容器的初始化
-->
<servlet>
<servlet-name>contextLoaderServlet</servlet-name>
<servlet-class>org.apache.web.context.ContextLoaderServelt</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>contextLoaderServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
五、一些细节
1. servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
在ContextLoaderListener中创建出来的WebApplicationContext对象被放置在了ServletContext中,设置的KEY=WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,即KEY=String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT";,所以要想获得WebApplicationContext对象,可以getAttribute(key=WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE)。
2. org.springframework.web.context.support.WebApplicationContextUtils
spring整合web的jar包中提供了便捷的工具类获取WebApplicationContext对象,org.springframework.web.context.support.WebApplicationContextUtils.getWebApplicationContext(ServletContext sc);
3. 反射
在ContextLoaderListener中this.context = createWebApplicationContext(servletContext);创建了WebApplicationContext对象,而该方法内通过ConfigurableWebApplicationContext wac =
(ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);实际创建,Spring beans包中Beanutils方法通过return instantiateClass(clazz.getDeclaredConstructor());来获取对象,在进入这个方法,可以看到最终是通过反射获取WebApplicationContext的Class构造器,从而newInstance()创建对象return ctor.newInstance(args);。
所以,最终还是通过反射获取spring web容器。
本文如有错漏,烦请不吝指正,谢谢!