Spring基礎系列-容器啟動流程(1)
概述
我說的容器啟動流程涉及兩種情況,SSM
開發模式和Springboot
開發模式。
SSM
開發模式中,需要配置web.xml
檔案用作啟動配置檔案,而Springboot
開發模式中由main
方法直接啟動。
下面是web
專案中容器啟動的流程,起點是web.xml
中配置的ContextLoaderListener
監聽器。
呼叫流程圖(右鍵可檢視大圖)
流程解析
Tomcat
伺服器啟動時會讀取專案中web.xml
中的配置項來生成ServletContext
,在其中註冊的ContextLoaderListener
是ServletContextListener
介面的實現類,它會時刻監聽ServletContext
ServletContext
建立的時候會觸發其contextInitialized()
初始化方法的執行。而Spring
容器的初始化操作就在這個方法之中被觸發。
原始碼1-來自:ContextLoaderListener
/** * Initialize the root web application context. */ @Override public void contextInitialized(ServletContextEvent event) { initWebApplicationContext(event.getServletContext()); }
ContextLoaderListener
是啟動和銷燬Spring
的root WebApplicationContext
(根web容器
或者根web應用上下文
)的引導器,其中實現了ServletContextListener
的contextInitialized
容器初始化方法與contextDestoryed
銷燬方法,用於引導根web容器
的建立和銷燬。
上面方法中contextInitialized
就是初始化根web容器
的方法。其中呼叫了initWebApplicationContext
方法進行Spring web容器
的具體建立。
原始碼2-來自:ContextLoader
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) { //SpringIOC容器的重複性建立校驗 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"); } //記錄Spring容器建立開始時間 long startTime = System.currentTimeMillis(); try { // Store context in local instance variable, to guarantee that // it is available on ServletContext shutdown. if (this.context == null) { //建立Spring容器例項 this.context = createWebApplicationContext(servletContext); } if (this.context instanceof ConfigurableWebApplicationContext) { ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context; if (!cwac.isActive()) { //容器只有被重新整理至少一次之後才是處於active(啟用)狀態 if (cwac.getParent() == null) { //此處是一個空方法,返回null,也就是不設定父級容器 ApplicationContext parent = loadParentContext(servletContext); cwac.setParent(parent); } //重點操作:配置並重新整理容器 configureAndRefreshWebApplicationContext(cwac, servletContext); } } //將建立完整的Spring容器作為一條屬性新增到Servlet容器中 servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context); ClassLoader ccl = Thread.currentThread().getContextClassLoader(); if (ccl == ContextLoader.class.getClassLoader()) { //如果當前執行緒的類載入器是ContextLoader類的類載入器的話,也就是說如果是當前執行緒載入了ContextLoader類的話,則將Spring容器在ContextLoader例項中保留一份引用 currentContext = this.context; } else if (ccl != null) { //新增一條ClassLoader到Springweb容器的對映 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
和Spring root ApplicationContext
的關係:後者只是前者的一個屬性,前者包含後者。
知識點:
ServletContext
表示的是一整個應用,其中囊括應用的所有內容,而Spring
只是應用所採用的一種框架。從ServletContext
的角度來看,Spring
其實也算是應用的一部分,屬於和我們編寫的程式碼同級的存在,只是相對於我們編碼人員來說,Spring
是作為一種即存的編碼架構而存在的,即我們將其看作我們編碼的基礎,或者看作應用的基礎部件。雖然是基礎部件,但也是屬於應用的一部分。所以將其設定到ServletContext
中,而且是作為一個單一屬性而存在,但是它的作用卻是很大的。
原始碼中WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
的值為:WebApplicationContext.class.getName() + ".ROOT"
,這個正是Spring容器
在Servlet容器
中的屬性名。
在這段原始碼中主要是概述Spring
容器的建立和初始化,分別由兩個方法實現:createWebApplicationContext方法和configureAndRefreshWebApplicationContext方法。
首先,我們需要建立Spring容器
,我們需要決定使用哪個容器實現。
原始碼3-來自:ContextLoader
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);
}
//我們在web.xml中以 <context-param> 的形式設定contextclass引數來指定使用哪個容器實現類,
//若未指定則使用預設的XmlWebApplicationContext,其實這個預設的容器實現也是預先配置在一個
//叫ContextLoader.properties檔案中的
protected Class<?> determineContextClass(ServletContext servletContext) {
//獲取Servlet容器中配置的系統引數contextClass的值,如果未設定則為null
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 {
//獲取預先配置的容器實現類
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);
}
}
}
BeanUtils
是Spring
封裝的反射實現,instantiateClass
方法用於例項化指定類。
我們可以在web.xml
中以<context-param>
的形式設定contextclass
引數手動決定應用使用哪種Spring容器
,但是一般情況下我們都遵循Spring
的預設約定,使用ContextLoader.properties
中配置的org.springframework.web.context.WebApplicationContext
的值來作為預設的Spring容器
來建立。
原始碼4-來自:ContextLoader.properties
# Default WebApplicationContext implementation class for ContextLoader.
# Used as fallback when no explicit context implementation has been specified as context-param.
# Not meant to be customized by application developers.
org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext
可見,一般基於Spring
的web
應用預設使用的都是XmlWebApplicationContext
作為容器實現類的。
到此位置容器例項就建立好了,下一步就是配置和重新整理了。
原始碼5-來自:ContextLoader
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()));
}
}
//在當前Spring容器中保留對Servlet容器的引用
wac.setServletContext(sc);
//設定web.xml中配置的contextConfigLocation引數值到當前容器中
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
//在容器重新整理之前,提前進行屬性資源的初始化,以備使用,將ServletContext設定為servletContextInitParams
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
}
//取得web.xml中配置的contextInitializerClasses和globalInitializerClasses對應的初始化器,並執行初始化操作,需自定義初始化器
customizeContext(sc, wac);
//重新整理容器
wac.refresh();
}
首先將ServletContext
的引用置入Spring容器
中,以便可以方便的訪問ServletContext
;然後從ServletContext
中找到contextConfigLocation
引數的值,配置是在web.xml
中以<context-param>
形式配置的。
知識點:
在
Spring
中凡是以<context-param>
配置的內容都會在web.xml
載入的時候優先儲存到ServletContext
之中,我們可以將其看成ServletContext
的配置引數,將引數配置到ServletContext
中後,我們就能方便的獲取使用了,就如此處我們就能直接從ServletContext
中獲取contextConfigLocation
的值,用於初始化Spring容器
。在
Java
的web
開發中,尤其是使用Spring
輔助開發的情況下,基本就是一個容器對應一套配置,這套配置就是用於初始化容器的。ServletContext
對應於<context-param>
配置,Spring容器
對應applicationContext.xml
配置,這個配置屬於預設的配置,如果自定義名稱就需要將其配置到<context-param>
中備用了,還有DispatchServlet
的Spring容器
對應spring-mvc.xml
配置檔案。
Spring容器
的environment
表示的是容器執行的基礎環境配置,其中儲存的是Profile
和Properties
,其initPropertySources
方法是在ConfigurableWebEnvironment
介面中定義的,是專門用於web應用
中來執行真實屬性資源與佔位符資源(StubPropertySource
)的替換操作的。
知識點:
StubPropertySource
就是一個佔位用的例項,在應用上下文建立時,當實際屬性資源無法及時初始化時,臨時使用這個例項進行佔位,等到容器重新整理的時候執行替換操作。
原始碼6-來自:StandardServletEnvironment
@Override
public void initPropertySources(@Nullable ServletContext servletContext, @Nullable ServletConfig servletConfig) {
WebApplicationContextUtils.initServletPropertySources(getPropertySources(), servletContext, servletConfig);
}
原始碼7-來自:WebApplicationContextUtils
public static void initServletPropertySources(MutablePropertySources sources,
@Nullable ServletContext servletContext, @Nullable ServletConfig servletConfig) {
Assert.notNull(sources, "'propertySources' must not be null");
String name = StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME;
if (servletContext != null && sources.contains(name) && sources.get(name) instanceof StubPropertySource) {
sources.replace(name, new ServletContextPropertySource(name, servletContext));
}
name = StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME;
if (servletConfig != null && sources.contains(name) && sources.get(name) instanceof StubPropertySource) {
sources.replace(name, new ServletConfigPropertySource(name, servletConfig));
}
}
從上面的原始碼中可以看出,這裡只進行了有關Servlet
的佔位資源的替換操作:ServletContext
和ServletConfig
。
上面原始碼中customizeContext
方法的目的是在重新整理容器之前對容器進行自定義的初始化操作,需要我們實現ApplicationContextInitializer<C extends ConfigurableApplicationContext>
介面,然後將其配置到web.xml
中即可生效。
原始碼8-來自:ContextLoader
protected void customizeContext(ServletContext sc, ConfigurableWebApplicationContext wac) {
//獲取初始化器類集合
List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>> initializerClasses =
determineContextInitializerClasses(sc);
for (Class<ApplicationContextInitializer<ConfigurableApplicationContext>> initializerClass : initializerClasses) {
Class<?> initializerContextClass =
GenericTypeResolver.resolveTypeArgument(initializerClass, ApplicationContextInitializer.class);
if (initializerContextClass != null && !initializerContextClass.isInstance(wac)) {
throw new ApplicationContextException(String.format(
"Could not apply context initializer [%s] since its generic parameter [%s] " +
"is not assignable from the type of application context used by this " +
"context loader: [%s]", initializerClass.getName(), initializerContextClass.getName(),
wac.getClass().getName()));
}
//例項化初始化器並新增到集合中
this.contextInitializers.add(BeanUtils.instantiateClass(initializerClass));
}
//排序並執行,編號越小越早執行
AnnotationAwareOrderComparator.sort(this.contextInitializers);
for (ApplicationContextInitializer<ConfigurableApplicationContext> initializer : this.contextInitializers) {
initializer.initialize(wac);
}
}
protected List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>>
determineContextInitializerClasses(ServletContext servletContext) {
List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>> classes =
new ArrayList<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>>();
//通過<context-param>屬性配置globalInitializerClasses獲取全域性初始化類名
String globalClassNames = servletContext.getInitParameter(GLOBAL_INITIALIZER_CLASSES_PARAM);
if (globalClassNames != null) {
for (String className : StringUtils.tokenizeToStringArray(globalClassNames, INIT_PARAM_DELIMITERS)) {
classes.add(loadInitializerClass(className));
}
}
//通過<context-param>屬性配置contextInitializerClasses獲取容器初始化類名
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;
}
customizeContext
的主要作用就是執行應用上下文初始化器(ApplicationContextInitializer
),這些初始化器的作用就是在容器重新整理之前資訊一輪預初始化操作。我們可以通過手動實現ApplicationContextInitializer
的方式自定義預初始化邏輯,然後通過<context-param>
配置到web.xml
中,這些配置就會在此處被載入處理並執行。
知識點:
ApplicationContextInitializer
是Spring
中定義的擴充套件點之一,可以通過手動實現該介面的方式來自定義預初始化邏輯,在重新整理操作之前來執行。 一般用於註冊屬性資源,或者啟用
profiles
等操作。
到達refresh
操作我們先暫停。refresh
操作是容器初始化的操作。是通用操作,而到達該點的方式確實有多種,每種就是一種Spring
的開發方式。
除了此處的web
開發方式,還有Springboot
開發方式,貌似就兩種。。。下面說說Springboot
啟動的流程,最後統一說refresh
流程。