1. 程式人生 > 實用技巧 >從設計模式+原始碼講到Tomcat載入Spring,你還不懂?

從設計模式+原始碼講到Tomcat載入Spring,你還不懂?

概述

大家是否清楚,Tomcat是如何載入Spring和SpringMVC,今天我們就弄清下這個過程(記錄最關鍵的東西)

其中會涉及到大大小小的知識,包括載入時候的設計模式,Servlet知識等,看了你肯定有所收穫~

文章首發個人公眾號:Java架構師聯盟,每日更新技術好文

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

<!--以下為載入Spring需要的配置-->
<!--Spring配置具體引數的地方-->
<context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>
   classpath:applicationContext.xml
  </param-value>
</context-param>
<!--Spring啟動的類-->
<listener>
  <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
​
 <!--以下為載入SpringMVC需要的配置-->
<servlet>
  <servlet-name>project</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <load-on-startup>1</load-on-startup>  <!--servlet被載入的順序,值越小優先順序越高(正數)-->
​
  <servlet-mapping>
     <servlet-name>project</servlet-name>
     <url-pattern>*.html</url-pattern>
  </servlet-mapping>
</servlet>

初始化Spring

tomcat首先會載入進ContextLoaderListener,然後將applicationContext.xml裡寫的引數注入進去,來完成一系列的Spring初始化(如各種bean,資料庫資源等等)

這裡就是經常聽到的Ioc容器的初始化了,我們搜尋這個類發現以下程式碼

public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
   //省略其他方法
​
   /**
   * Initialize the root web application context.
   */
   @Override
   public void contextInitialized(ServletContextEvent event) {
     initWebApplicationContext(event.getServletContext());
   }
​
   //省略其他方法
}

這裡最重要的是通過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

當然,也能自己指定檔案

 <!--以下為載入SpringMVC需要的配置-->
<servlet>
  <servlet-name>project</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <load-on-startup>1</load-on-startup>
​
  <!--指定配置檔案-->
  <init-param>
          <param-name>contextCOnfigLocation</param-name>
          <param-value>classPath:spring-servlet.xml</param-value>
  </init-param>
​
  <servlet-mapping>
     <servlet-name>project</servlet-name>
     <url-pattern>*.html</url-pattern>
  </servlet-mapping>
</servlet>

當SpringMVC載入好後,瀏覽器有請求過來,如果是.html結尾,tomcat就會交給DispatcherServlet處理

而DispatcherServlet會根據路徑找到對應的處理器處理,可以理解為我們寫的Controller接收到了(具體SpringMVC處理流程會寫一篇博文)

至此,瀏覽器傳送請求,到執行我們寫的程式碼這個流程就結束了撒花

Spring和SpringMVC的容器問題

既然講到tomcat載入這兩個框架,大家發現沒有,在web.xml中,Spring載入是寫在DispatcherServlet載入前面的

我們不妨來看看DispatcherServlet的初始化方法,由於DispatcherServlet是通過層層繼承而來的,所以初始化的方法也變成了

public abstract class HttpServletBean extends HttpServlet implements EnvironmentCapable, EnvironmentAware {
 
   //其他方法
 
   @Override
   public final void init() throws ServletException {
     
     //其他程式碼
 
     // Let subclasses do whatever initialization they like.
     initServletBean();
   }
 
   protected void initServletBean() throws ServletException {
   }
 
   //其他方法
}
 
public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {
 
   //其他方法
 
   @Override
   protected final void initServletBean() throws ServletException {
     //其他程式碼
 
     try {
       this.webApplicationContext = initWebApplicationContext();
       initFrameworkServlet();
     }
     catch (ServletException | RuntimeException ex) {
       logger.error("Context initialization failed", ex);
       throw ex;
     }
 
     //其他程式碼
   }
 
   protected WebApplicationContext initWebApplicationContext() {
 
     //獲得了Spring創造的webApplicationContext,關鍵
     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) {
             // The context instance was injected without an explicit parent -> set
             // the root application context (if any; may be null) as the parent
             cwac.setParent(rootContext);    //設定父上下文
           }
           configureAndRefreshWebApplicationContext(cwac);
         }
       }
     }
     
     //其他程式碼
   }
 
   //其他方法
}

我們可以看到,HttpServlet將init()實現了,留下了initServletBean()抽象方法

而FrameworkServlet實現了initServletBean()方法並定義成final(不允許重寫),在此方法中呼叫了initWebApplicationContext()

而initWebApplicationContext()中說明了如果tomcat裡存在webapplication就獲取它,然後將其設定為SpringMVC的父上下文

至此DispatcherServlet初始化完成(當然我省略了其他的初始化做的事)

因此Spring和SpringMVC容器之間是父子關係,由於子容器可以訪問父容器的內容,而反過來不行,所以不要想在Service裡自動注入Controller這種操作

因此會有下面情況

Spring配置進行掃包的時候,如果將Controller也掃進來了會怎樣

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:context="http://www.springframework.org/schema/context"
   ...../ 此處省略>
 
   <!-- 掃描全部包 -->
   <context:component-scan base-package="com.shop"></context:component-scan>     <!--本來正確寫法是這樣的>  <!--    <!-- 掃描包Service實現類 -->     <context:component-scan base-package="com.shop.service"></context:component-scan>  >
</beans>

那麼,Controller將進入Spring的上下文,SpringMVC裡就沒Controller了,到時候有請求給DispatcherServlet時,就會找不到Controller而404

小提問:那麼SpringMVC掃描所有的包可以嗎

這個是可以的,SpringMVC不需要Spring也能使用,但是加入Spring是為了更好的相容其他的框架(資料庫框架等等)

但是如果用了Spring就不能這樣做,包括Spring掃Controller,SpringMVC也掃一次Controller這些操作,會出現各種奇怪的問題

來源:朔州SEO