上一篇关于Spring是如何启动的文章,主要是分析了从Tomcat启动到web.xml文件加载,再到通过ContextLoaderListener监听器开始初始化WebApplicationContext,如果对这个过程不熟悉可以参考这篇文章- 漫谈Spring的启动与初始化(一)
,但是该文并没有分析到Spring容器是如何根据我们在web.xml里面配置的 contextConfigLocation
参数和Spring的特殊的配置文件 applicationContext.xml
来初始化Spring,本文着力于解决这个问题。
当ContextLoaderListener监听到ServletContext初始化事件的时候就会调用ContextLoader的initWebApplicationContext方法,这个方法完成了很多的工作,其中下面这段代码,当context为空的时候,去创建WebApplicationContext,源码如下:
private WebApplicationContext context; publicWebApplicationContextinitWebApplicationContext(ServletContext servletContext){ ... if(this.context == null) { //如果context为null就开始创建WebApplicationContext this.context = this.createWebApplicationContext(servletContext); } ... }
protectedWebApplicationContextcreateWebApplicationContext(ServletContext sc){ Class contextClass = this.determineContextClass(sc); if(!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) { throw new ApplicationContextException("Custom context class [" + contextClass.getName() + "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]"); } else { //根据类返回实例 return (ConfigurableWebApplicationContext)BeanUtils.instantiateClass(contextClass); } }
其中 determineContextClass(sc)
方法是用来寻找实现WebApplicationContext的类,实现方法如下:
protected Class<?> determineContextClass(ServletContext servletContext) { //关键代码一: String contextClassName = servletContext.getInitParameter("contextClass"); if(contextClassName != null) { try { return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader()); } catch (ClassNotFoundException var4) { throw new ApplicationContextException("Failed to load custom context class [" + contextClassName + "]", var4); } } else { //关键代码二: contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName()); try { return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader()); } catch (ClassNotFoundException var5) { throw new ApplicationContextException("Failed to load default context class [" + contextClassName + "]", var5); } } }
上面这段有两行关键的代码是决定返回的Class:如果开发人员在web.xml中配置了一个参数名为contextClass,值为WebApplicationContext接口实现类,那 getInitParameter("contextClass")
就会返回这个配置的实现类Class;如果没有配置,也就是 contextClassName==null
,那么通过 defaultStrategies.getProperty(...)
则会返回Spring默认的实现类XmlWebApplicationContext。可能有同学会好奇为什么是这个,这个类是从哪儿来的。
在 ContextLoader
这个类中有这么几行代码,在类一加载的时候执行,如下:
public class ContextLoader{ private static final Properties defaultStrategies; static { try { ClassPathResource ex = new ClassPathResource("ContextLoader.properties", ContextLoader.class); defaultStrategies = PropertiesLoaderUtils.loadProperties(ex); } catch (IOException var1) { throw new IllegalStateException("Could not load /'ContextLoader.properties/': " + var1.getMessage()); } currentContextPerThread = new ConcurrentHashMap(1); } }
在 ContextLoader.class
的同一个包下面,发现了这个配置文件,其中只有一行配置如下:
org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext
XmlWebApplicationContext这个类就是WebApplicationContext这个接口最终的实现类,也是Spring启动时默认使用的类。其实还有一些实现类,让我们自己去加载 applicationContext.xml
,比如ClassPathXmlApplicationContext。
这样在上述的createWebApplicationContext方法中,我们拿到的就是 XmlWebApplicationContext.class
, BeanUtils.instantiateClass(contextClass)
方法根据类名创建实例,并且进行强制转换得到ConfigurableWebApplicationContext接口的实例,因为XmlWebApplicationContext是后者的实现类,所以这样转换是没问题的(当然没问题哈哈)。
那么createWebApplicationContext方法分析到此为止。
我们继续看initWebApplicationContext方法中紧接着createxxx的下一段代码:
if(this.context == null) { //如果context为null就开始创建WebApplicationContext this.context = this.createWebApplicationContext(servletContext); } if(this.context instanceof ConfigurableWebApplicationContext) { ConfigurableWebApplicationContext err = (ConfigurableWebApplicationContext)this.context; if(!err.isActive()) { if(err.getParent() == null) { ApplicationContext elapsedTime = this.loadParentContext(servletContext); err.setParent(elapsedTime); } //关键代码:配置和刷新WebApplicationContext,这里其实就是XmlWebApplicationContext了 this.configureAndRefreshWebApplicationContext(err, servletContext); } }
在上面代码中我们通过XmlWebApplicationContext类创建了WebApplicationContext的实例,本节方法则是对该实例通过我们的配置文件信息进行配置和创建各种bean。关于该方法有几行关键代码如下:
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc){ ... wac.setServletContext(sc); //关键代码一:在ServletContext中获取contextConfigLocation值,查找配置文件地址 configLocationParam = sc.getInitParameter("contextConfigLocation"); if(configLocationParam != null) { wac.setConfigLocation(configLocationParam); } ... //关键代码二:刷新XmlWebApplicationContext wac.refresh(); }
在Spring的项目中,经常要在web.xml中配置contextConfigLocation的参数,我们对于下面的xml代码应该也很熟悉:
<!-- Context ConfigLocation --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath*:/spring-context*.xml</param-value> </context-param>
既然是在web.xml中配置的这些参数,为什么是在ServletContext中去取呢?在上一篇文章中分析过,Tomcat在加载web.xml文件时,会最终将该配置文件的配置属性参数以键值对的形式存放在每个web应用对应的ServletContext中,所以才有了通过上述代码 sc.getInitParameter("contextConfigLocation")
到ServletContext中取我们想要的参数值。
当然,如果我们没有在web.xml中配置该参数的话,XmlWebApplicationContext类也是有默认值的,如下:
public class XmlWebApplicationContextextends AbstractRefreshableWebApplicationContext{ public static final String DEFAULT_CONFIG_LOCATION = "/WEB-INF/applicationContext.xml"; public static final String DEFAULT_CONFIG_LOCATION_PREFIX = "/WEB-INF/"; public static final String DEFAULT_CONFIG_LOCATION_SUFFIX = ".xml"; }
到这里,配置文件信息拿到了,再就是 wac.refresh()
方法了,这个方法具体实现在AbstractApplicationContext类中,进入该方法:
public void refresh()throwsBeansException, IllegalStateException{ Object var1 = this.startupShutdownMonitor; synchronized(this.startupShutdownMonitor) { //容器预先准备,记录容器启动时间和标记 prepareRefresh(); //创建bean工厂,里面实现了BeanDefinition的装载 ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); //配置bean工厂的上下文信息,如类装载器等 prepareBeanFactory(beanFactory); try { //在BeanDefinition被装载后,提供一个修改BeanFactory的入口 postProcessBeanFactory(beanFactory); //在bean初始化之前,提供对BeanDefinition修改入口,PropertyPlaceholderConfigurer在这里被调用 invokeBeanFactoryPostProcessors(beanFactory); //注册各种BeanPostProcessors,用于在bean被初始化时进行拦截,进行额外初始化操作 registerBeanPostProcessors(beanFactory); //初始化MessageSource initMessageSource(); //初始化上下文事件广播 initApplicationEventMulticaster(); //模板方法 onRefresh(); //注册监听器 registerListeners(); //初始化所有未初始化的非懒加载的单例Bean finishBeanFactoryInitialization(beanFactory); //发布事件通知 finishRefresh(); } catch (BeansException var5) { if(this.logger.isWarnEnabled()) { this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var5); } this.destroyBeans(); this.cancelRefresh(var5); throw var5; } } }
这个方法里面就是IOC容器初始化的大致步骤了。
在IOC容器初始化之后,也就是configureAndRefreshWebApplicationContext方法执行结束后有一行代码如下:
//关键代码: servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
可以看到,这里将初始化后的context存到了servletContext中,具体的就是存到了一个Map变量中,key值就是WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE这个常量。这个常量在WebApplicationContext接口中设置的,如下:
public interface WebApplicationContextextends ApplicationContext{ //org.springframework.web.context.WebApplicationContext.ROOT String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT"; }
另外,使用Spring的WebApplicationContextUtils工具类获取这个WebApplicationContext(不过这里request获取ServletContext是有限制的,要求servlet-api.jar 包的版本是在3.0以上)方式如下:
WebApplicationContext applicationContext = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
到这里Spring的启动与初始化应该就结束了,这里面要理清ServletContext和Spring容器的关系,Spring整个容器被放置在ServletContext这样一个类似于Map的结构中。ServletContext 从字面上理解也是Servlet的容器,被 Servlet 程序间接用来与外层 Web 容器通信。例如写日志,转发请求。每一个 Web 应用程序含有一个Context ,被Web 应用内的各个程序共享。因为Context 可以用来保存资源并且共享,所以ServletContext 的常用场景是Web级应用缓存—- 把不经常更改的内容读入内存,所以服务器响应请求的时候就不需要进行慢速的磁盘I/O 了。
-EOF-