springMVC源码(1) ContextLoaderListener

ContextLoaderListener的作用

在spring官网有这样一幅图(如下),它讲述了web项目中父子容器的概念,从图中可以看到在进行web项目开发的时候我们可以把跟前端比较靠近的一部分(如:Controller,ViewResolver,HandlerMapping)组件放入servlet webApplicationContext这个子容器中,把跟业务逻辑相关的组件(如:Service,Repositories)放在Root webApplicationContext父容器中。当我们需要这些组件的时候,只需要通过子容器就可以拿到,如果在子容器中没找到就会去父容器中找。父子容器并不是web项目特有的,在使用spring作为Bean容器的项目中都可以为一个容器设置父容器。
springMVC源码(1)    ContextLoaderListener
这样做的好处在于将组件配置文件分离,不必在一个xml文件中配置,分工明确,减少不必要的错误和麻烦。在web项目中ContextLoaderListener就承担着创建父容器的任务,DispatcherServlet承担创建子容器的任务。

ContextLoaderListener的结构

public class ContextLoaderListener extends ContextLoader implements ServletContextListener

可以看到ContextLoaderListener继承ContextLoader类并实现了ServletContextListener接口。

public interface ServletContextListener extends EventListener {
    public void contextInitialized(ServletContextEvent sce);
    public void contextDestroyed(ServletContextEvent sce);
}

可以看到ServletContextListener有两个方法,它们的作用是在servletContext初始化之后和销毁之前做一些事情。

public class ContextLoader {
    	public WebApplicationContext initWebApplicationContext(ServletContext servletContext);
        protected WebApplicationContext createWebApplicationContext(ServletContext sc);
        protected Class<?> determineContextClass(ServletContext servletContext);
        protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc)
        ......
}

ContextLoader中方法主要是实例化一个webApplicationContext对象即IOC容器。

对ContextLoaderListener进行配置

<!-- 初始化spring容器 -->
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring/applicationContext*.xml</param-value>
  </context-param>
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>

在web.xml文件中进行这样的简单的配置就可以在启动一个web项目的时候创建一个webApplicationContext对象即在web项目中使用的IOC容器,容器会加载你配置的xml文件。

ContextLoaderListener的初始化

如我们在web.xml配置的一样,ContextLoaderListener在tomcat中只是一个Listener,从它实现的接口来看它是一个ServletContextListener,前面也介绍了实现了这个接口的类会在一个servletContext初始化的时候被调用。所以从contextInitialized方法跟踪ContextLoaderListener的初始化。

@Override
	public void contextInitialized(ServletContextEvent event) {
		initWebApplicationContext(event.getServletContext());
	}

这个方法调用了initWebApplication方法并将一个servletContext作为参数传入。

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
		// 第一步: 查看 servletContext是否已经有了webApplicationContext
                // WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.ROOT
		if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE)!= null) {
			throw new IllegalStateException();
		}
			if (this.context == null) {
				// 第二步:创建一个xmlwebpApplicationContext对象
				this.context = createWebApplicationContext(servletContext);
			}
			if (this.context instanceof ConfigurableWebApplicationContext) {
				ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
				if (!cwac.isActive()) {
					if (cwac.getParent() == null) {
						ApplicationContext parent = loadParentContext(servletContext);
						cwac.setParent(parent);
					}
                                        //第三步  配置webApplicationContext并调用其onfresh方法加载xml中的单例bean
					configureAndRefreshWebApplicationContext(cwac, servletContext);
				}
			}
			//第四步:将application Context 放入servletContext 中
			servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
			return this.context;
	}

可以看到在上面方法中做了这个几件事情:

1.查看servletContext中是否已经了webApplicationContext(通过查看servletContext是否有属性名为WebApplicationContext.ROOT的属性值来判断的),有就抛出异常,没有则进行下一步。

2.如果没有通过构造方法传递一个WebApplicationContext对象也就是说this.context == null 的时候就调用createWebApplicationContext方法实例化一个XmlWebApplicationContext对象(ContextLoader 可以通过构造方法传入一个webApplicationContext对象,但是一般我们在web.xml配置的都是使用默认构造方法,所以这里this.context == null)

3.调用configureAndRefreshWebApplicationContext方法配置webApplicationContext并加载xml中单例Bean。

4.将webApplicationContext对象放入ServletContext中。

看看是如何create一个WebApplicationContext对象的。

protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
                //获得WebApplicationContext的class
		Class<?> contextClass = determineContextClass(sc);
		.....
                //利用反射实例化一个contextClass对象
		return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
	}

接着看是如何获得WebApplicationContext的class对象的。

protected Class<?> determineContextClass(ServletContext servletContext) {
	          // 从servletContext中获得参数名为contextclass的参数                          
		String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
		if (contextClassName != null) {
			try {
                                //返回在web.xml中配置的contextClass的Class对象
				return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
			}
			catch (ClassNotFoundException ex) {
				throw new ApplicationContextException(
						"Failed to load custom context class [" + contextClassName + "]", ex);
			}
		}
		else {
                        //如果没有在web.xml中设置,则使用默认的。
			contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
			try {
				//返回一个org.springframework.web.context.support.XmlWebApplicationContext Class对象
				return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
			}
			catch (ClassNotFoundException ex) {
				throw new ApplicationContextException(
						"Failed to load default context class [" + contextClassName + "]", ex);
			}
		}
	}

