SpringMVC是一款Web MVC框架聚蝶。 它跟Struts框架類似箩溃,是目前主流的Web MVC框架之一
一、SpringMVC的由來
早期 Java Web 的開發(fā)中,把顯示層堵漱、控制層、數(shù)據(jù)層的操作全部交給 JSP 或者 JavaBean 來進(jìn)行處理遗增,我們稱之為 Model1:
然而出現(xiàn)了很多的弊端执解,比如:
- JSP 和 Java Bean 之間嚴(yán)重耦合,Java 代碼和 HTML 代碼也耦合在了一起
- 要求開發(fā)者不僅要掌握 Java 哲身,還要有高超的前端水平
- 前端和后端相互依賴辩涝,前端需要等待后端完成,后端也依賴前端完成勘天,才能進(jìn)行有效的測試
隨后出現(xiàn)servlet怔揩,就有了早期的MVC模式
首先用戶請求到servlet捉邢,然后根據(jù)請求調(diào)用響應(yīng)的JavaBean,并把所有的顯示交給Jsp去處理商膊,這樣就稱之為mvc模式:
- M代表模型(Model):數(shù)據(jù)伏伐、bean
- V代表視圖(View):網(wǎng)頁,jsp....展示模型中的數(shù)據(jù)
- C代表控制器(Controller):把不同的數(shù)據(jù)(Model)晕拆,顯示在不同的視圖(View)上藐翎,Servlet 扮演的就是這樣的角色
為解決持久層中一直未處理好的數(shù)據(jù)庫事務(wù)的編程,又為了迎合 NoSQL 的強(qiáng)勢崛起实幕,Spring MVC 給出了方案
二吝镣、初始化過程
當(dāng)一個web應(yīng)用部署tomcat時,在接收用戶請求之前茬缩,會進(jìn)行以下初始化過程
- 部署在web.xml文件里由
<listener>
元素標(biāo)記事件的監(jiān)聽器會被創(chuàng)建和初始化 - 對于所有事件監(jiān)聽器赤惊,如果實現(xiàn)了
ServletContextListener
接口,將會執(zhí)行其實現(xiàn)的contextInitialized()
方法 - 部署描述文件由
<filter>
元素標(biāo)記的過濾器會被創(chuàng)建和初始化凰锡,并調(diào)用其init()
方法 - 部署在描述文件由
<servlet>
元素標(biāo)記的servlet會根據(jù)<load-on-start>
的權(quán)值按順序創(chuàng)建并初始化未舟,并調(diào)用其init()
方法
init
三、啟動流程
以下為一個常見的web.xml
配置進(jìn)行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>
<!--監(jiān)聽器-->
<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>
標(biāo)簽內(nèi)容讀取后會放進(jìn)application中,作為web應(yīng)用的全局變量使用勇哗,接下來創(chuàng)建listener
時會使用這個變量昼扛,因此,web應(yīng)用在啟動時欲诺,會先讀取這個變量抄谐,之后才進(jìn)行下一步,接著定義了一個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類并實現(xiàn)了ServletContextListener接口蛹含,首先看一下前面講述的ServletContextListener接口源碼:
public interface ServletContextListener extends EventListener {
void contextInitialized(ServletContextEvent var1);
void contextDestroyed(ServletContextEvent var1);
}
該接口只有兩個方法contextInitialized
和contextDestroyed
,這里采用的是觀察者模式塞颁,也稱為為訂閱-發(fā)布模式浦箱,實現(xiàn)了該接口的listener會向發(fā)布者進(jìn)行訂閱,當(dāng)Web應(yīng)用初始化或銷毀時會分別調(diào)用上述兩個方法, ContextLoaderListener的contextInitialized()
方法直接調(diào)用了initWebApplicationContext()
方法祠锣,這個方法是繼承自ContextLoader類酷窥,通過函數(shù)名可以知道,該方法是用于初始化Web應(yīng)用上下文伴网,即IOC容器蓬推,這里使用的是代理模式,繼續(xù)查看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()方法如上講述澡腾,主要目的就是創(chuàng)建root WebApplicationContext對象即IOC容器
拳氢,其中比較重要的就是募逞,整個Web應(yīng)用如果存在IOC容器則有且只能有一個,根IoC容器作為全局變量存儲在ServletContext
即application
對象中馋评。將根IOC容器
放入到application對象之前進(jìn)行了IOC容器
的配置和刷新操作,調(diào)用了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>
標(biāo)簽配置的全局變量contextConfigLocation
留特,并最后一行調(diào)用了refresh()
方法,ConfigurableWebApplicationContext
是一個接口玛瘸,通過對常用實現(xiàn)類ClassPathXmlApplicationContext
逐層查找后可以找到一個抽象類AbstractApplicationContext實現(xiàn)了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();
}
}
}
該方法主要用于創(chuàng)建并初始化contextConfigLocation
類配置的xml文件中的Bean
,因此糊渊,如果我們在配置Bean時出錯右核,在Web應(yīng)用啟動時就會拋出異常
,而不是等到運行時才拋出異常
整個ContextLoaderListener
類的啟動過程到此就結(jié)束了渺绒,可以發(fā)現(xiàn)贺喝,創(chuàng)建ContextLoaderListener
是比較核心的一個步驟,主要工作就是為了創(chuàng)建根IoC容器
并使用特定的key將其放入到application對象中宗兼,供整個Web應(yīng)用使用躏鱼,由于在ContextLoaderListener類中構(gòu)造的IOC容器
配置的Bean是全局共享的,因此殷绍,在<context-param>標(biāo)識的contextConfigLocation
的xml配置文件一般包括: 數(shù)據(jù)庫DataSource染苛、DAO層、Service層主到、事務(wù)等相關(guān)Bean
2茶行、Filter的初始化
在監(jiān)聽器listener
初始化完成后,接下來會進(jìn)行filter的初始化操作登钥,GenericFilterBean
是任何類型的過濾器的一個比較方便的超類畔师,這個類主要實現(xiàn)的就是從web.xml
文件中取得init-param中
設(shè)定的值,然后對Filter進(jìn)行初始化(當(dāng)然怔鳖,其子類可以覆蓋init()
方法)茉唉。
Filter的生命周期如下:
- void init(FilterConfig config): 用于完成Filter 的初始化
- void destroy(): 用于Filter 銷毀前结执,完成某些資源的回收
- void doFilter(ServletRequest request, ServletResponse response,FilterChain chain): 實現(xiàn)過濾功能度陆,該方法就是對每個請求及響應(yīng)增加的額外處理。 過濾器Filter也具有生命周期:init()->doFilter()->destroy()献幔,由部署文件中的filter元素驅(qū)動
3懂傀、Servlet的初始化
Web應(yīng)用啟動的最后一個步驟就是創(chuàng)建和初始化相關(guān)Servlet
,在開發(fā)中常用的Servlet就是DispatcherServlet
類前端控制器蜡感,前端控制器作為中央控制器是整個Web應(yīng)用的核心蹬蚁,用于獲取分發(fā)用戶請求并返回響應(yīng)
DispatcherServlet
類的間接父類實現(xiàn)了Servlet
接口恃泪,因此其本質(zhì)上依舊是一個Servlet
。DispatcherServlet
設(shè)計很巧妙犀斋,上層父類不同程度的實現(xiàn)了相關(guān)接口的部分方法贝乎,并留出了相關(guān)方法用于子類覆蓋,將不變的部分統(tǒng)一實現(xiàn)叽粹,將變化的部分預(yù)留方法用于子類實現(xiàn)具體順序圖
DispatcherServelt
類的本質(zhì)是Servlet
览效,通過文章開始的講解可知,在Web應(yīng)用部署到容器后進(jìn)行Servlet
初始化時會調(diào)用相關(guān)的init(ServletConfig)
方法虫几,因此锤灿,DispatchServlet
類的初始化過程也由該方法開始。上述調(diào)用邏輯中比較重要的就是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
抽象類在執(zhí)行init()
方法時會調(diào)用initServletBean()
方法啡氢,由于多態(tài)的特性状囱,最終會調(diào)用其子類FrameworkServlet
抽象類的initServletBean()
方法。該方法由final標(biāo)識空执,子類就不可再次重寫了浪箭。該方法中比較重要的就是initWebApplicationContext()
方法的調(diào)用,該方法仍由FrameworkServlet
抽象類實現(xiàn)
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;
}
該方法的主要作用同樣是創(chuàng)建一個WebApplicationContext
對象辨绊,即IOC容器
奶栖,不過前文講過每個Web應(yīng)用最多只能存在一個IOC容器
,這里創(chuàng)建的則是特定Servlet
擁有的子IOC容器
门坷。
父子IOC容器的訪問特性
父子容器類似于類的繼承關(guān)系宣鄙,子類可以訪問父類中的成員變量,而父類不可訪問子類的成員變量默蚌,同樣的冻晤,子容器可以訪問父容器中定義的Bean,但父容器無法訪問子容器定義的Bean绸吸,根IOC容器
做為全局共享的IOC容器
放入Web應(yīng)用需要共享的Bean鼻弧,而子IOC容器
根據(jù)需求的不同,放入不同的Bean锦茁,這樣能夠做到隔離
攘轩,保證系統(tǒng)的安全性
DispatcherServlet
類的子IOC容器
創(chuàng)建過程,如果當(dāng)前Servlet存在一個IOC容器
則為其設(shè)置根IOC容器
作為其父類码俩,并配置刷新該容器度帮,用于構(gòu)造其定義的Bean,這里的方法與前文講述的根IOC容器
類似稿存,同樣會讀取用戶在web.xml
中配置的<servlet>
中的<init-param>
值笨篷,用于查找相關(guān)的xml配置文件用于構(gòu)造定義的Bean瞳秽。如果當(dāng)前Servlet不存在一個子IoC容器就去查找一個,如果仍然沒有查找到則調(diào)用
createWebApplicationContext()方法去創(chuàng)建
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;
}
}
該方法用于創(chuàng)建一個子IOC容器
并將根IOC容器
做為其父容器率翅,接著進(jìn)行配置和刷新操作用于構(gòu)造相關(guān)的Bean练俐。至此,根IOC容器
以及相關(guān)Servlet
的子IoC容器
已經(jīng)配置完成安聘,子容器中管理的Bean一般只被該Servlet使用痰洒,因此,其中管理的Bean一般是局部
的浴韭,如SpringMVC中需要的各種重要組件,包括Controller脯宿、Interceptor念颈、Converter、ExceptionResolver等
當(dāng)IOC子容器
構(gòu)造完成后調(diào)用了onRefresh()
方法连霉,該方法的調(diào)用與initServletBean()
方法的調(diào)用相同榴芳,由父類調(diào)用但具體實現(xiàn)由子類覆蓋,調(diào)用onRefresh()
方法時將前文創(chuàng)建的IOC子容器作為參數(shù)傳入
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()
方法直接調(diào)用了initStrategies()
方法跺撼,源碼如上窟感,通過函數(shù)名可以判斷,該方法用于初始化創(chuàng)建multipartResovle
來支持圖片等文件的上傳歉井、本地化解析器柿祈、主題解析器、HandlerMapping處理器映射器哩至、HandlerAdapter
處理器適配器躏嚎、異常解析器、視圖解析器菩貌、flashMap管理器等卢佣,這些組件都是SpringMVC開發(fā)中的重要組件,相關(guān)組件的初始化創(chuàng)建過程均在此完成箭阶。
到此虚茶,初始化就全部結(jié)束了
四、總結(jié)
在Spring的web容器啟動時會去讀取web.xml
文件仇参,相關(guān)啟動順序為:<context-param> --> <listener> --> <filter> --> <servlet>
嘹叫,具體為:
- 1、解析
<context-param>
鍵值對 - 2冈敛、創(chuàng)建一個application對象即ServletContext待笑,servlet上下文,用于全局共享
- 3抓谴、將
<context-param>
鍵值對放入ServletContext中暮蹂,web應(yīng)用全局共享 - 4寞缝、讀取
<listener>
標(biāo)簽,創(chuàng)建監(jiān)聽器仰泻,一般使用ContextLoaderListener
荆陆,如果使用了ContextLoaderListener
,Spring就會創(chuàng)建一個WebApplicationContext對象集侯,這個就是IOC容器
,ContextLoaderListener
創(chuàng)建的IOC容器
是全局共享的被啼,并將其放在ServletContext
中, 鍵名為WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
, 讀取web.xml
文件里的contextConfigLocation
配置中的xml文件來創(chuàng)建bean - 5、listener創(chuàng)建完畢后如果有Filter會去創(chuàng)建Filter
- 6棠枉、初始化Servlet浓体,一般使用DispatchServlet類
- 7、DispatchServlet的父類FrameworkServlet會重寫其父類的initServletBean方法辈讶,并調(diào)用
initWebApplicationContext()
以及onRefresh()
方法 - 8命浴、
initWebApplicationContext()
方法會創(chuàng)建一個當(dāng)前servlet的一個IOC子容器,如果存在上述的全局WebApplicationContext
則將其設(shè)置為父容器贱除,如果不存在上述全局的則父容器為null生闲。 - 9、讀取
<servlet>
標(biāo)簽的<init-param>
配置的xml文件并加載相關(guān)Bean - 10月幌、
onRefresh()
方法創(chuàng)建Web應(yīng)用相關(guān)組件
好了碍讯,本文到此就結(jié)束了,篇幅過長扯躺,如果其中有誤捉兴,可以在下方評論留言,也希望可以關(guān)注我哦