SpringMVC啟動流程原始碼解密
我們知道,SpringMVC最後是通過Tomcat來進行部署的。當在Servlet中進行進行應用部署時,主要步驟為(引用來自http://download.oracle.com/otn-pub/jcp/servlet-3.0-fr-eval-oth-JSpec/servlet-3_0-final-spec.pdf):
When a web application is deployed into a container, the following steps must be
performed, in this order, before the web application begins processing client
requests.
■ Instantiate an instance of each event listener identified by a
翻譯下:當應用部署到容器時,在應用相應客戶的請求之前,需要執行以下步驟:
建立並初始化由
對於時間監聽器,如果實現了ServletContextListener介面,那麼呼叫其contextInitialized()方法。
建立和初始化由
根據
所以在Tomcat下部署的應用,會先初始化listener,然後初始化filter,最後初始化servlet。
在我們的SpringMVC中,最簡單的web.xml配置如下
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1"> <!--告訴載入器,去這個位置去載入spring的相關配置--> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </context-param> <!--配置前端控制器--> <servlet> <servlet-name>springMvc</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!--SpringMVC配置檔案--> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>0</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springMvc</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!--解決亂碼問題的filter--> <filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>utf-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app>
現在我們根據這個配置檔案和上面的初始化流程一起來看一下,SpringMVC是如何來一步步啟動容器,並載入相關資訊的。
初始化Listener
我們這裡定義的的listener類是 ContextLoaderListener ,我們看一下具體類定義
/** * Bootstrap listener to start up and shut down Spring's root {@link WebApplicationContext}. * Simply delegates to {@link ContextLoader} as well as to {@link ContextCleanupListener}. * * <p>As of Spring 3.1, {@code ContextLoaderListener} supports injecting the root web * application context via the {@link #ContextLoaderListener(WebApplicationContext)} * constructor, allowing for programmatic configuration in Servlet 3.0+ environments. * See {@link org.springframework.web.WebApplicationInitializer} for usage examples. * * @author Juergen Hoeller * @author Chris Beams * @since 17.02.2003 * @see #setContextInitializers * @see org.springframework.web.WebApplicationInitializer */ public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
ContextLoaderListener 類繼承了 ContextLoader 類並且實現了 ServletContextListener 介面,按照啟動程式,會呼叫其 contextInitialized() 方法
/**
* Initialize the root web application context.
*/
//初始化根應用上下文
@Override
public void contextInitialized(ServletContextEvent event) {
initWebApplicationContext(event.getServletContext());
}
那麼我們現在再看一下這個應用上下文的初始化過程
//初始化web應用的上下文
//ServletContext官方叫servlet上下文。伺服器會為每一個工程建立一個物件,這個物件就是ServletContext物件。這個物件全域性唯一,而且工程內部的所有servlet都共享這個物件。所以叫全域性應用程式共享物件。
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
/*
首先通過WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
這個String型別的靜態變數獲取一個根IoC容器,根IoC容器作為全域性變數
儲存在application物件中,如果存在則有且只能有一個
如果在初始化根WebApplicationContext即根IoC容器時發現已經存在
則直接丟擲異常,因此web.xml中只允許存在一個ContextLoader類或其子類的物件
*/
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.
//如果context不存在,則進行建立
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);
}
/**
* 配置並重新整理應用的根IOC容器,這裡會進行bean的建立和初始化工作。這裡面最終會呼叫
* {@link org.springframework.context.support.AbstractApplicationContext#refresh refresh()方法}
* 並且IOC容器中的bean類會被放在application中
*/
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
//以屬性的配置方式將application配置servletContext中,因為servletContext是整個應用唯一的,所以可以根據key值獲取到application,從而能夠獲取到應用的所有資訊
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
....
}
可以看到,initWebApplicationContext() 方法的整個執行過程都是為了建立應用的上下文,即根IOC容器。並且以 setAttribute 的方式將應用上下文設定到了servletContext中,這樣在整個應用中都可以使用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()));
}
}
//將ServletContext設定到application的屬性中
wac.setServletContext(sc);
//獲取web.xml中配置的contextConfigLocation引數值
String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
if (configLocationParam != null) {
//將web.xml的配置資訊設定到application中
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);
//呼叫應用的refresh方法,進行IOC容器的裝載
wac.refresh();
}
我們跟蹤一下debug程式碼,看看實際的的資訊。
我們進入到 refresh() 方法中
可以看到,在refresh中完成了對於IOC容器中bean類的載入處理。
到此為止,SpringMVC已經完成了對於由
初始化Filter
在完成了對於 listener 的初始化操作以後,會進行 filter 的建立和初始化操作。我們這裡使用的是 CharacterEncodingFilter 。我們先看一下這個類的具體類圖資訊。
因為其實現了 Filter 介面,所以會呼叫其對應的 init(FilterConfig filterConfig) 方法。我們在其父類 GenericFilterBean 中找到了該方法的實現。
@Override
public final void init(FilterConfig filterConfig) throws ServletException {
this.filterConfig = filterConfig;
//將設定的初始化引數資訊設定到pvs中
PropertyValues pvs = new FilterConfigPropertyValues(filterConfig, this.requiredProperties);
if (!pvs.isEmpty()) {
try {
//將具體的filter類進行包裝
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
//建立對應的資源載入類
ResourceLoader resourceLoader = new ServletContextResourceLoader(filterConfig.getServletContext());
Environment env = this.environment;
if (env == null) {
env = new StandardServletEnvironment();
}
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, env));
initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
String msg = "Failed to set bean properties on filter '" +
filterConfig.getFilterName() + "': " + ex.getMessage();
logger.error(msg, ex);
throw new NestedServletException(msg, ex);
}
}
//交由子類來實現
initFilterBean();
...
}
這裡並未進行任何的初始化操作。其實Filter的主要作用還是在有請求過來時,進行的 doFilter() 中的處理,在啟動階段,處理比較少。
Servlet的初始化
web應用啟動的最後一個步驟就是建立和初始化 Servlet ,我們就從我們使用的 DispatcherServlet 這個類來進行分析,這個類是前端控制器,主要用於分發使用者請求到具體的實現類,並返回具體的響應資訊。
根據類圖可以看到,DispatchServlet 實現了 Servlet 介面,所以按照載入過程,最終會呼叫其 init(ServletConfig config) 方法。我們從 DispatchServlet 中尋找 init(ServletConfig config) 方法的實現,會發現該方法不存在,那麼我們繼續向上查詢,在其父類中去尋找,最終在 GenericServlet 中找到了方法
public void init(ServletConfig config) throws ServletException {
this.config = config;
//交由子類來實現。
this.init();
}
我們在 HttpServletBean 中找到了 init() 方法的具體實現。
public final void init() throws ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Initializing servlet '" + getServletName() + "'");
}
// Set bean properties from init parameters.
//設定屬性資訊
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
//將具體的實現來進行包裝,使用了包裝者模式
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
initBeanWrapper(bw);
//將web.xml裡面設定的屬性資訊設定到bw中
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
if (logger.isErrorEnabled()) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
}
throw ex;
}
}
// Let subclasses do whatever initialization they like.
//由子類來實現
initServletBean();
if (logger.isDebugEnabled()) {
logger.debug("Servlet '" + getServletName() + "' configured successfully");
}
}
其中,對於 initServletBean() 方法則又交給了子類來處理,我們最終在 FrameworkServlet 類中找到了對應的實現
@Override
protected final void initServletBean() throws ServletException {
getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'");
if (logger.isInfoEnabled()) {
logger.info("FrameworkServlet '" + getServletName() + "': initialization started");
}
long startTime = System.currentTimeMillis();
try {
//初始化web應用容器
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet();
}
...
}
這裡面主要是進行了web應用容器上下文的建立,並進行了初始化工作。我們跟蹤一下初始化的具體流程
protected WebApplicationContext () {
//獲取到根IOC容器
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;
if (this.webApplicationContext != null) {
// A context instance was injected at construction time -> use it
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
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) {
//將根IOC容器設定為servlet的IOC容器的父類
//如果當前Servlet存在一個WebApplicationContext即子IoC容器
// 並且上文獲取的根IoC容器存在,則將根IoC容器作為子IoC容器的父容器
cwac.setParent(rootContext);
}
//配置並重新整理子容器,載入子IOC容器中對應的bean實體類
configureAndRefreshWebApplicationContext(cwac);
}
}
}
if (wac == null) {
// No context instance was injected at construction time -> see if one
// has been registered in the servlet context. If one exists, it is assumed
// that the parent context (if any) has already been set and that the
// user has performed any initialization such as setting the context id
//如果當前Servlet中不存在子IOC容器,則去查詢
wac = findWebApplicationContext();
}
if (wac == null) {
// No context instance is defined for this servlet -> create a local one
//如果查詢不到,則去建立一個
wac = createWebApplicationContext(rootContext);
}
...
return wac;
}
程式碼比較長,其實 initWebApplicationContext() 函式的主要作用就是初始化一個Web類的IOC容器。通過debug跟蹤的時候,我們看到最終會執行到 createWebApplicationContext() 這個方法,繼續跟蹤程式碼的執行流程
protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
Class<?> contextClass = getContextClass();
...
//根據類資訊初始化一個ConfigurableWebApplicationContext物件
ConfigurableWebApplicationContext wac =
(ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
//設定web上下文的環境資訊
wac.setEnvironment(getEnvironment());
//設定其父類為根IOC容器,根IOC容器是整個應用唯一的。
wac.setParent(parent);
//設定其具體的配置資訊的位置,這裡是 classpath:spring-mvc.xml
String configLocation = getContextConfigLocation();
if (configLocation != null) {
wac.setConfigLocation(configLocation);
}
//配置並重新整理web應用的IOC容器
configureAndRefreshWebApplicationContext(wac);
return wac;
}
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
....
//配置容器的相關資訊
wac.setServletContext(getServletContext());
wac.setServletConfig(getServletConfig());
wac.setNamespace(getNamespace());
//配置容應用的載入監聽器
wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));
// 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(getServletContext(), getServletConfig());
}
postProcessWebApplicationContext(wac);
//應用初始化資訊
applyInitializers(wac);
//重新整理載入裡面的Bean實體類
wac.refresh();
}
這裡可以看到,在這個其實主要是根據配置檔案資訊進行類的載入工作,並且配置了一個容器載入資訊的監聽器 SourceFilteringListener。在最後通過 refresh() 方法進行了容器中實體類的載入過程。這個 refresh() 方法和我們在 Listener 中實現類的初始化過程使用的是同一個方法。到此為止,在我們應用中配置的所有的類都能夠掃描到,並且配置到了我們的IOC容器中。
因為我們配置了相關的容器載入的監聽器,在refresh()方法中呼叫 finishRefresh() 方法時,傳送對應的容器載入完成廣播資訊,從而能夠呼叫我們所註冊的監聽器 SourceFilteringListener 。我們看一下里面的呼叫邏輯
public SourceFilteringListener(Object source, ApplicationListener<?> delegate) {
this.source = source;
this.delegate = (delegate instanceof GenericApplicationListener ?
(GenericApplicationListener) delegate : new GenericApplicationListenerAdapter(delegate));
}
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event.getSource() == this.source) {
onApplicationEventInternal(event);
}
}
protected void onApplicationEventInternal(ApplicationEvent event) {
if (this.delegate == null) {
throw new IllegalStateException(
"Must specify a delegate object or override the onApplicationEventInternal method");
}
/**
* 這裡的delegate,是傳入的具體的代理類
*/
this.delegate.onApplicationEvent(event);
}
可以看到最後,其實是呼叫了我們建立這個類時,傳入的 ContextRefreshListener 這個物件的 onApplicationEvent 這個方法。我們繼續看一眼這個類
private class ContextRefreshListener implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
//最終呼叫了FrameworkServlet的onApplicationEvent方法
FrameworkServlet.this.onApplicationEvent(event);
}
}
繼續跟蹤:
public void onApplicationEvent(ContextRefreshedEvent event) {
this.refreshEventReceived = true;
synchronized (this.onRefreshMonitor) {
//呼叫了onRefresh方法
onRefresh(event.getApplicationContext());
}
}
protected void onRefresh(ApplicationContext context) {
// For subclasses: do nothing by default.
}
可以看到,最後的 onRefresh() 是由子類來實現的,也就是我們的 DispatcherServlet 類。
終於到我們的正主出場了。。
我們看看我們的正主到底幹了個什麼活
//context為DispatcherServlet建立的一個IoC子容器
@Override
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}
//初始化servlet使用的策略資訊,子類可以通過覆寫該方法類增加更多的呃策略方法
protected void initStrategies(ApplicationContext context) {
//初始化MultipartResolver,可以支援檔案的上傳
initMultipartResolver(context);
//初始化本地解析器
initLocaleResolver(context);
//初始化主題解析器
initThemeResolver(context);
//處理器對映器,將請求和方法進行對映關聯
initHandlerMappings(context);
//處理器介面卡
initHandlerAdapters(context);
//處理器異常解析器
initHandlerExceptionResolvers(context);
//
initRequestToViewNameTranslator(context);
//檢視解析器
initViewResolvers(context);
//FlashMap管理器
initFlashMapManager(context);
}
可以看到裡面主要是初始化了我們的所使用到的一些解析器和處理器等。當接收到請求後,就可以根據這些解析器來進行請求的解析處理、方法的呼叫、異常的處理等等。
到此為止,Servlet的初始化工作就整個完成了。想當的複雜,主要是將很多的方法實現在父類中進行了處理。層級比較複雜,需要一點點跟蹤分析。
本文由 開了肯 釋出!