上一篇关于Spring是如何启动的文章,主要是分析了从Tomcat启动到web.xml文件加载,再到通过ContextLoaderListener
监听器开始初始化WebApplicationContext
这个过程,如果不熟悉可以参考这篇-漫谈Spring的启动与初始化(一),但是上一篇还没有分析到Spring容器是如何通过web.xml里面配置的contextConfigLocation
参数和Spring容器的配置文件applicationContext.xml
来初始化Spring,本文着力于解决这个疑惑。
initWebApplicationContext方法
当ContextLoaderListener
监听到ServletContext
初始化事件的时候就会调用ContextLoader
的initWebApplicationContext
方法,这个方法完成了很多的工作,其中便有下面这段关键代码,源码如下:
private WebApplicationContext context;
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
...
if(this.context == null) {
//如果context为null就开始创建WebApplicationContext
this.context = this.createWebApplicationContext(servletContext);
}
...
}
createWebApplicationContext方法
下面进入该方法,源码如下:
protected WebApplicationContext createWebApplicationContext(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
方法中下面这段代码的最后一行:
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()) {
...
//关键代码:配置和刷新WebApplicationContext,这里其实就是XmlWebApplicationContext了
this.configureAndRefreshWebApplicationContext(err, servletContext);
}
}
configureAndRefreshWebApplicationContext方法
在上面代码中我们通过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代码应该也很熟悉,它设置了Spring容器配置文件的路径:
<!-- 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中,这样我们在web应用中的任何地方都可以拿到该参数值(后面还会提到ServletContext其他的使用场景)。
当然,如果我们没有在web.xml中配置该参数的话,XmlWebApplicationContext类也是有默认值的,如下:
public class XmlWebApplicationContext extends 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() throws BeansException, 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容器初始化的大致步骤了。
after configureAndRefresh
在IOC容器初始化之后,也就是configureAndRefreshWebApplicationContext方法执行结束后有一行代码如下:
//关键代码:
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
可以看到,这里初始化后的context被存放到了servletContext中,具体的就是存到了一个Map变量中,key值就是WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE这个常量。这个key常量在WebApplicationContext接口中设置的,如下:
public interface WebApplicationContext extends 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 了。
参考
深入理解Spring系列之七:web应用自动装配Spring配置
-EOF-