【Spring】Spring&WEB整合原理及源码剖析

一、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容器。


附注:

        本文如有错漏,烦请不吝指正,谢谢!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值