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