javascript
从web.xml谈谈SpringMVC集成spring的初始化流程及SpringBoot集成SpringMVC
一、關于servlet
- servlet與servlet容器
Java Servlet(Java服務器小程序)是一個基于Java技術的Web組件,運行在服務器端,它由Servlet容器所管理,用于生成動態的內容。 Servlet是平臺獨立的Java類,編寫一個Servlet,實際上就是按照Servlet規范編寫一個Java類。Servlet被編譯為平臺獨立 的字節碼,可以被動態地加載到支持Java技術的Web服務器中運行。
- Servlet容器也叫做Servlet引擎,是Web服務器或應用程序服務器的一部分,用于在發送的請求和響應之上提供網絡服務,解碼基于 MIME的請求,格式化基于MIME的響應。Servlet沒有main方法,不能獨立運行,它必須被部署到Servlet容器中,由容器來實例化和調用 Servlet的方法(如doGet()和doPost()),Servlet容器在Servlet的生命周期內包容和管理Servlet。在JSP技術 推出后,管理和運行Servlet/JSP的容器也稱為Web容器。
(注:常用的MIME類型:text/html,application/pdf,video/quicktime,application /java,image/jpeg,application/jar,application/octet-stream,application/x- zip)
有了servlet之后,用戶通過單擊某個鏈接或者直接在瀏覽器的地址欄中輸入URL來訪問Servlet,Web服務器接收到該請求后,并不是將 請求直接交給Servlet,而是交給Servlet容器。Servlet容器實例化Servlet,調用Servlet的一個特定方法對請求進行處理, 并產生一個響應。這個響應由Servlet容器返回給Web服務器,Web服務器包裝這個響應,以HTTP響應的形式發送給Web瀏覽器。
- servlet容器能提供什么?
我們知道需要由servlet容器來管理和運行servlet,但是為什么要這樣做呢?使用servlet容器的原因有:
-
通信支持:利用容器提供的方法,你能輕松的讓servlet與web服務器對話,而不用自己建立serversocket、監聽某個端口、創建流等等。容器知道自己與web服務器之間的協議,所以你的servlet不用擔心web服務器(如Apache,jetty)和你自己的web代碼之間的API,只需要考 慮如何在servlet中實現業務邏輯(如處理一個訂單)。
-
生命周期管理:servlet容器控制著servlet的生與死,它負責加載類、實例化和初始化servlet,調用servlet方法,以及使servlet實例被垃圾回收,有了servlet容器,你不需要太多的考慮資源管理。
-
多線程支持:容器會自動為它所接收的每個servlet請求創建一個新的java線程。針對用戶的請求,如果servlet已經運行完相應的http服務方法,這個線程就會結束。這并不是說你不需要考慮線程安全性,其實你還會遇到同步問題,不過這樣能使你少做很多工作。
-
聲明方式實現安全:利用servlet容器,你可以使用xml部署描述文件來配置和修改安全性,而不必將其硬編碼寫到servlet類代碼中。
-
JSP支持:servlet容器負責將jsp代碼翻譯為真正的java代碼。
- Servlet具有以下優點:
- Servlet是單實例多線程的運行方式,每個請求在一個獨立的線程中運行,而提供服務的Servlet實例只有一個。
- Servlet具有可升級性,能響應更多的請求,因為Servlet容器使用一個線程而不是操作系統進程,而線程僅占用有限的系統資源。
- Servlet使用標準的API,被更多的Web服務器所支持。
- Servlet使用Java語言編寫,因此擁有Java程序語言的所有優點,包括容易開發和平臺獨立性。
- Servlet可以訪問Java平臺豐富的類庫,使得各種應用的開發更為容易。
- Servlet容器給Servlet提供額外的功能,如錯誤處理和安全。
其實,servlet就是一種使用http協議在服務器與客戶端之間通信的技術。是Socket的一種應用。
- Tomcat
學習Servlet技術,就需要有一個Servlet運行環境,也就是需要有一個Servlet容器,本文用的是Tomcat。還有其他如jetty。Tomcat和IIS、Apache等Web服務器一樣,具有處理HTML頁面的功能,另外它還是一個Servlet和JSP容器,獨立的 Servlet容器是Tomcat的默認模式。不過,Tomcat處理靜態HTML的能力不如Apache,我們可以將Apache和Tomcat集成在 一起使用,Apache作為HTTP Web服務器,Tomcat作為Web容器。關于apache和tomcat的區別。
Tomcat服務器接受客戶請求并做出響應的過程如下:
二、從web.xml說起
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"version="5.0"><!-- spring配置文件,包括開啟bean掃描,aop,事務 --><!-- Spring加載的xml文件,不配置默認為applicationContext.xml --><context-param><param-name>contextConfigLocation</param-name><param-value>/WEB-INF/spring-config.xml</param-value></context-param><!--ContextLoaderListener用于在啟動web容器的時候,去上面的位置 讀取配置文件并初始化Spring容器。啟動父容器,即IOC容器,管理Dao,Service--><!-- 該類作為spring的listener使用,它會在創建時自動查找web.xml配置的applicationContext.xml文件 --><listener><listener-class>org.springframework.web.context.ContextLoaderListener</listener-class></listener><!--spring mvc配置--><!-- 配置Spring MVC的DispatcherServlet,也可以配置為繼承了DispatcherServlet的自定義類,這里配置spring mvc的配置(掃描controller) --><!--用于啟動子容器,也即是springMVC容器--><servlet><servlet-name>dispatcher</servlet-name><servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class><init-param><param-name>contextConfigLocation</param-name><param-value>/WEB-INF/spring-mvc.xml</param-value></init-param><load-on-startup>1</load-on-startup></servlet><servlet-mapping><servlet-name>dispatcher</servlet-name><url-pattern>/</url-pattern></servlet-mapping> </web-app>- spring父容器的加載
Tomcat啟動的時候會依次加載web.xml中配置的Listener、Filter和Servlet。所以根據上面的配置,會首先加載ContextLoaderListener,這個類繼承了ContextLoader,用來初始化Spring根上下文,并將其放入ServletContext中。
實現 javax.servlet.ServletContextListener 接口,繼承 ContextLoader 類,實現 Servlet 容器啟動和關閉時,分別初始化和銷毀 WebApplicationContext 容器
下面就以這個為入口分析下代碼。Tomcat容器首先會調用ContextLoadListener的contextInitialized()方法,這個方法又調用了父類ContextLoader的initWebApplicationContext()方法。下面是這個方法的源代碼。
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) { //如果ServletContext中已經存在Spring容器則報錯 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) {//這里創建了webApplicationContext,默認創建的是XmlWebApplicationContext//如果想要自定義實現類,可以在web.xml的<context-param>中配置contextClass這個參數//此時的Context還沒進行配置,相當于只是個"空殼"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, etcif (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);}//讀取Spring的配置文件,初始化父上下文環境configureAndRefreshWebApplicationContext(cwac, servletContext);}}//將根上下文存入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;} }spring體系下,ApplicationContext的集成體系
執行時序圖:
至此,Spring的父(根)上下文已經初始化完畢,并且已經存在ServletContext中。
- springMVC子容器的加載
DispatcherServlet的繼承圖如下
我們知道,加載web.xml中配置的Listener、Filter后,就會加載Servlet。在我們的web.xml中就是org.springframework.web.servlet.DispatcherServlet,而通過繼承樹我們知道,DispatcherServlet也是Servlet的實現,所以加載流程也是Servlet的流程。因為DispatcherServlet繼承了FrameworkServlet,FrameworkServlet又繼承了HttpServletBean,HttpServletBean又繼承HttpServlet并且重寫了init方法,所以創建子上下文時的入口就在這個init方法。
- HttpServletBean.init(): 重寫 GenericServlet 中的方法,負責將 ServletConfig 設置到當前 Servlet 對象中
下面是FrameWorkServlet這個類的initServletBean()方法
//FrameWorkServlet.initServletBean() protected final void initServletBean() throws ServletException {getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'");if (this.logger.isInfoEnabled()) {this.logger.info("FrameworkServlet '" + getServletName() + "': initialization started");}long startTime = System.currentTimeMillis();try {//這里是重點,初始化子Spring上下文this.webApplicationContext = initWebApplicationContext();initFrameworkServlet();}catch (ServletException ex) {this.logger.error("Context initialization failed", ex);throw ex;}catch (RuntimeException ex) {this.logger.error("Context initialization failed", ex);throw ex;}if (this.logger.isInfoEnabled()) {long elapsedTime = System.currentTimeMillis() - startTime;this.logger.info("FrameworkServlet '" + getServletName() + "': initialization completed in " +elapsedTime + " ms");} }下面是initWebApplicationContext()方法的具體代碼
protected WebApplicationContext initWebApplicationContext() {// <1> 獲得根 WebApplicationContext 對象WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext());// <2> 獲得 WebApplicationContext wac 對象WebApplicationContext wac = null;// 第一種情況,如果構造方法已經傳入 webApplicationContext 屬性,則直接使用if (this.webApplicationContext != null) {// A context instance was injected at construction time -> use itwac = this.webApplicationContext;// 如果是 ConfigurableWebApplicationContext 類型,并且未激活,則進行初始化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, etcif (cwac.getParent() == null) {// The context instance was injected without an explicit parent -> set// the root application context (if any; may be null) as the parentcwac.setParent(rootContext);}// 配置和初始化 wacconfigureAndRefreshWebApplicationContext(cwac);}}}// 第二種情況,從 ServletContext 獲取對應的 WebApplicationContext 對象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 idwac = findWebApplicationContext();}// 第三種,創建一個 WebApplicationContext 對象if (wac == null) {// No context instance is defined for this servlet -> create a local onewac = createWebApplicationContext(rootContext);}// <3> 如果未觸發刷新事件,則主動觸發刷新事件if (!this.refreshEventReceived) {// Either the context is not a ConfigurableApplicationContext with refresh// support or the context injected at construction time had already been// refreshed -> trigger initial onRefresh manually here.synchronized (this.onRefreshMonitor) {onRefresh(wac);}}// <4> 將 context 設置到 ServletContext 中if (this.publishContext) {// 將Spring子上下文存入ServletContextString attrName = getServletContextAttributeName();getServletContext().setAttribute(attrName, wac);}return wac; }最后看下DispatcherServlet中的onRefresh()方法,這個方法初始化了很多策略:
注意:FrameWorkServlet.onRefresh()只有DispatcherServlet中進行重寫
上面都是通過類似的機制,加載所有實現了該接口的類:
到此為止,SpringMVC的啟動過程結束了。這邊做下SpringMVC初始化總結:
時序圖:
三、傳統的Spring MVC項目啟動流程
四、SpringBoot集成SpringMVC
我們知道,SpringBoot的自動導入機制就是依賴spring framework內部使用的通用的工廠加載機制。其導入鏈如下:
@SpringBootApplication-> @EnableAutoConfiguration> @Import({AutoConfigurationImportSelector.class}) AutoConfigurationImportSelector類下的List<String>configurations=this.getCandidateConfigurations(annotationMetadata,attributes); protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),getBeanClassLoader());Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "+ "are using a custom packaging, make sure that file is correct.");return configurations;}springFactoriesLoader是spring framework內部使用的通用的工廠加載機制,其可加載并實例化可能出現在classpath上的多個jar包中的META-INF/spring.factories文件中定義的指定類型的工廠,可視為一種類似于SPI的接口。 SpringBoot利用這種SPI接口實現了autoconfiguration機制:委托SpringFactoriesLoader來加載所有配置在META-INF/spring.factories中的org.springframework.boot.autoconfigure.EnableAutoConfiguration對應的值,spring-boot-autoconfiguration jar包中的META-INF/spring.factories中的EnableAutoConfiguration配置了眾多供springboot導入的類。我們先回想一下SpringBoot預先讀取的可配置Config類有哪個是與WebMVC相關的?經過比對我們發現在Key=EnableAutoConfiguration,Value=WebMvcAutoConfiguration就是關于SpringBoot自動配置WebMVC的可配置類,關于SpringBoot自動配置WebMVC的玄機也正是在這個配置類里面。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\于是,我們必須好好看一下這個可配置類到底做了什么事情,能讓SpringBoot啟動完成之后也自動初始化好了一個WebMVC的環境。
點開可以看到這個可配置類上有很多注解修飾,這些注解分別限定在某些條件滿足或不滿足情況下,spring才會初始化并配置這個Config類。關于這些注解和文字解釋如下:
從上面可以看到,SpringBoot要想自動配置WebMVC環境,那么是需要滿足上述注解定義的一些條件:
- 處于Web環境下
- 容器中已經初始化好了WebMVC必須的組件
- 用戶沒有自己手工配置過WebMVC
這些條件都滿足后,SpringBoot就會為我們自動配置一個WebMVC環境,但是這個環境是怎么樣的呢(比如采用什么容器,端口號是什么,DispatchServlet怎么配置,Resover怎么配置,Converter怎么配置,等等)?這個就是最后一個注解@AutoConfigureAfter內聲明的來定義了,實際上也就是由DispatcherServletAutoConfiguration.class通過Java零XML配置方式在代碼里為我們提前寫好定義的環境。
對于核心DispatcherServlet的自動配置,點開這個核心DispatcherServletAutoConfiguration.class:
可以看到這個DispatcherServlet自動配置類也有很多注解修飾:
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) //如果環境中已經有了用戶自己配置的DispatcherServlet,就不再自動配置 @ConditionalOnClass(DispatcherServlet.class) //在這個DispatcherServlet自動配置之后,通過ServletWebServerFactoryAutoConfiguration對web容器進行配置 @AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class)在這個DispatcherServlet自動配置類中,最核心的就是對DispatcherServlet的配置,可以看到對DispatcherServlet配置的核心代碼在靜態內部類DispatcherServletConfiguration的dispatcherServlet方法上,通過@Bean返回給Spring容器:
從這個方法內部我們就看出了,SpringBoot自動配置DispatcherServlet時是直接new的,然后設置各種參數最終通過@Bean交給Spring容器。在上面的方法執行結束將一個DispatcherServlet交給Spring容器后,接下來會調用靜態內部類DispatcherServletRegistrationConfiguration的dispatcherServletRegistration方法,對DispatcherServlet的參數進行配置:
這里設置DispatcherServlet名字為dispatcherServlet;啟動順序是默認值-1(使用時加載);請求接收路徑是/;配置完成后交給Spring容器,這就是對DispatcherServlet的Java代碼配置了。
不過這里有一個問題,就是這里返回給Spring容器的是一個DispatcherServletRegistrationBean,那么Tomcat等Web容器時怎么將這個DispatcherServletRegistrationBean內包含的DispatcherServlet信息解析出來并生效運行呢?這個知識點有必要做一下說明。
首先看這個DispatcherServletRegistrationBean的構造是怎么樣的:
這個DispatcherServletRegistrationBean繼承了ServletRegistrationBean,這個ServletRegistrationBean是攜帶了供Tomcat容器解析加載的DispatcherServlet,讓我們看下這個ServletRegistrationBean的繼承關系圖:
就可以知道這個DispatcherServletRegistrationBean實際最終是實現了ServletContextInitializer接口,看到這里如果前面對Servlet的SPI有所了解很快就能反應過來,這里是有一個onStartup方法通過SPI機制讓Tomcat啟動時能自動調用到onStartup里面來,通過下面調用鏈最終Spring容器進行了addServlet操作將DispatchServlet添加到Spring容器中并生效,最后進行config方法對DispatcherServlet進行設置:
關于SPI機制可以參考這篇文章
但是讀了上面的源碼又引發了兩個很值得深思的問題:
這需要我們看SpringBoot中DispatcherServlet的源碼是怎么寫的,讓我們看下有參的構造是怎么樣的,因為無參構造只是一個空方法:
使用IDEAJ的查找工具,看下這個this.webApplicationContext在哪些地方被設置,可以找到是在setApplicationContext方法內進行設置的:
看到這個重寫方法以及方法注釋,根據Spring的知識我們應該猜想到這個類應該是實現了ApplicationContextAware接口。ApplicationContextAware接口是做什么的呢?這個接口是當Spring容器初始化結束之后,實現了ApplicationContextAware接口的實現類就會被調用并且執行setApplicationContext方法。回到這里也就是說當Spring容器初始化完成之后,由于實現了ApplicationContextAware接口,于是會執行setApplicationContext方法,在這個方法中將初始化完成的Spring上下文賦值給this.webApplicationContext變量(這個變量就是DispatchServlet內部的Spring上下文變量),于是就解釋了為什么SpringBoot在new一個DispatchServlet時不需要傳入Spring上下文的原因。
前面學習SpringMVC時知道對DispatcherServlet的配置是將Spring容器對象作為參數傳給DispatcherServlet;但是SpringBoot自動配置WebMVC時卻是將DispatcherServlet對象傳給Spring容器;這兩種場景的實現是反過來的,為什么會這樣設計呢?其實這兩種方式的區別就在于它們分別是怎么接在Spring容器的。
回答為什么SpringBoot是將DispatchServlet傳給Spring容器的問題,就要結合上面第一個問題的解析。這是因為要調用實現了ApplicationContextAware接口的實現類,則必須保證這個實現類在Spring容器當中才能生效。也就是說SpringBoot中實現了ApplicationContextAware接口的DispatchServlet要想觸發接口方法,則必須作為一個Bean存在于Spring容器中,這就是SpringBoot為什么要將DispatchServlet作為Bean傳入到Spring容器中的原因了。
在這個自動配置類中,定義了一個靜態內部類WebMvcAutoConfigurationAdapter,實現了WebMvcConfigurer接口。這個就和之前學習SpringMVC應用時介紹的一樣,通過實現WebMvcConfigurer接口就可以擁有一個類似web.xml功能,可以對其它組件進行配置操作:
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer在這個里面實現了默認的Resolver,MessageConverters,ViewResolver等組件。如果我們要添加自定義的組件怎么辦呢?對于SpringMVC而言我們必須在方法中將這個自定義的組件添加到對應組件集合中,但是SpringBoot對此進行的拓展,使得我們可以直接通過@Bean的方式就可以添加自定義組件(這一點在SpringMVC是做不到自定義添加的):
五、總結一下SpringBoot除了自動配置DispatchServlet之外,還配置了以下
主要做2個事情:
- 整合所有的視圖解析器
- 遍歷所有的視圖解析器選一個最佳的方案
spring boot 會在當前的spring容器里面找到所有HttpMessageConverter類型的Bean 封裝成一個集合
- spring boot會自己注入一個默認的實現 jackson
- 如果用戶需要自己配置的話 只需要@Bean
每當前端要將字符串日期傳遞到后臺之后,要自動轉換成日期格式類型時,可以寫一個Converter進行轉換。
至此,關于SpringBoot自動配置WebMVC的源碼原理就分享到這里了。
- 以springmvc自動裝配為例看底層源碼細節:配置類WebMvcAutoConfiguration.
- 這個配置類有多個注解分別限定在某些條件滿足或不滿足情況下,spring才會初始化并配置這個Config類,
- 其中注解@AutoConfigureAfter中DispatcherServletAutoConfiguration是自動裝配的核心類,
- 這里主要對DispatcherServlet進行配置,和servlet被tomcat調用onStartup()一樣,
- 需要先new出DispatcherServlet并放入Spring容器,然后在tomcat調用前進行addXXX參數的操作,
- 在springboot實現的javaConfig里,filter,listener,DispatcherServlet,servlet這些servler子類都用XXXRegistrationBean來封裝,
- 這些XXXRegistrationBean都實現了RegistrationBean超類(這個超類擁有onStartup方法),于是保證這些Bean都可以被 tomcat識別并執行,在執行各種不同類型的Bean時,會調用不同的register或configure方法進行add各自不同參數的方法,
- 從而實現了各種servlet組件參數的自動配置和加載步驟。對于web容器的配置是ServletWebServerFactoryAutoConfiguration配置類中進行主要對web容器類型IP端口連接數等參數進行配置。
參考文章
總結
以上是生活随笔為你收集整理的从web.xml谈谈SpringMVC集成spring的初始化流程及SpringBoot集成SpringMVC的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2020快手母婴行业数据价值报告
- 下一篇: HTTP,request,respons