轉載 https://mp.weixin.qq.com/s/Lf4akWFmcyn9ZVGUYNi0Lwjava
在《深刻理解Spring系列之一:開篇》的示例代碼中使用以下方式去加載Spring的配置文件並初始化容器。web
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("applicationContext.xml");
複製代碼
在web應用中,配置文件都是自動加載的,示例代碼中的方式就不能知足需求了。在web應用中使用Spring,須要在web.xml中添加以下配置spring
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:applicationContext.xml;
</param-value>
</context-param>
複製代碼
先了解一下ServletContext和ServletContextListener。ServletContext定義了一些方法方便Servlet和Servlet容器進行通信,在一個web應用中全部的Servlet都公用一個ServletContext,Spring在和web應用結合使用的時候,是將Spring的容器存到ServletContext中的,通俗的說就是將一個ApplicationContext存儲到ServletContext的一個Map屬性中;bash
而ServletContextListener用於監聽ServletContext一些事件。分析就從ContextLoaderListener開始。在web應用啓動讀取web.xml時,發現配置了ContextLoaderListener,而ContextLoaderListener實現了ServletContextListener接口,所以會執行ContextLoaderListener類中的contextInitialized方法,方法的具體代碼以下。app
@Override
public void contextInitialized(ServletContextEvent event) {
initWebApplicationContext(event.getServletContext());
}
複製代碼
繼續進入initWebApplicationContext方法,這個方法在其父類ContextLoader實現,根據方法名能夠看出這個方法是用於初始化一個WebApplicationContext,簡單理解就是初始化一個Web應用下的Spring容器。方法的具體代碼以下。ide
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
// 檢查servletContext是否已經存儲了一個默認名稱的WebApplicationContext
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);
}
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);
cwac.setParent(parent);
}
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
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;
}
}
複製代碼
方法的第一行就是檢查servletContext是否已經存儲了一個默認名稱的WebApplicationContext,由於在一個應用中Spring容器只能有一個,因此須要校驗,至於這個默認的名稱爲何是WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,這個變量聲明代碼以下:函數
String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT";
複製代碼
看到後面就會慢慢明白。直接關注重點代碼,代碼以下。工具
if (this.context == null) {
this.context = createWebApplicationContext(servletContext);
}
複製代碼
這裏的context是ContextLoader的一個變量,聲明代碼以下。post
private WebApplicationContext context;
複製代碼
繼續進入createWebApplicationContext方法,具體代碼以下。ui
protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
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);
}
複製代碼
這個方法主要用於建立一個WebApplicationContext對象。由於WebApplicationContext只是一個接口,不能建立對象,因此須要找到一個WebApplicationContext接口的實現類,determineContextClass方法就是用於尋找實現類,
WebApplicationContext的實現類
若是開發人員在web.xml中配置了一個參數名爲contextClass,值爲WebApplicationContext接口實現類,那就會返回這個配置的實現類Class;若是沒有配置,則會返回Spring默認的實現類XmlWebApplicationContext。直接進入determineContextClass方法體,代碼以下。
protected Class<?> determineContextClass(ServletContext servletContext) {
String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
if (contextClassName != null) {
try {
return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
}
catch (ClassNotFoundException ex) {
throw new ApplicationContextException(
"Failed to load custom context class [" + contextClassName + "]", ex);
}
}
else {
// 用於獲取Spring默認的WebApplicationContext接口實現類XmlWebApplicationContext.java
contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
try {
return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
}
catch (ClassNotFoundException ex) {
throw new ApplicationContextException(
"Failed to load default context class [" + contextClassName + "]", ex);
}
}
}
複製代碼
上面的代碼中使用了defaultStrategies,用於獲取Spring默認的WebApplicationContext接口實現類XmlWebApplicationContext.java,
defaultStrategies 聲明以下。
private static final String DEFAULT_STRATEGIES_PATH = "ContextLoader.properties";
private static final Properties defaultStrategies;
static {
// Load default strategy implementations from properties file.
// This is currently strictly internal and not meant to be customized
// by application developers.
try {
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());
}
}
複製代碼
也就是說在ContextLoader.java的路徑下,有一個ContextLoader.properties文件,查找並打開這個文件,文件內容以下。
org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext
複製代碼
這裏配置了Spring默認的WebApplicationContext接口實現類XmlWebApplicationContext.java。
回到createWebApplicationContext方法,WebApplicationContext接口實現類的Class已經找到,而後就是使用構造函數進行初始化完成WebApplicationContext對象建立。
繼續回到initWebApplicationContext方法,此時這個context就指向了剛剛建立的WebApplicationContext對象。由於XmlWebApplicationContext間接實現了ConfigurableWebApplicationContext接口,因此將會執行以下代碼。
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);
cwac.setParent(parent);
}
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
複製代碼
這裏關注重點代碼configureAndRefreshWebApplicationContext(cwac,servletContext),configureAndRefreshWebApplicationContext方法具體代碼以下。
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
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()));
}
}
wac.setServletContext(sc);
//關注重點代碼
String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
if (configLocationParam != null) {
wac.setConfigLocation(configLocationParam);
}
// 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(); } 複製代碼
仍是同樣,關注重點代碼。看一下CONFIG_LOCATION_PARAM這個常量的值是"contextConfigLocation",OK,這個就是web.xml中配置applicationContext.xml的。這個參數若是沒有配置,在XmlWebApplicationContext中是有默認值的,具體的值以下。
public static final String DEFAULT_CONFIG_LOCATION = "/WEB-INF/applicationContext.xml";
複製代碼
也就是說,若是沒有配置contextConfigLocation參數,將會使用/WEB-INF/applicationContext.xml。關注configureAndRefreshWebApplicationContext方法的最後一行代碼wac.refresh(),是否是有點眼熟,繼續跟蹤代碼,refresh方法的具體代碼以下。
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
prepareRefresh();
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
prepareBeanFactory(beanFactory);
try {
postProcessBeanFactory(beanFactory);
invokeBeanFactoryPostProcessors(beanFactory);
registerBeanPostProcessors(beanFactory);
initMessageSource();
initApplicationEventMulticaster();
onRefresh();
registerListeners();
finishBeanFactoryInitialization(beanFactory);
finishRefresh();
}catch (BeansException ex) {
logger.warn("Exception encountered during context initialization - cancelling refresh attempt", ex);
destroyBeans();
cancelRefresh(ex);
throw ex;
}
}
}
複製代碼
這個refresh方法就是《深刻理解Spring系列之四:BeanDefinition裝載前奏曲》介紹的那個refresh方法,用於完成Bean的解析、實例化及註冊。 繼續分析,回到 initWebApplicationContext 方法,將執行以下代碼。
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
複製代碼
能夠看到,這裏將初始化後的context存到了servletContext中,具體的就是存到了一個Map變量中,key值就是WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE這個常量。
使用Spring的 WebApplicationContextUtils 工具類獲取這個 WebApplicationContext 方式以下。
WebApplicationContext applicationContext = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
複製代碼