SpringMVC是一款Web MVC架構。 它跟Struts架構類似,是目前主流的Web MVC架構之一
一、SpringMVC的由來
早期 Java Web 的開發中,把顯示層、控制層、資料層的操作全部交給 JSP 或者 JavaBean 來進行處理,我們稱之為 Model1:
然而出現了很多的弊端,比如:
- JSP 和 Java Bean 之間嚴重耦合,Java 代碼和 HTML 代碼也耦合在了一起
- 要求開發者不僅要掌握 Java ,還要有高超的前端水準
- 前端和後端互相依賴,前端需要等待後端完成,後端也依賴前端完成,才能進行有效的測試
随後出現servlet,就有了早期的MVC模式
首先使用者請求到servlet,然後根據請求調用響應的JavaBean,并把所有的顯示交給Jsp去處理,這樣就稱之為mvc模式:
- M代表模型(Model):資料、bean
- V代表視圖(View):網頁,jsp....展示模型中的資料
- C代表控制器(Controller):把不同的資料(Model),顯示在不同的視圖(View)上,Servlet 扮演的就是這樣的角色
為解決持久層中一直未處理好的資料庫事務的程式設計,又為了迎合 NoSQL 的強勢崛起,Spring MVC 給出了方案
二、初始化過程
當一個web應用部署tomcat時,在接收使用者請求之前,會進行以下初始化過程
- 部署在web.xml檔案裡由
元素标記事件的監聽器會被建立和初始化<listener>
- 對于所有事件監聽器,如果實作了
接口,将會執行其實作的ServletContextListener
方法contextInitialized()
- 部署描述檔案由
元素标記的過濾器會被建立和初始化,并調用其<filter>
init()
- 部署在描述檔案由
元素标記的servlet會根據<servlet>
的權值按順序建立并初始化,并調用其<load-on-start>
init()
三、啟動流程
以下為一個常見的
web.xml
配置進行SpringMVC啟動流程分析
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>spring-demo</display-name>
<!--全局變量-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<!--監聽器-->
<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>
<!--dispatcherServlet-->
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
1、Listener初始化過程
首先定義了一個
<context-param>
, 用于配置一個全局的變量,
<context-param>
标簽内容讀取後會放進application中,作為web應用的全局變量使用,接下來建立
listener
時會使用這個變量,是以,web應用在啟動時,會先讀取這個變量,之後才進行下一步,接着定義了一個
ContextLoaderListener
的
listener
,這個listener的源碼為
public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
public ContextLoaderListener() {
}
public ContextLoaderListener(WebApplicationContext context) {
super(context);
}
public void contextInitialized(ServletContextEvent event) {
this.initWebApplicationContext(event.getServletContext());
}
public void contextDestroyed(ServletContextEvent event) {
this.closeWebApplicationContext(event.getServletContext());
ContextCleanupListener.cleanupAttributes(event.getServletContext());
}
}
ContextLoaderListener類繼承了ContextLoader類并實作了ServletContextListener接口,首先看一下前面講述的ServletContextListener接口源碼:
public interface ServletContextListener extends EventListener {
void contextInitialized(ServletContextEvent var1);
void contextDestroyed(ServletContextEvent var1);
}
該接口隻有兩個方法
contextInitialized
和
contextDestroyed
,這裡采用的是觀察者模式,也稱為為訂閱-釋出模式,實作了該接口的listener會向釋出者進行訂閱,當Web應用初始化或銷毀時會分别調用上述兩個方法,
ContextLoaderListener的contextInitialized()
方法直接調用了
initWebApplicationContext()
方法,這個方法是繼承自ContextLoader類,通過函數名可以知道,該方法是用于初始化Web應用上下文,即IOC容器,這裡使用的是代理模式,繼續檢視ContextLoader類的initWebApplicationContext()方法的源碼如下:
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
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!");
} else {
servletContext.log("Initializing Spring root WebApplicationContext");
Log logger = LogFactory.getLog(ContextLoader.class);
if (logger.isInfoEnabled()) {
logger.info("Root WebApplicationContext: initialization started");
}
long startTime = System.currentTimeMillis();
try {
if (this.context == null) {
this.context = this.createWebApplicationContext(servletContext);
}
if (this.context instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext)this.context;
if (!cwac.isActive()) {
if (cwac.getParent() == null) {
ApplicationContext parent = this.loadParentContext(servletContext);
cwac.setParent(parent);
}
this.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.isInfoEnabled()) {
long elapsedTime = System.currentTimeMillis() - startTime;
logger.info("Root WebApplicationContext initialized in " + elapsedTime + " ms");
}
return this.context;
} catch (Error | RuntimeException var8) {
logger.error("Context initialization failed", var8);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, var8);
throw var8;
}
}
}
initWebApplicationContext()方法如上講述,主要目的就是建立root WebApplicationContext對象即
IOC容器
,其中比較重要的就是,整個Web應用如果存在IOC容器則有且隻能有一個,
根IOC容器
作為全局變量存儲在
ServletContext
即
application
對象中。将
根IOC容器
放入到application對象之前進行了
IOC容器
的配置和重新整理操作,調用了
configureAndRefreshWebApplicationContext()
方法,該方法源碼如下:
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
String configLocationParam;
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
configLocationParam = sc.getInitParameter("contextId");
if (configLocationParam != null) {
wac.setId(configLocationParam);
} else {
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + ObjectUtils.getDisplayString(sc.getContextPath()));
}
}
wac.setServletContext(sc);
configLocationParam = sc.getInitParameter("contextConfigLocation");
if (configLocationParam != null) {
wac.setConfigLocation(configLocationParam);
}
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
((ConfigurableWebEnvironment)env).initPropertySources(sc, (ServletConfig)null);
}
this.customizeContext(sc, wac);
wac.refresh();
}
比較重要的就是擷取到了
web.xml中的<context-param>
标簽配置的全局變量
contextConfigLocation
,并最後一行調用了
refresh()
方法,
ConfigurableWebApplicationContext
是一個接口,通過對常用實作類
ClassPathXmlApplicationContext
逐層查找後可以找到一個抽象類AbstractApplicationContext實作了
refresh()
方法,其源碼如下
public void refresh() throws BeansException, IllegalStateException {
synchronized(this.startupShutdownMonitor) {
this.prepareRefresh();
ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
this.prepareBeanFactory(beanFactory);
try {
this.postProcessBeanFactory(beanFactory);
this.invokeBeanFactoryPostProcessors(beanFactory);
this.registerBeanPostProcessors(beanFactory);
this.initMessageSource();
this.initApplicationEventMulticaster();
this.onRefresh();
this.registerListeners();
this.finishBeanFactoryInitialization(beanFactory);
this.finishRefresh();
} catch (BeansException var9) {
if (this.logger.isWarnEnabled()) {
this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var9);
}
this.destroyBeans();
this.cancelRefresh(var9);
throw var9;
} finally {
this.resetCommonCaches();
}
}
}
該方法主要用于建立并初始化
contextConfigLocation
類配置的xml檔案中的
Bean
,是以,如果我們在配置Bean時出錯,在Web應用
啟動時就會抛出異常
,而不是等到運作時才抛出異常
整個
ContextLoaderListener
類的啟動過程到此就結束了,可以發現,建立
ContextLoaderListener
是比較核心的一個步驟,
主要工作就是為了建立根IoC容器
并使用特定的key将其放入到application對象中,供整個Web應用使用,由于在ContextLoaderListener類中構造的
IOC容器
配置的Bean是全局共享的,是以,在辨別的
contextConfigLocation
的xml配置檔案一般包括: 資料庫DataSource、DAO層、Service層、事務等相關Bean
2、Filter的初始化
在監聽器
listener
初始化完成後,接下來會進行filter的初始化操作,
GenericFilterBean
是任何類型的過濾器的一個比較友善的超類,這個類主要實作的就是從
web.xml
檔案中取得
init-param中
設定的值,然後對Filter進行初始化(當然,其子類可以覆寫
init()
方法)。
Filter的生命周期如下:
- void init(FilterConfig config): 用于完成Filter 的初始化
- void destroy(): 用于Filter 銷毀前,完成某些資源的回收
- void doFilter(ServletRequest request, ServletResponse response,FilterChain chain): 實作過濾功能,該方法就是對每個請求及響應增加的額外處理。 過濾器Filter也具有生命周期:init()->doFilter()->destroy(),由部署檔案中的filter元素驅動
3、Servlet的初始化
Web應用啟動的最後一個步驟就是建立和初始化相關
Servlet
,在開發中常用的Servlet就是
DispatcherServlet
類前端控制器,前端控制器作為中央控制器是整個Web應用的核心,用于擷取分發使用者請求并傳回響應
DispatcherServlet
類的間接父類實作了
Servlet
接口,是以其本質上依舊是一個
Servlet
。
DispatcherServlet
設計很巧妙,上層父類不同程度的實作了相關接口的部分方法,并留出了相關方法用于子類覆寫,将不變的部分統一實作,将變化的部分預留方法用于子類實作
具體順序圖
DispatcherServelt
類的本質是
Servlet
,通過文章開始的講解可知,在Web應用部署到容器後進行
Servlet
初始化時會調用相關的
init(ServletConfig)
方法,是以,
DispatchServlet
類的初始化過程也由該方法開始。上述調用邏輯中比較重要的就是
FrameworkServlet
抽象類中的
initServletBean()
方法、
initWebApplicationContext()
方法以及
DispatcherServlet
類中的
onRefresh()
a、initServletBean()
protected final void initServletBean() throws ServletException {
this.getServletContext().log("Initializing Spring " + this.getClass().getSimpleName() + " '" + this.getServletName() + "'");
if (this.logger.isInfoEnabled()) {
this.logger.info("Initializing Servlet '" + this.getServletName() + "'");
}
long startTime = System.currentTimeMillis();
try {
this.webApplicationContext = this.initWebApplicationContext();
this.initFrameworkServlet();
} catch (RuntimeException | ServletException var4) {
this.logger.error("Context initialization failed", var4);
throw var4;
}
if (this.logger.isDebugEnabled()) {
String value = this.enableLoggingRequestDetails ? "shown which may lead to unsafe logging of potentially sensitive data" : "masked to prevent unsafe logging of potentially sensitive data";
this.logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails + "': request parameters and headers will be " + value);
}
if (this.logger.isInfoEnabled()) {
this.logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
}
}
該方法是重寫了父類
HttpServletBean
抽象類的
initServletBean()
HttpServletBean
抽象類在執行
init()
方法時會調用
initServletBean()
方法,由于多态的特性,最終會調用其子類
FrameworkServlet
initServletBean()
方法。該方法由final辨別,子類就不可再次重寫了。該方法中比較重要的就是
initWebApplicationContext()
方法的調用,該方法仍由
FrameworkServlet
抽象類實作
protected WebApplicationContext initWebApplicationContext() {
WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(this.getServletContext());
WebApplicationContext wac = null;
if (this.webApplicationContext != null) {
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext)wac;
if (!cwac.isActive()) {
if (cwac.getParent() == null) {
cwac.setParent(rootContext);
}
this.configureAndRefreshWebApplicationContext(cwac);
}
}
}
if (wac == null) {
wac = this.findWebApplicationContext();
}
if (wac == null) {
wac = this.createWebApplicationContext(rootContext);
}
if (!this.refreshEventReceived) {
synchronized(this.onRefreshMonitor) {
this.onRefresh(wac);
}
}
if (this.publishContext) {
String attrName = this.getServletContextAttributeName();
this.getServletContext().setAttribute(attrName, wac);
}
return wac;
}
該方法的主要作用同樣是建立一個
WebApplicationContext
對象,即
IOC容器
,不過前文講過每個Web應用最多隻能存在一個
IOC容器
,這裡建立的則是特定
Servlet
擁有的
子IOC容器
父子IOC容器的通路特性
父子容器類似于類的繼承關系,子類可以通路父類中的成員變量,而父類不可通路子類的成員變量,同樣的,子容器可以通路父容器中定義的Bean,但父容器無法通路子容器定義的Bean,
根IOC容器
做為全局共享的
IOC容器
放入Web應用需要共享的Bean,而
子IOC容器
根據需求的不同,放入不同的Bean,這樣能夠做到
隔離
,保證系統的
安全性
DispatcherServlet
類的子
IOC容器
建立過程,如果目前Servlet存在一個
IOC容器
則為其設定
根IOC容器
作為其父類,并配置重新整理該容器,用于構造其定義的Bean,這裡的方法與前文講述的
根IOC容器
類似,同樣會讀取使用者在
web.xml
中配置的
<servlet>
中的
<init-param>
值,用于查找相關的xml配置檔案用于構造定義的Bean。如果目前Servlet不存在一個子IoC容器就去查找一個,如果仍然沒有查找到則調用
createWebApplicationContext()方法去建立
protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
Class<?> contextClass = this.getContextClass();
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException("Fatal initialization error in servlet with name '" + this.getServletName() + "': custom WebApplicationContext class [" + contextClass.getName() + "] is not of type ConfigurableWebApplicationContext");
} else {
ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext)BeanUtils.instantiateClass(contextClass);
wac.setEnvironment(this.getEnvironment());
wac.setParent(parent);
String configLocation = this.getContextConfigLocation();
if (configLocation != null) {
wac.setConfigLocation(configLocation);
}
this.configureAndRefreshWebApplicationContext(wac);
return wac;
}
}
該方法用于建立一個
子IOC容器
并将
根IOC容器
做為其父容器,接着進行配置和重新整理操作用于構造相關的Bean。至此,
根IOC容器
以及相關
Servlet
子IOC容器
已經配置完成,子容器中管理的Bean一般隻被該Servlet使用,是以,其中管理的Bean一般是
局部
的,如SpringMVC中需要的各種重要元件,包括Controller、Interceptor、Converter、ExceptionResolver等
當
IOC子容器
構造完成後調用了
onRefresh()
方法,該方法的調用與
initServletBean()
方法的調用相同,由父類調用但具體實作由子類覆寫,調用
onRefresh()
方法時将前文建立的IOC子容器作為參數傳入
protected void onRefresh(ApplicationContext context) {
this.initStrategies(context);
}
protected void initStrategies(ApplicationContext context) {
this.initMultipartResolver(context);
this.initLocaleResolver(context);
this.initThemeResolver(context);
this.initHandlerMappings(context);
this.initHandlerAdapters(context);
this.initHandlerExceptionResolvers(context);
this.initRequestToViewNameTranslator(context);
this.initViewResolvers(context);
this.initFlashMapManager(context);
}
onRefresh()
initStrategies()
方法,源碼如上,通過函數名可以判斷,該方法用于初始化建立
multipartResovle
來支援圖檔等檔案的上傳、本地化解析器、主題解析器、HandlerMapping處理器映射器、
HandlerAdapter
處理器擴充卡、異常解析器、視圖解析器、flashMap管理器等,這些元件都是SpringMVC開發中的重要元件,相關元件的初始化建立過程均在此完成。
到此,初始化就全部結束了
四、總結
在Spring的web容器啟動時會去讀取
web.xml
檔案,相關啟動順序為:
<context-param> --> <listener> --> <filter> --> <servlet>
,具體為:
- 1、解析
鍵值對<context-param>
- 2、建立一個application對象即ServletContext,servlet上下文,用于全局共享
- 3、将
鍵值對放入ServletContext中,web應用全局共享<context-param>
- 4、讀取
标簽,建立監聽器,一般使用<listener>
,如果使用了ContextLoaderListener
,Spring就會建立一個WebApplicationContext對象,這個就是ContextLoaderListener
,IOC容器
建立的ContextLoaderListener
是全局共享的,并将其放在IOC容器
中, 鍵名為ServletContext
, 讀取WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
檔案裡的web.xml
配置中的xml檔案來建立beancontextConfigLocation
- 5、listener建立完畢後如果有Filter會去建立Filter
- 6、初始化Servlet,一般使用DispatchServlet類
- 7、DispatchServlet的父類FrameworkServlet會重寫其父類的initServletBean方法,并調用
以及initWebApplicationContext()
onRefresh()
- 8、
方法會建立一個目前servlet的一個IOC子容器,如果存在上述的全局initWebApplicationContext()
則将其設定為父容器,如果不存在上述全局的則父容器為null。WebApplicationContext
- 9、讀取
标簽的<servlet>
配置的xml檔案并加載相關Bean<init-param>
- 10、
方法建立Web應用相關元件onRefresh()
好了,本文到此就結束了,篇幅過長,如果其中有誤,可以在下方評論留言,也希望可以關注我哦