SpringMVC源码 1 ContextLoaderListener和Servlet容器web.xml配置
SpringMVC源码 1 ContextLoaderListener和Servlet容器web.xml配置
1.先说下ServletContext
javaee标准规定了,servlet容器需要在应用项目启动时,给应用项目初始化一个ServletContext作为公共环境容器存放公共信息。ServletContext中的信息都是由容器提供的。
通过contextListener获取web.xml中的参数。<context-param>以及<listener>
1.servlet容器启动的时候,找到应用配置文件(web.xml)中的<context-param>作为键值对放到ServletContext中。
2.servlet容器读取<listener>节点,构建对应的listener,容器调用了contextInitialized(ServletContextEvent event)方法,执行其中的操作。(所有Listener必须继承自ServletContextListener)
<context-param>只是将上下文的键值对传入servletContext中,本身并没有什么实际意义,需要Listener对参数进行解析后才具有一定意义。其中param-name是规定的,有Listener中进行定义。例如定义了m-name,在配置文件中<param-name>也需要是m-name。
<listener> 初始化上下文的监听器。 <listener>是否可以设置多个???答案是显然的,当然可以。
web.xml
<context-param>
<param-name>contextConfigLocation</param-name> //contextConfigLocation是在Spring的ContextLoader中定义的参数,确定Spring配置文件位置
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<context-param>
<param-name>m-name</param-name> //m-name是在自己写的ServletContextListener实现类中定义的参数
<param-value>mname</param-value>
</context-param>
<context-param>
<param-name>contextId</param-name> //contextId是在Spring的ContextLoader中定义的参数
<param-value>MMM-WebApplicationContext</param-value>
</context-param>
//如果想要在web程序初始化的时候使用ServletContextListener,就必须在web.xml中加入<listener>节点
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<listener>
<listener-class>com.XXX.MYContextListener</listener-class>
</listener>
public class MYContextListener implements ServletContextListener{
public static final String CONTEXT_NAME_PARAM = "m-name";
public static final String CONTEXT_CLASS_PARAM = "m-class";
@Override
public void contextInitialized(ServletContextEvent sce) {
ServletContext context = sce.getServletContext();
String mname = (String) context.getInitParameter(CONTEXT_NAME_PARAM);
String mclass = context.getInitParameter(CONTEXT_CLASS_PARAM);
System.out.println(mname+" "+mclass);
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
// TODO Auto-generated method stub
}
}
public class ContextLoaderListener extends ContextLoader implements ServletContextListener{
........
@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());
}
}
其中spring需要的contextConfigLocation也是在spring中的ContextLoader.class中定义的
public static final String CONTEXT_ID_PARAM = "contextId";
public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation";
public static final String CONTEXT_CLASS_PARAM = "contextClass";等等等等 具体看后面的源码解释。
2.执行流程
web.xml在<context-param>标签中声明应用范围内的初始化参数
1.启动一个WEB项目的时候,容器(如:Tomcat)会去读它的配置文件web.xml.读两个节点: <listener>和 <context-param>
2.紧接着,容器创建一个ServletContext(上下文)。在该应用内全局共享。
3.容器将<context-param>转化为键值对,并交给ServletContext。有接下来的ServletContextListener去调用
4.容器创建<listener>中的类实例,即创建监听.该监听器必须实现自ServletContextListener接口。可以定义多个listener,执行顺序按照web.xml中定义的顺序来执行。
5.在Listener中会有contextInitialized(ServletContextEvent event)初始化方法
在这个方法中获得ServletContext = ServletContextEvent.getServletContext();
String value = ServletContext.getInitParameter("<context-param name>");
6.得到这个context-param的值之后,你就可以做一些操作了.注意,这个时候你的WEB项目还没有完全启动完成.这个动作会比所有的Servlet都要早.
换句话说,这个时候,你对<context-param>中的键值做的操作,将在你的WEB项目完全启动之前被执行.只有在这个时候<context-param>中的参数才是有意义的(也只对相应的listener而言)。对于ServletContext来说只是一组String键值对
所有的servlet容器和listener监听类,都是同意按照java Servlet API来进行开发的。Package javax.servlet
3.ContextLoaderListener 和ContextLoader
项目中使用Spring 时,applicationContext.xml配置文件中并没有BeanFactory,要想在业务层中的class 文件中直接引用Spring容器管理的bean可通过以下方式
1、在web.xml配置监听器ContextLoaderListener。ContextLoaderListener的作用就是启动Web容器时,自动装配ApplicationContext的配置信息。因为它实现了ServletContextListener这个接口,在 web.xml配置这个监听器,启动容器时,就会默认执行它实现的方法。
在ContextLoaderListener继承了ContextLoader这个类,所以整个加载配置过程由ContextLoader来完成。
ContextLoaderListener作为springMVC,在Servlet容器启动时的上下文件监听器。在servlet容器启动的时候,会调用在<listener>中定义的ContextLoaderListener,调用contextInitialzed()方法,初始化上下文。
ContextLoaderListener实现ServletContextListener接口,用于在Servlet容器启动时开启监听,获取ServletContext对象。
ContextLoaderListener实现了ContextLoader,也是ContextLoader的唯一子类。主要的上下文加载工作由ContextLoader完成。
public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
public ContextLoaderListener() {
}
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());
}
}
public class ContextLoader {
//定义web.xml <context-param>中可以配置的<context-name>Name of servlet context parameter
public static final String CONTEXT_ID_PARAM = "contextId"; //上下文的id
public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation"; //为root context确定配置的位置。(配置文件的位置)
public static final String CONTEXT_CLASS_PARAM = "contextClass"; //确定使用WebApplicationContext的那个子类。默认使用XmlWebApplicationContext。
//也可以配置为AnnotationConfigWebApplicationContext、GenericWebApplicationContext等
public static final String CONTEXT_INITIALIZER_CLASSES_PARAM = "contextInitializerClasses";
public static final String GLOBAL_INITIALIZER_CLASSES_PARAM = "globalInitializerClasses";
public static final String LOCATOR_FACTORY_SELECTOR_PARAM = "locatorFactorySelector";
public static final String LOCATOR_FACTORY_KEY_PARAM = "parentContextKey";
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
//如果已经存在了根共享Web应用程序环境,则抛出异常提示客户
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。
1.确定需要构造的WebApplicationContext的类型 class<?> contextClass。 从ServletContext中获取contextClass的定义。需要在web.xml中配置<context-param>对应的<param-name>和<param-value>配置需要全类名
如果在web.xml没有配置,则默认使用的是org.springframework.web.context.support.XmlWebApplicationContext。
2.判断是否是contextClass是不是ConfigurableWebApplicationContext(可配置的web应用上下文)的子类。不是的话抛出异常。 ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)
3.通过contexClass构造WebApplicationContext 的实例
}
if (this.context instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
if (!cwac.isActive()) {
// The context has not yet been refreshed -> provide services such as
// setting the parent context, setting the application context id, etc
if (cwac.getParent() == null) {
// The context instance was injected without an explicit parent ->
// determine parent for root web application context, if any.
ApplicationContext
parent = loadParentContext(servletContext);
事实上,根共享环境的加载时同样可以加载一个父环境。尽管这种情况是不常见的,但是Spring Web MVC提供了这样的扩展性。在Servlet初始化参数中可以配置一个Bean工厂路径(locatorFactorySelector),这个Bean工厂路径会被Bean工厂定位器所加载,Bean工厂定位器会在这个Bean工厂中查找以另外一个Servlet参数(parentContextKey)为名字的Bean工厂对象,最后得到的Bean工厂对象则是根共享环境的父环境。如果在初始化参数中没有配置Bean工厂路径,则用缺省的Bean工厂路径classpath*:beanRefFactory.xml。
没有在web.xml配置上述两个参数时,返回的ptarent是null。即根web应用上下文没有父级。
cwac.setParent(parent);
}
configureAndRefreshWebApplicationContext(cwac,
servletContext); //配置和刷新web应用上下文,这才是启动sprigIoc的关键
}
}
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
将WebApplicationcontext放到ServletContext中,将这个环境以ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE为关键字保存在Servlet环境中,这个根共享环境在Servlet加载专用子环境中被引用作为父环境。
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;
}
}
}
protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
//根据ServletContext中“contextClass”的参数去创建对应的WebApplication,如果没有这个参数,则通过去加载ContextLoader.properties文件中的默认参数。(XMLWebApplicationContext)
Class<?> contextClass = determineContextClass(sc);
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);
}
- protected ApplicationContext loadParentContext(ServletContext servletContext) {
- ApplicationContext parentContext = null;
-
- //取得Web.xml初始化参数配置中对LOCATOR_FACTORY_SELECTOR_PARAM的配置串,这是Bean工厂定位器使用的Bean工厂的路径,如果这个值没有配置,则使用缺省的classpath*:beanRefFactory.xml
- String locatorFactorySelector = servletContext.getInitParameter(LOCATOR_FACTORY_SELECTOR_PARAM);
- //取得Web.xml初始化参数配置中对LOCATOR_FACTORY_KEY_PARAM的配置串,这是用来取得Bean工厂的关键字
- String parentContextKey = servletContext.getInitParameter(LOCATOR_FACTORY_KEY_PARAM);
-
- if (parentContextKey != null) {
- //locatorFactorySelector如果为空,则使用缺省值classpath*:beanRefFactory.xml初始化Bean工厂定位器
- BeanFactoryLocator locator = ContextSingletonBeanFactoryLocator.getInstance(locatorFactorySelector);
- Log logger = LogFactory.getLog(ContextLoader.class);
- if (logger.isDebugEnabled()) {
- logger.debug("Getting parent context definition: using parent context key of '" +
- parentContextKey + "' with BeanFactoryLocator");
- }
- //Bean工厂定位器从配置的Bean工厂中找到制定关键字(参数LOCATOR_FACTORY_KEY_PARAM的值) 的工厂
- this.parentContextRef = locator.useBeanFactory(parentContextKey);
-
- //进而取得一个应用程序环境,这个应用程序环境作为根共享应用程序环境的父环境
- parentContext = (ApplicationContext) this.parentContextRef.getFactory();
- }
- return parentContext;
- }
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
if (idParam != null) {
wac.setId(idParam);
}else {
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + //默认id
ObjectUtils.getDisplayString(sc.getContextPath()));
}
}
wac.setServletContext(sc); //对WebApplication设置Servlet容器的ServletContext。
String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
if (configLocationParam != null) {
wac.setConfigLocation(configLocationParam); //配置spring的配置文件路径
XmlWebApplicationContext默认为 “/WEB-INF/applicationContext.xml”
}
// The wac environment's
#initPropertySources will be called in any case when the context
// is refreshed; do it eagerly here to ensure servlet property sources are in place for
// use in any post-processing or initialization that occurs below prior to #refresh
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
}
customizeContext(sc, wac);
wac.refresh(); 刷新上线文,具体上下文初始化操作,可以看AbstractApplicationContext.refresh()。
}
4.Spring上下文在web.xml中的配置
Spring为我们提供了实现ServletContextListener接口的上下文初始化监听器:org.springframework.web.context.ContextLoaderListener,也是唯一的一个实现。SpringMVC使用其进行初始化。
spring为我们提供的IOC容器,需要我们指定容器的配置文件,然后由该监听器初始化并创建该容器。
要求你指定配置文件的地址及文件名称,一定要使用:contextConfigLocation作为参数名称。因为这是在ContextLoader中定义好的。
<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>
该监听器,默认读取/WEB-INF/下的applicationContext.xml文件。但是通过context-param指定配置文件路径后,便会去你指定的路径下读取对应的配置文件,并进行初始化。
public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation";
<param-value>/WEB-INF/applicationContext.xml</param-value>
<param-value>/WEB-INF/applicationContext.xml,/WEB-INF/action-servlet.xml,/WEB-INF/jason-servlet.xml</param-value>
<param-value>classpath:applicationContext.xml </param-value>
5.web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<!--1、在web.xml配置监听器ContextLoaderListener。-->
<!--ContextLoaderListener的作用就是启动Web容器时,自动装配ApplicationContext的配置信息。因为它实现了ServletContextListener这个接口,在 web.xml配置这个监听器,启动容器时,就会默认执行它实现的方法。在ContextLoaderListener继承了ContextLoader这个类,所以整个加载配置过程由ContextLoader来完成。-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!--2、部署applicationContext的xml文件-->
<!--如果在web.xml中不写任何参数配置信息,默认的路径是"/WEB-INF/applicationContext.xml, 在WEB-INF目录下创建的xml文件的名称必须是applicationContext.xml。
如果是要自定义文件名可以在web.xml里加入contextConfigLocation这个context参数:
在<param-value> </param-value>里指定相应的xml文件名,如果有多个xml文件,可以写在一起并以“,”号分隔。 也可以这样applicationContext-*.xml采用通配符,比如这那个目录下有applicationContext-ibatis-base.xml,applicationContext-action.xml,applicationContext-ibatis-dao.xml等文件,都会一同被载入。
在ContextLoaderListener中继承了ContextLoader这个类,所以整个加载配置过程由ContextLoader来完成。-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/applicationContext.xml</param-value>
</context-param>
<!--使用Spring MVC,配置DispatcherServlet是第一步。DispatcherServlet是一个Servlet,,所以可以配置多个DispatcherServlet-->
<!--DispatcherServlet是前置控制器,配置在web.xml文件中的。拦截匹配的请求,Servlet拦截匹配规则要自已定义,把拦截下来的请求,依据某某规则分发到目标Controller(我们写的Action)来处理。-->
<servlet>
<!--在DispatcherServlet的初始化过程中,框架会在web应用的 WEB-INF文件夹下寻找名为[servlet-name]-servlet.xml 的配置文件,生成文件中定义的bean。-->
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--指明了配置文件的文件名,不使用默认配置文件名,而使用dispatcher-servlet.xml配置文件。-->
<init-param>
<param-name>contextConfigLocation</param-name>
<!--其中<param-value>**.xml</param-value> 这里可以使用多种写法-->
<!--1、不写,使用默认值:/WEB-INF/<servlet-name>-servlet.xml-->
<!--2、<param-value>/WEB-INF/classes/dispatcher-servlet.xml</param-value>-->
<!--3、<param-value>classpath*:dispatcher-servlet.xml</param-value>-->
<!--4、多个值用逗号分隔-->
<param-value>classpath:spring/dispatcher-servlet.xml</param-value>
</init-param>
<!--是启动顺序,让这个Servlet随Servletp容器一起启动。-->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<!--这个Servlet的名字是dispatcher,可以有多个DispatcherServlet,是通过名字来区分的。每一个DispatcherServlet有自己的WebApplicationContext上下文对象。同时保存的ServletContext中和Request对象中.-->
<!--ApplicationContext是Spring的核心,Context我们通常解释为上下文环境,我想用“容器”来表述它更容易理解一些,ApplicationContext则是“应用的容器”了:P,Spring把Bean放在这个容器中,在需要的时候,用getBean方法取出-->
<servlet-name>DispatcherServlet</servlet-name>
<url-pattern>/</url-pattern> <!--会拦截URL中带“/”的请求。-->
</servlet-mapping>
</web-app>
contextConfigLocation:Spring的核心就是配置文件,可以说配置文件是Spring中必不可少的。这个参数就是制定了SpringMVC需要使用的配置文件,这个参数将Web和Spring的配置文件相结合的一个关键参数。主要就是通过ContextLoaderListener在Servlet容器初始化过程中,获取contextConfigLocation参数,然后ContextLoader根据参数指定的路径加载配置文件,完成Spring容器的加载。
DispatchServlet:包含了SpringMVC的请求逻辑,SpringMVC使用此类拦截web请求并转发到相应的Controller中进行相应的逻辑处理。
注意:<context-param>中的contextConfigLocation初始化的Spring Ioc容器,针对整个Spring应用。但是在<init-param>中的contextConfigLocation是属于DispatchServlet的,也就是说的SpringMVC
Ioc容器,主要包含的是@Controller。需要注意Spring Ioc和SpringMVC Ioc容器的关系,防止bean的重复加载。
5.Spring上下文初始化了什么??
ServletContext由Servlet容器进行初始化,spring的ContextLoaderListener初始化了WebApplicationContext。
1.servlet容器启动,为web应用创建一个全局上下文环境,ServletContext。每个web.xml对应一个全局上下文环境。
2.servlet容器调用web.xml中配置的ContextLoaderListener,加载context-param指定的配置文件的信息到IOC容器中。 初始化属于SpringMVC
的WebApplicationContext上下文环境(即IOC容器)。
从上文的源码中可以看到WebApplicationContext在ServletContext中以键值对的形式保存。
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
将WebApplicationcontext放到ServletContext中
3.servlet容器开始初始化web.xml中配置的<servlet>。为其初始化属于自己的ServletContext(这个每个servlet都是独立的,上面的ServletContext是整个webapp中全局上下文)。并加载其设置的配置信息到该上下文中。将WebApplicationContext设置为Servlet的父容器。
4. 此后的所有servlet的初始化都按照3步中方式创建,初始化自己的上下文环境,将WebApplicationContext设置为自己的父上下文环境。
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc-servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
keyA = WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
spring配置时:<context:exclude-filter>的使用原因,为什么在applicationContext.xml中排除controller,而在spring-mvc.xml中incloud这个controller
既然知道了spring的启动流程,那么web容器初始化webApplicationContext时作为公共的上下文环境,只需要将service、dao等的配置信息在这里加载,而servlet自己的上下文环境信息不需要加载。故,在applicationContext.xml中将@Controller注释的组件排除在外,而在dispatcherServlet加载的配置文件中将@Controller注释的组件加载进来,方便dispatcherServlet进行控制和查找。故,配置如下:
applicationContext.mxl中:
<context:component-scan base-package="com.linkage.edumanage">
<context:exclude-filter expression="org.springframework.stereotype.Controller" type="annotation" />
</context:component-scan>
spring-mvc.xml中:
<context:component-scan base-package="com.brolanda.cloud" use-default-filters="false">
<context:include-filter expression="org.springframework.stereotype.Controller" type="annotation" />
</context:component-scan>
若SpringIoC容器和SpringMVCIoc容器扫描的包有重合的部分,会导致重合的Bean被创建两次。
SpringIOC容器可以理解为是WebApplicationCont。SpringMVCIoc容器则是,DispatchServlet对应的ServletContext。