概述
大家是否清楚,Tomcat是如何加載Spring和SpringMVC,今天我們就弄清下這個過程(記錄最關鍵的東西)
其中會涉及到大大小小的知識,包括加載時候的設計模式,Servlet知識等,看了你肯定有所收獲~
Tomcat
tomcat是一種Java寫的Web應用服務器,也被稱為Web容器,專門運行Web程序
tomcat啟動
tomcat啟動了之后會在操作系統中生成一個Jvm(Java虛擬機)的進程,從配置監聽端口(默認8080)監聽發來的HTTP/1.1協議的消息
默認配置文件這樣
當Tomcat啟動完成后,它就會加載其安裝目錄下webapps里的項目(放war包會自動解壓成項目)
小提問:webapps里多個項目,是運行在同一個JVM上嗎
是運行在同一個JVM上的(Tomcat啟動時創建的那個),多個項目就是多個線程,之所以項目間數據不共享,是因為類加載器不一樣的緣故
加載Web程序(Spring+SpringMVC框架)
tomcat啟動完畢后,最關鍵的是生成了ServletContext(Tomcat的上下文),然后會根據webapps項目里的web.xml進行加載項目
下面是一個SpringMVC+Spring項目的部分web.xml
1 <!--以下為加載Spring需要的配置--> 2 <!--Spring配置具體參數的地方--> 3 <context-param> 4 <param-name>contextConfigLocation</param-name> 5 <param-value> 6 classpath:applicationContext.xml 7 </param-value> 8 </context-param> 9 <!--Spring啟動的類--> 10 <listener> 11 <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> 12 </listener> 13 14 <!--以下為加載SpringMVC需要的配置--> 15 <servlet> 16 <servlet-name>project</servlet-name> 17 <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> 18 <load-on-startup>1</load-on-startup> <!--servlet被加載的順序,值越小優先級越高(正數)--> 19 20 <servlet-mapping> 21 <servlet-name>project</servlet-name> 22 <url-pattern>*.html</url-pattern> 23 </servlet-mapping> 24 </servlet>
初始化Spring
tomcat首先會加載進ContextLoaderListener,然后將applicationContext.xml里寫的參數注入進去,來完成一系列的Spring初始化(如各種bean,數據庫資源等等)
這里就是經常聽到的Ioc容器的初始化了,我們搜索這個類發現以下代碼
1 public class ContextLoaderListener extends ContextLoader implements ServletContextListener { 2 //省略其他方法 3 4 /** 5 * Initialize the root web application context. 6 */ 7 @Override 8 public void contextInitialized(ServletContextEvent event) { 9 initWebApplicationContext(event.getServletContext()); 10 } 11 12 //省略其他方法 13 }
這里最重要的是通過ServletContext,初始化屬於Spring的上下文WebApplicationContext,並將其存放在ServletContext
WebApplicationContext多重要老鐵們都懂得,我們經常用webApplicationContext.getBean()來獲取被Spring管理的類,所以這也是IOC容器的核心
Spring采用這種監聽器來啟動自身的方法,也是一種設計模式,叫觀察者模式:
整個過程是這樣的,Tomcat加載webapps項目時,先通過反射加載在web.xml標明的類(通通放入一個數組)
到某個時刻,我tomcat(事件源,事件的起源)會發起一個叫ServletContextEvent的事件(里面帶着各種參數)
凡是實現了ServletContextListener接口的類,我都會調用里面的contextInitialized方法,並把這個事件參數傳進去
咳咳,現在我看看這個數組里有沒符合條件的(遍歷),發現真有實現這個接口的(類 instanceof 接口),就調用contextInitialized方法
於是Spring就被動態加載進來了~~
題外話:
加載一個類,可以用用完整的類名,通過java反射加載,Class.forName(類名)
也能直接new 一個類 來加載
初始化SpringMVC
看配置文件,標簽是servlet,我們得首先了解下servlet是什么東東
Servlet簡介
Servlet是一個接口,為web通信而生(說白了就是一堆sun公司的大佬們開會,拍板造出的類,有固定的幾個方法)
tomcat有一套定義好的程序(其實不只是tomcat,能跑java寫的web應用服務器如Jetty等,都有這固定程序)
1.當tomcat加載進來一個類時,如果它實現了Servlet接口,那么會記載到一個Map里,然后執行一次init()方法進行Servlet初始化
2.當tomcat收到瀏覽器的請求后,就會在Map里找對應路徑的Servlet處理,路徑就是寫在<url-pattern>標簽里的參數,調用service()這個方法
3.當Servlet要被銷毀了,就調用一次destroy()方法
各位看到這是不是感覺相識,跟Spring加載差不多嘛,都是實現了一個接口后就被命運(tomcat)安排~~
當然,我們自己實現Servlet接口太雞兒麻煩了,於是有HttpServlet(一個抽象類)幫我們實現了大部分方法(包含http頭的設置,doXXX方法判斷等等)
所以我們只要繼承HttpServlet就實現幾個方法就能用啦
SpringMVC加載
為什么要講Servlet,因為SpringMVC的核心就是DispatcherServlet(前置控制器),如圖
DispatcherServlet由SpringMVC的實現,已經實現的很棒棒了,我們不需要再動它
tomcat從web.xml中加載DispatcherServlet,然后會調用它的init()方法
Servlet配置文件默認在/WEB-INF/<servlet-name>-servlet.xml,所以現在默認叫project-servlet.xml
當然,也能自己指定文件
1 <!--以下為加載SpringMVC需要的配置--> 2 <servlet> 3 <servlet-name>project</servlet-name> 4 <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> 5 <load-on-startup>1</load-on-startup> 6 7 <!--指定配置文件--> 8 <init-param> 9 <param-name>contextCOnfigLocation</param-name> 10 <param-value>classPath:spring-servlet.xml</param-value> 11 </init-param> 12 13 <servlet-mapping> 14 <servlet-name>project</servlet-name> 15 <url-pattern>*.html</url-pattern> 16 </servlet-mapping> 17 </servlet>
當SpringMVC加載好后,瀏覽器有請求過來,如果是.html結尾,tomcat就會交給DispatcherServlet處理
而DispatcherServlet會根據路徑找到對應的處理器處理,可以理解為我們寫的Controller接收到了(具體SpringMVC處理流程會寫一篇博文)
至此,瀏覽器發送請求,到執行我們寫的代碼這個流程就結束了 ~~撒花~~
Spring和SpringMVC的容器問題
既然講到tomcat加載這兩個框架,大家發現沒有,在web.xml中,Spring加載是寫在DispatcherServlet加載前面的
我們不妨來看看DispatcherServlet的初始化方法,由於DispatcherServlet是通過層層繼承而來的,所以初始化的方法也變成了
1 public abstract class HttpServletBean extends HttpServlet implements EnvironmentCapable, EnvironmentAware { 2 3 //其他方法 4 5 @Override 6 public final void init() throws ServletException { 7 8 //其他代碼 9 10 // Let subclasses do whatever initialization they like. 11 initServletBean(); 12 } 13 14 protected void initServletBean() throws ServletException { 15 } 16 17 //其他方法 18 } 19 20 public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware { 21 22 //其他方法 23 24 @Override 25 protected final void initServletBean() throws ServletException { 26 //其他代碼 27 28 try { 29 this.webApplicationContext = initWebApplicationContext(); 30 initFrameworkServlet(); 31 } 32 catch (ServletException | RuntimeException ex) { 33 logger.error("Context initialization failed", ex); 34 throw ex; 35 } 36 37 //其他代碼 38 } 39 40 protected WebApplicationContext initWebApplicationContext() { 41 42 //獲得了Spring創造的webApplicationContext,關鍵 43 WebApplicationContext rootContext = 44 WebApplicationContextUtils.getWebApplicationContext(getServletContext()); 45 WebApplicationContext wac = null; 46 47 if (this.webApplicationContext != null) { 48 // A context instance was injected at construction time -> use it 49 wac = this.webApplicationContext; 50 if (wac instanceof ConfigurableWebApplicationContext) { 51 ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac; 52 if (!cwac.isActive()) { 53 // The context has not yet been refreshed -> provide services such as 54 // setting the parent context, setting the application context id, etc 55 if (cwac.getParent() == null) { 56 // The context instance was injected without an explicit parent -> set 57 // the root application context (if any; may be null) as the parent 58 cwac.setParent(rootContext); //設置父上下文 59 } 60 configureAndRefreshWebApplicationContext(cwac); 61 } 62 } 63 } 64 65 //其他代碼 66 } 67 68 //其他方法 69 }
我們可以看到,HttpServlet將init()實現了,留下了initServletBean()抽象方法
而FrameworkServlet實現了initServletBean()方法並定義成final(不允許重寫),在此方法中調用了initWebApplicationContext()
而initWebApplicationContext()中說明了如果tomcat里存在webapplication就獲取它,然后將其設置為SpringMVC的父上下文
至此DispatcherServlet初始化完成(當然我省略了其他的初始化做的事)
因此Spring和SpringMVC容器之間是父子關系,由於子容器可以訪問父容器的內容,而反過來不行,所以不要想在Service里自動注入Controller這種操作
因此會有下面情況
Spring配置進行掃包的時候,如果將Controller也掃進來了會怎樣
1 <?xml version="1.0" encoding="UTF-8"?> 2 <beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:context="http://www.springframework.org/schema/context" 4 ...../ 此處省略> 5 6 <!-- 掃描全部包 --> 7 <context:component-scan base-package="com.shop"></context:component-scan> 8 9 <!--本來正確寫法是這樣的> 10 <!-- 11 <!-- 掃描包Service實現類 --> 12 <context:component-scan base-package="com.shop.service"></context:component-scan> 13 > 14 15 </beans>
那么,Controller將進入Spring的上下文,SpringMVC里就沒Controller了,到時候有請求給DispatcherServlet時,就會找不到Controller而404
小提問:那么SpringMVC掃描所有的包可以嗎
這個是可以的,SpringMVC不需要Spring也能使用,但是加入Spring是為了更好的兼容其他的框架(數據庫框架等等,例如SpringMVC掃了service和dao,那是不支持事務的)
但是如果用了Spring就不能這樣做,包括Spring掃Controller,SpringMVC也掃一次Controller這些操作,會出現各種奇怪的問題
小提問:SpringMVC和Spring為什么要兩個容器呢,Springboot一個容器就搞定了呀
其實這和Spring設計理念相關,畢竟一個框架不可能做完一切,總要和第三方框架配合。假設公用一個容器,那么多個第三方框架的Bean沖突概率就很高,由每個第三方框架維護自己的容器,將Spring作為父類容器,則能和平相處。
Springboot之所以被大家喜愛,是因為它遵循默認大於配置,它幫我們配置好了常用的框架(大部分是寫好了配置類,更改了框架的啟動注冊流程),底層該用SpringMVC還是SpringMVC,該用Spring還是Spring。所以可以在框架啟動注冊時,就用一個容器把相關內容合理的安排好(這不是全部框架都支持,例如SpringCloud就有自己的獨有的容器,並把它設置為Springboot的父容器~)
以上就講完啦,希望大家點點贊,或者一鍵3連不迷路~~
參考:https://www.cnblogs.com/wyq1995/p/10672457.html
參考:https://blog.csdn.net/s740556472/article/details/54879954