首先从servletContext中获得属性名为contextclass的属性值,如果没有配置这样的属性,则使用默认的WebApplicationContext类型。

那么默认的又是如何设置的呢?设置的WebApplicationContext的类型又是什么呢?

从ContextLoaderListener一段静态代码块看出是加载了ContextLoader.properites文件,

文件位于spring-web\src\main\resources\org\springframework\web\context\ContextLoader.properties

文件内容是org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext。

所以通过defaultStrategies.getProperty(WebApplicationContext.class.getName())就获得了webApplicationContext的类型。

static {
		try {
			//  加载了  ContextLoader.properties  中的属性			
			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());
		}
	}

从determineContextClass方法中可以看出,我们可以自己配置WebApplicationContext的类型,但是配置的类型一定要是ConfigurableWebApplicationContext 的子类!!!!!

<context-param>
    <param-name>contextclass</param-name>
    <param-value>....</param-value>
</context-param>

在执行第三步之前,首先判断WebApplicationContext容器isActive,意思是确定此容器是否处于活动状态,即是否已至少刷新一次并且尚未关闭。刷新指执行容器的onfresh方法,没有关闭是 没有执行close方法。然后在容器的父容器为空的情况下使用loadParentContext方法找出父容器,这个方法直接返回null。

protected ApplicationContext loadParentContext(ServletContext servletContext) {
		return null;
	}

看看第三步到底做了什么

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
		// 1.  设置id
		if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
			// The application context id is still set to its original default value
			// -> assign a more useful id based on available information
			String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
			if (idParam != null) {
				wac.setId(idParam);
			}
			else {
				// Generate default id...
				wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
						ObjectUtils.getDisplayString(sc.getContextPath()));
			}
		}
		//2. 设置servletContext
		wac.setServletContext(sc);
                //3.设置xml文件
		String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
		// 设置configLocation
		if (configLocationParam != null) {
			wac.setConfigLocation(configLocationParam);
		}
		//4.将servletContext作为属性放入Environment中,可以通过getProperty来获取你配置在servletContext中的initParam参数值
		ConfigurableEnvironment env = wac.getEnvironment();
		if (env instanceof ConfigurableWebEnvironment) {
			((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
		}
                //5.执行实现了ApplicationContextInitializer接口的类
		customizeContext(sc, wac);
                //6. 执行refresh方法在单例bean
		wac.refresh();
	}

可以看到通过ServletContext获得初始化参数来设置了id,配置文件地址等信息,所以我们也可以自己来自定义这些信息。值得注意的步骤是4,5,6步骤。

4步骤就是将servletContext作为属性设置到Environment中。

5步骤主要是执行实现了ApplicationContextInitializer接口的类,这个接口只有一个方法

public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {
	void initialize(C applicationContext);
}

可以看到initialize 方法传入一个ApplicationContext参数,可以方便实现这个接口的类在执行refresh方法即初始化bean容器之前做一些事情。

在看看具体是怎么执行的吧

protected void customizeContext(ServletContext sc, ConfigurableWebApplicationContext wac) {
                // 获得配置的实现了ApplicationContextInitializer接口的类的class对象
		List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>> initializerClasses =
				determineContextInitializerClasses(sc);

		for (Class<ApplicationContextInitializer<ConfigurableApplicationContext>> initializerClass : initializerClasses) {
			...
                        // 利用反射将他们实例化,然后放入contextInitializers中
			this.contextInitializers.add(BeanUtils.instantiateClass(initializerClass));
		}

		AnnotationAwareOrderComparator.sort(this.contextInitializers);
                    //一一执行其initialize方法
		for (ApplicationContextInitializer<ConfigurableApplicationContext> initializer : this.contextInitializers) {
			initializer.initialize(wac);
		}
	}

在看看determineContextInitializerClasses方法是如何找到我们配置的类的。

protected List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>>
			determineContextInitializerClasses(ServletContext servletContext) {

		List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>> classes =
				new ArrayList<>();
		String globalClassNames = servletContext.getInitParameter(GLOBAL_INITIALIZER_CLASSES_PARAM);
		if (globalClassNames != null) {
			for (String className : StringUtils.tokenizeToStringArray(globalClassNames, INIT_PARAM_DELIMITERS)) {
				classes.add(loadInitializerClass(className));
			}
		}
		String localClassNames = servletContext.getInitParameter(CONTEXT_INITIALIZER_CLASSES_PARAM);
		if (localClassNames != null) {
			for (String className : StringUtils.tokenizeToStringArray(localClassNames, INIT_PARAM_DELIMITERS)) {
				classes.add(loadInitializerClass(className));
			}
		}
		return classes;
	}

可以看到 比较重要的两句话 :

servletContext.getInitParameter(GLOBAL_INITIALIZER_CLASSES_PARAM);

servletContext.getInitParameter(CONTEXT_INITIALIZER_CLASSES_PARAM);

也就是说 他是从servletContext的初始化参数中查找contextInitializerClasses和globalInitializerClasses参数名的配置

<context-param>
    <param-name>contextInitializerClasses</param-name>
    <param-value></param-value>
</context-param>
<context-param>
    <param-name>globalInitializerClasses</param-name>
    <param-value></param-value>
</context-param>

6步骤就是让ApplicationContext启动起来,配置一些信息,加载xml文件,实例化一些singleton bean。

相关推荐