黄a在线观看-黄a在线-黄a大片-黄色片在线看-黄色毛片免费-黄色大片网站

您的位置:首頁技術文章
文章詳情頁

解析Tomcat架構原理到架構設計

瀏覽:281日期:2023-03-19 16:50:55
目錄
  • 一、學習目的
    • 1.1、掌握 Tomcat 架構設計與原理提高內功
    • 1.2、宏觀理解一個請求如何與 Spring 聯系起來
    • 1.3、提升自己的系統設計能力
  • 二、整體架構設計
    • 2.1、連接器
    • 2.2、封裝變與不變
    • 2.3、容器
    • 2.4、請求定位 Servlet 的過程
  • 三、Tomcat 為何打破雙親委派機制
    • 3.1、雙親委派
    • 3.2、Tomcat 熱加載
    • 3.3、Tomcat 的類加載器
    • 3.4、Tomcat 類加載器層次
  • 四、整體架構設計解析收獲總結
    • 4.1、連接器
    • 4.2、容器
    • 4.3、類加載器
  • 五、實際場景運用
    • 5.1、責任鏈模式
    • 5.2、模板方法模式
    • 5.3、策略模式

一、學習目的

1.1、掌握 Tomcat 架構設計與原理提高內功

宏觀上看

Tomcat 作為一個 「Http 服務器 + Servlet 容器」,對我們屏蔽了應用層協議和網絡通信細節,給我們的是標準的 RequestResponse 對象;對于具體的業務邏輯則作為變化點,交給我們來實現。我們使用了SpringMVC 之類的框架,可是卻從來不需要考慮 TCP 連接、 Http 協議的數據處理與響應。就是因為 Tomcat 已經為我們做好了這些,我們只需要關注每個請求的具體業務邏輯。

微觀上看

Tomcat 內部也隔離了變化點與不變點,使用了組件化設計,目的就是為了實現「俄羅斯套娃式」的高度定制化(組合模式),而每個組件的生命周期管理又有一些共性的東西,則被提取出來成為接口和抽象類,讓具體子類實現變化點,也就是模板方法設計模式。

當今流行的微服務也是這個思路,按照功能將單體應用拆成「微服務」,拆分過程要將共性提取出來,而這些共性就會成為核心的基礎服務或者通用庫。「中臺」思想亦是如此。

設計模式往往就是封裝變化的一把利器,合理的運用設計模式能讓我們的代碼與系統設計變得優雅且整潔。

這就是學習優秀開源軟件能獲得的「內功」,從不會過時,其中的設計思想與哲學才是根本之道。從中借鑒設計經驗,合理運用設計模式封裝變與不變,更能從它們的源碼中汲取經驗,提升自己的系統設計能力。

1.2、宏觀理解一個請求如何與 Spring 聯系起來

在工作過程中,我們對 Java 語法已經很熟悉了,甚至「背」過一些設計模式,用過很多 Web 框架,但是很少有機會將他們用到實際項目中,讓自己獨立設計一個系統似乎也是根據需求一個個 Service 實現而已。腦子里似乎沒有一張 Java Web 開發全景圖,比如我并不知道瀏覽器的請求是怎么跟 Spring 中的代碼聯系起來的。

為了突破這個瓶頸,為何不站在巨人的肩膀上學習優秀的開源系統,看大牛們是如何思考這些問題。

學習 Tomcat 的原理,我發現 Servlet 技術是 Web 開發的原點,幾乎所有的 Java Web 框架(比如 Spring)都是基于 Servlet 的封裝,Spring 應用本身就是一個 ServletDispatchSevlet),而 Tomcat 和 Jetty 這樣的 Web 容器,負責加載和運行 Servlet。如圖所示:

1.3、提升自己的系統設計能力

學習 Tomcat ,我還發現用到不少 Java 高級技術,比如 Java 多線程并發編程、Socket 網絡編程以及反射等。之前也只是了解這些技術,為了面試也背過一些題。但是總感覺「知道」與會用之間存在一道溝壑,通過對 Tomcat 源碼學習,我學會了什么場景去使用這些技術。

還有就是系統設計能力,比如面向接口編程、組件化組合模式、骨架抽象類、一鍵式啟停、對象池技術以及各種設計模式,比如模板方法、觀察者模式、責任鏈模式等,之后我也開始模仿它們并把這些設計思想運用到實際的工作中。

二、整體架構設計

今天咱們就來一步一步分析 Tomcat 的設計思路,一方面我們可以學到 Tomcat 的總體架構,學會從宏觀上怎么去設計一個復雜系統,怎么設計頂層模塊,以及模塊之間的關系;另一方面也為我們深入學習 Tomcat 的工作原理打下基礎。

Tomcat 啟動流程:

startup.sh -> catalina.sh start ->java -jar org.apache.catalina.startup.Bootstrap.main()

Tomcat 實現的 2 個核心功能:

  • 處理 Socket 連接,負責網絡字節流與 RequestResponse 對象的轉化。
  • 加載并管理 Servlet ,以及處理具體的 Request 請求。

所以 Tomcat 設計了兩個核心組件連接器(Connector)和容器(Container)。連接器負責對外交流,容器負責內部 處理

Tomcat為了實現支持多種 I/O 模型和應用層協議,一個容器可能對接多個連接器,就好比一個房間有多個門。

  • Server 對應的就是一個 Tomcat 實例。
  • Service 默認只有一個,也就是一個 Tomcat 實例默認一個 Service。
  • Connector:一個 Service 可能多個 連接器,接受不同連接協議。
  • Container: 多個連接器對應一個容器,頂層容器其實就是 Engine。

每個組件都有對應的生命周期,需要啟動,同時還要啟動自己內部的子組件,比如一個 Tomcat 實例包含一個 Service,一個 Service 包含多個連接器和一個容器。而一個容器包含多個 Host, Host 內部可能有多個 Contex t 容器,而一個 Context 也會包含多個 Servlet,所以 Tomcat 利用組合模式管理組件每個組件,對待過個也想對待單個組一樣對待。整體上每個組件設計就像是「俄羅斯套娃」一樣。

2.1、連接器

在開始講連接器前,我先鋪墊一下 Tomcat支持的多種 I/O 模型和應用層協議。

Tomcat支持的 I/O 模型有:

  • NIO:非阻塞 I/O,采用 Java NIO 類庫實現。
  • NIO2:異步I/O,采用 JDK 7 最新的 NIO2 類庫實現。
  • APR:采用 Apache可移植運行庫實現,是 C/C++ 編寫的本地庫。

Tomcat 支持的應用層協議有:

  • HTTP/1.1:這是大部分 Web 應用采用的訪問協議。
  • AJP:用于和 Web 服務器集成(如 Apache)。
  • HTTP/2:HTTP 2.0 大幅度的提升了 Web 性能。

所以一個容器可能對接多個連接器。連接器對 Servlet 容器屏蔽了網絡協議與 I/O 模型的區別,無論是 Http 還是 AJP,在容器中獲取到的都是一個標準的 ServletRequest 對象。

細化連接器的功能需求就是:

  • 監聽網絡端口。
  • 接受網絡連接請求。
  • 讀取請求網絡字節流。
  • 根據具體應用層協議(HTTP/AJP)解析字節流,生成統一的 Tomcat Request 對象。
  • Tomcat Request 對象轉成標準的 ServletRequest
  • 調用 Servlet容器,得到 ServletResponse
  • ServletResponse轉成 Tomcat Response 對象。
  • Tomcat Response 轉成網絡字節流。將響應字節流寫回給瀏覽器。

需求列清楚后,我們要考慮的下一個問題是,連接器應該有哪些子模塊?優秀的模塊化設計應該考慮高內聚、低耦合。

  • 高內聚是指相關度比較高的功能要盡可能集中,不要分散。
  • 低耦合是指兩個相關的模塊要盡可能減少依賴的部分和降低依賴的程度,不要讓兩個模塊產生強依賴。

我們發現連接器需要完成 3 個高內聚的功能:

  • 網絡通信。
  • 應用層協議解析。
  • Tomcat Request/ResponseServletRequest/ServletResponse 的轉化。

因此 Tomcat 的設計者設計了 3 個組件來實現這 3 個功能,分別是 EndPoint、Processor 和 Adapter

網絡通信的 I/O 模型是變化的, 應用層協議也是變化的,但是整體的處理邏輯是不變的,EndPoint 負責提供字節流給 ProcessorProcessor負責提供 Tomcat Request 對象給 AdapterAdapter負責提供 ServletRequest對象給容器。

2.2、封裝變與不變

因此 Tomcat 設計了一系列抽象基類來封裝這些穩定的部分,抽象基類 AbstractProtocol實現了 ProtocolHandler接口。每一種應用層協議有自己的抽象基類,比如 AbstractAjpProtocolAbstractHttp11Protocol,具體協議的實現類擴展了協議層抽象基類。

這就是模板方法設計模式的運用。

總結下來,連接器的三個核心組件 EndpointProcessorAdapter來分別做三件事情,其中 EndpointProcessor放在一起抽象成了 ProtocolHandler組件,它們的關系如下圖所示。

ProtocolHandler 組件:

主要處理 網絡連接 和 應用層協議 ,包含了兩個重要部件 EndPoint 和 Processor,兩個組件組合形成 ProtocoHandler,下面我來詳細介紹它們的工作原理。

EndPoint:

EndPoint是通信端點,即通信監聽的接口,是具體的 Socket 接收和發送處理器,是對傳輸層的抽象,因此 EndPoint是用來實現 TCP/IP 協議數據讀寫的,本質調用操作系統的 socket 接口。

EndPoint是一個接口,對應的抽象實現類是 AbstractEndpoint,而 AbstractEndpoint的具體子類,比如在 NioEndpointNio2Endpoint中,有兩個重要的子組件:AcceptorSocketProcessor

其中 Acceptor 用于監聽 Socket 連接請求。SocketProcessor用于處理 Acceptor 接收到的 Socket請求,它實現 Runnable接口,在 Run方法里調用應用層協議處理組件 Processor 進行處理。為了提高處理能力,SocketProcessor被提交到線程池來執行。

我們知道,對于 Java 的多路復用器的使用,無非是兩步:

  • 創建一個 Seletor,在它身上注冊各種感興趣的事件,然后調用 select 方法,等待感興趣的事情發生。
  • 感興趣的事情發生了,比如可以讀了,這時便創建一個新的線程從 Channel 中讀數據。

在 Tomcat 中 NioEndpoint 則是 AbstractEndpoint 的具體實現,里面組件雖然很多,但是處理邏輯還是前面兩步。它一共包含 LimitLatchAcceptorPollerSocketProcessorExecutor 共 5 個組件,分別分工合作實現整個 TCP/IP 協議的處理。

LimitLatch 是連接控制器,它負責控制最大連接數,NIO 模式下默認是 10000,達到這個閾值后,連接請求被拒絕。

Acceptor跑在一個單獨的線程里,它在一個死循環里調用 accept方法來接收新連接,一旦有新的連接請求到來,accept方法返回一個 Channel 對象,接著把 Channel對象交給 Poller 去處理。

Poller 的本質是一個 Selector,也跑在單獨線程里。Poller在內部維護一個 Channel數組,它在一個死循環里不斷檢測 Channel的數據就緒狀態,一旦有 Channel可讀,就生成一個 SocketProcessor任務對象扔給 Executor去處理。

SocketProcessor 實現了 Runnable 接口,其中 run 方法中的 getHandler().process(socketWrapper, SocketEvent.CONNECT_FAIL); 代碼則是獲取 handler 并執行處理 socketWrapper,最后通過 socket 獲取合適應用層協議處理器,也就是調用 Http11Processor 組件來處理請求。Http11Processor 讀取 Channel 的數據來生成 ServletRequest 對象,Http11Processor 并不是直接讀取 Channel 的。這是因為 Tomcat 支持同步非阻塞 I/O 模型和異步 I/O 模型,在 Java API 中,相應的 Channel 類也是不一樣的,比如有 AsynchronousSocketChannel 和 SocketChannel,為了對 Http11Processor 屏蔽這些差異,Tomcat 設計了一個包裝類叫作 SocketWrapper,Http11Processor 只調用 SocketWrapper 的方法去讀寫數據。

Executor就是線程池,負責運行 SocketProcessor任務類,SocketProcessorrun方法會調用 Http11Processor 來讀取和解析請求數據。我們知道,Http11Processor是應用層協議的封裝,它會調用容器獲得響應,再把響應通過 Channel寫出。

工作流程如下所示:

Processor:

Processor 用來實現 HTTP 協議,Processor 接收來自 EndPoint 的 Socket,讀取字節流解析成 Tomcat Request 和 Response 對象,并通過 Adapter 將其提交到容器處理,Processor 是對應用層協議的抽象。

從圖中我們看到,EndPoint 接收到 Socket 連接后,生成一個 SocketProcessor 任務提交到線程池去處理,SocketProcessor 的 Run 方法會調用 HttpProcessor 組件去解析應用層協議,Processor 通過解析生成 Request 對象后,會調用 Adapter 的 Service 方法,方法內部通過 以下代碼將請求傳遞到容器中。

// Calling the containerconnector.getService().getContainer().getPipeline().getFirst().invoke(request, response);

Adapter 組件:

由于協議的不同,Tomcat 定義了自己的 Request 類來存放請求信息,這里其實體現了面向對象的思維。但是這個 Request 不是標準的 ServletRequest ,所以不能直接使用 Tomcat 定義 Request 作為參數直接容器。

Tomcat 設計者的解決方案是引入 CoyoteAdapter,這是適配器模式的經典運用,連接器調用 CoyoteAdapterSevice 方法,傳入的是 Tomcat Request 對象,CoyoteAdapter負責將 Tomcat Request 轉成 ServletRequest,再調用容器的 Service方法。

2.3、容器

連接器負責外部交流,容器負責內部處理。具體來說就是,連接器處理 Socket 通信和應用層協議的解析,得到 Servlet請求;而容器則負責處理 Servlet請求。

容器:顧名思義就是拿來裝東西的, 所以 Tomcat 容器就是拿來裝載 Servlet

Tomcat 設計了 4 種容器,分別是 EngineHostContextWrapperServer 代表 Tomcat 實例。

要注意的是這 4 種容器不是平行關系,屬于父子關系,如下圖所示:

你可能會問,為啥要設計這么多層次的容器,這不是增加復雜度么?其實這背后的考慮是,Tomcat 通過一種分層的架構,使得 Servlet 容器具有很好的靈活性。因為這里正好符合一個 Host 多個 Context, 一個 Context 也包含多個 Servlet,而每個組件都需要統一生命周期管理,所以組合模式設計這些容器

Wrapper 表示一個 ServletContext 表示一個 Web 應用程序,而一個 Web 程序可能有多個 ServletHost 表示一個虛擬主機,或者說一個站點,一個 Tomcat 可以配置多個站點(Host);一個站點( Host) 可以部署多個 Web 應用;Engine 代表 引擎,用于管理多個站點(Host),一個 Service 只能有 一個 Engine

可通過 Tomcat 配置文件加深對其層次關系理解。

<Server port="8005" shutdown="SHUTDOWN"> // 頂層組件,可包含多個 Service,代表一個 Tomcat 實例  <Service name="Catalina">  // 頂層組件,包含一個 Engine ,多個連接器    <Connector port="8080" protocol="HTTP/1.1"       connectionTimeout="20000"       redirectPort="8443" />    <!-- Define an AJP 1.3 Connector on port 8009 -->    <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />  // 連接器	// 容器組件:一個 Engine 處理 Service 所有請求,包含多個 Host    <Engine name="Catalina" defaultHost="localhost">	  // 容器組件:處理指定Host下的客戶端請求, 可包含多個 Context      <Host name="localhost"  appBase="webapps"    unpackWARs="true" autoDeploy="true">			// 容器組件:處理特定 Context Web應用的所有客戶端請求			<Context></Context>      </Host>    </Engine>  </Service></Server>

如何管理這些容器?我們發現容器之間具有父子關系,形成一個樹形結構,是不是想到了設計模式中的 組合模式 。

Tomcat 就是用組合模式來管理這些容器的。具體實現方法是,所有容器組件都實現了 Container接口,因此組合模式可以使得用戶對單容器對象和組合容器對象的使用具有一致性。這里單容器對象指的是最底層的 Wrapper,組合容器對象指的是上面的 ContextHost或者 EngineContainer 接口定義如下:

public interface Container extends Lifecycle {    public void setName(String name);    public Container getParent();    public void setParent(Container container);    public void addChild(Container child);    public void removeChild(Container child);    public Container findChild(String name);}

我們看到了getParentSetParentaddChildremoveChild等方法,這里正好驗證了我們說的組合模式。我們還看到 Container接口拓展了 Lifecycle ,Tomcat 就是通過 Lifecycle 統一管理所有容器的組件的生命周期。通過組合模式管理所有容器,拓展 Lifecycle 實現對每個組件的生命周期管理 ,Lifecycle 主要包含的方法init()、start()、stop() 和 destroy()

2.4、請求定位 Servlet 的過程

一個請求是如何定位到讓哪個 WrapperServlet 處理的?答案是,Tomcat 是用 Mapper 組件來完成這個任務的。

Mapper 組件的功能就是將用戶請求的 URL 定位到一個 Servlet,它的工作原理是:Mapper組件里保存了 Web 應用的配置信息,其實就是容器組件與訪問路徑的映射關系,比如 Host容器里配置的域名、Context容器里的 Web應用路徑,以及 Wrapper容器里 Servlet 映射的路徑,你可以想象這些配置信息就是一個多層次的 Map

當一個請求到來時,Mapper 組件通過解析請求 URL 里的域名和路徑,再到自己保存的 Map 里去查找,就能定位到一個 Servlet。請你注意,一個請求 URL 最后只會定位到一個 Wrapper容器,也就是一個 Servlet

假如有用戶訪問一個 URL,比如圖中的http://user.shopping.com:8080/order/buy,Tomcat 如何將這個 URL 定位到一個 Servlet 呢?

1.首先根據協議和端口號確定 Service 和 Engine。Tomcat 默認的 HTTP 連接器監聽 8080 端口、默認的 AJP 連接器監聽 8009 端口。上面例子中的 URL 訪問的是 8080 端口,因此這個請求會被 HTTP 連接器接收,而一個連接器是屬于一個 Service 組件的,這樣 Service 組件就確定了。我們還知道一個 Service 組件里除了有多個連接器,還有一個容器組件,具體來說就是一個 Engine 容器,因此 Service 確定了也就意味著 Engine 也確定了。

2.根據域名選定 Host。 Service 和 Engine 確定后,Mapper 組件通過 URL 中的域名去查找相應的 Host 容器,比如例子中的 URL 訪問的域名是user.shopping.com,因此 Mapper 會找到 Host2 這個容器。

3.根據 URL 路徑找到 Context 組件。 Host 確定以后,Mapper 根據 URL 的路徑來匹配相應的 Web 應用的路徑,比如例子中訪問的是 /order,因此找到了 Context4 這個 Context 容器。

4.根據 URL 路徑找到 Wrapper(Servlet)。 Context 確定后,Mapper 再根據 web.xml 中配置的 Servlet 映射路徑來找到具體的 Wrapper 和 Servlet。

連接器中的 Adapter 會調用容器的 Service 方法來執行 Servlet,最先拿到請求的是 Engine 容器,Engine 容器對請求做一些處理后,會把請求傳給自己子容器 Host 繼續處理,依次類推,最后這個請求會傳給 Wrapper 容器,Wrapper 會調用最終的 Servlet 來處理。那么這個調用過程具體是怎么實現的呢?答案是使用 Pipeline-Valve 管道。

Pipeline-Valve 是責任鏈模式,責任鏈模式是指在一個請求處理的過程中有很多處理者依次對請求進行處理,每個處理者負責做自己相應的處理,處理完之后將再調用下一個處理者繼續處理,Valve 表示一個處理點(也就是一個處理閥門),因此 invoke方法就是來處理請求的。

public interface Valve {  public Valve getNext();  public void setNext(Valve valve);  public void invoke(Request request, Response response)}

繼續看 Pipeline 接口

public interface Pipeline {  public void addValve(Valve valve);  public Valve getBasic();  public void setBasic(Valve valve);  public Valve getFirst();}

Pipeline中有 addValve方法。Pipeline 中維護了 Valve鏈表,Valve可以插入到 Pipeline中,對請求做某些處理。我們還發現 Pipeline 中沒有 invoke 方法,因為整個調用鏈的觸發是 Valve 來完成的,Valve完成自己的處理后,調用 getNext.invoke() 來觸發下一個 Valve 調用。

其實每個容器都有一個 Pipeline 對象,只要觸發了這個 Pipeline 的第一個 Valve,這個容器里 Pipeline中的 Valve 就都會被調用到。但是,不同容器的 Pipeline 是怎么鏈式觸發的呢,比如 Engine 中 Pipeline 需要調用下層容器 Host 中的 Pipeline。

這是因為 Pipeline中還有個 getBasic方法。這個 BasicValve處于 Valve鏈表的末端,它是 Pipeline中必不可少的一個 Valve,負責調用下層容器的 Pipeline 里的第一個 Valve。

整個過程分是通過連接器中的 CoyoteAdapter 觸發,它會調用 Engine 的第一個 Valve:

@Overridepublic void service(org.apache.coyote.Request req, org.apache.coyote.Response res) {    // 省略其他代碼    // Calling the container    connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);    ...}

Wrapper 容器的最后一個 Valve 會創建一個 Filter 鏈,并調用 doFilter() 方法,最終會調到 Servletservice方法。

前面我們不是講到了 Filter,似乎也有相似的功能,那 ValveFilter有什么區別嗎?它們的區別是:

  • ValveTomcat的私有機制,與 Tomcat 的基礎架構 API是緊耦合的。Servlet API是公有的標準,所有的 Web 容器包括 Jetty 都支持 Filter 機制。
  • 另一個重要的區別是 Valve工作在 Web 容器級別,攔截所有應用的請求;而 Servlet Filter 工作在應用級別,只能攔截某個 Web 應用的所有請求。如果想做整個 Web容器的攔截器,必須通過 Valve來實現。

Lifecycle 生命周期

前面我們看到 Container容器 繼承了 Lifecycle 生命周期。如果想讓一個系統能夠對外提供服務,我們需要創建、組裝并啟動這些組件;在服務停止的時候,我們還需要釋放資源,銷毀這些組件,因此這是一個動態的過程。也就是說,Tomcat 需要動態地管理這些組件的生命周期。

如何統一管理組件的創建、初始化、啟動、停止和銷毀?如何做到代碼邏輯清晰?如何方便地添加或者刪除組件?如何做到組件啟動和停止不遺漏、不重復?

一鍵式啟停:LifeCycle 接口

設計就是要找到系統的變化點和不變點。這里的不變點就是每個組件都要經歷創建、初始化、啟動這幾個過程,這些狀態以及狀態的轉化是不變的。而變化點是每個具體組件的初始化方法,也就是啟動方法是不一樣的。

因此,Tomcat 把不變點抽象出來成為一個接口,這個接口跟生命周期有關,叫作 LifeCycle。LifeCycle 接口里定義這么幾個方法:init()、start()、stop() 和 destroy(),每個具體的組件(也就是容器)去實現這些方法。

在父組件的 init() 方法里需要創建子組件并調用子組件的 init() 方法。同樣,在父組件的 start()方法里也需要調用子組件的 start() 方法,因此調用者可以無差別的調用各組件的 init() 方法和 start() 方法,這就是組合模式的使用,并且只要調用最頂層組件,也就是 Server 組件的 init()start() 方法,整個 Tomcat 就被啟動起來了。所以 Tomcat 采取組合模式管理容器,容器繼承 LifeCycle 接口,這樣就可以向針對單個對象一樣一鍵管理各個容器的生命周期,整個 Tomcat 就啟動起來。

可擴展性:LifeCycle 事件

我們再來考慮另一個問題,那就是系統的可擴展性。因為各個組件init()start() 方法的具體實現是復雜多變的,比如在 Host 容器的啟動方法里需要掃描 webapps 目錄下的 Web 應用,創建相應的 Context 容器,如果將來需要增加新的邏輯,直接修改start() 方法?這樣會違反開閉原則,那如何解決這個問題呢?開閉原則說的是為了擴展系統的功能,你不能直接修改系統中已有的類,但是你可以定義新的類。

組件的 init()start() 調用是由它的父組件的狀態變化觸發的,上層組件的初始化會觸發子組件的初始化,上層組件的啟動會觸發子組件的啟動,因此我們把組件的生命周期定義成一個個狀態,把狀態的轉變看作是一個事件。而事件是有監聽器的,在監聽器里可以實現一些邏輯,并且監聽器也可以方便的添加和刪除,這就是典型的觀察者模式。

以下就是 Lyfecycle 接口的定義:

重用性:LifeCycleBase 抽象基類

再次看到抽象模板設計模式。

有了接口,我們就要用類去實現接口。一般來說實現類不止一個,不同的類在實現接口時往往會有一些相同的邏輯,如果讓各個子類都去實現一遍,就會有重復代碼。那子類如何重用這部分邏輯呢?其實就是定義一個基類來實現共同的邏輯,然后讓各個子類去繼承它,就達到了重用的目的。

Tomcat 定義一個基類 LifeCycleBase 來實現 LifeCycle 接口,把一些公共的邏輯放到基類中去,比如生命狀態的轉變與維護、生命事件的觸發以及監聽器的添加和刪除等,而子類就負責實現自己的初始化、啟動和停止等方法。

public abstract class LifecycleBase implements Lifecycle{    // 持有所有的觀察者    private final List<LifecycleListener> lifecycleListeners = new CopyOnWriteArrayList<>();    /**     * 發布事件     *     * @param type  Event type     * @param data  Data associated with event.     */    protected void fireLifecycleEvent(String type, Object data) {LifecycleEvent event = new LifecycleEvent(this, type, data);for (LifecycleListener listener : lifecycleListeners) {    listener.lifecycleEvent(event);}    }    // 模板方法定義整個啟動流程,啟動所有容器    @Override    public final synchronized void init() throws LifecycleException {//1. 狀態檢查if (!state.equals(LifecycleState.NEW)) {    invalidTransition(Lifecycle.BEFORE_INIT_EVENT);}try {    //2. 觸發 INITIALIZING 事件的監聽器    setStateInternal(LifecycleState.INITIALIZING, null, false);    // 3. 調用具體子類的初始化方法    initInternal();    // 4. 觸發 INITIALIZED 事件的監聽器    setStateInternal(LifecycleState.INITIALIZED, null, false);} catch (Throwable t) {    ExceptionUtils.handleThrowable(t);    setStateInternal(LifecycleState.FAILED, null, false);    throw new LifecycleException(    sm.getString("lifecycleBase.initFail",toString()), t);}    }}

Tomcat 為了實現一鍵式啟停以及優雅的生命周期管理,并考慮到了可擴展性和可重用性,將面向對象思想和設計模式發揮到了極致,Containaer接口維護了容器的父子關系,Lifecycle 組合模式實現組件的生命周期維護,生命周期每個組件有變與不變的點,運用模板方法模式。 分別運用了組合模式、觀察者模式、骨架抽象類和模板方法。

如果你需要維護一堆具有父子關系的實體,可以考慮使用組合模式。

觀察者模式聽起來 “高大上”,其實就是當一個事件發生后,需要執行一連串更新操作。實現了低耦合、非侵入式的通知與更新機制。

Container 繼承了 LifeCycle,StandardEngine、StandardHost、StandardContext 和 StandardWrapper 是相應容器組件的具體實現類,因為它們都是容器,所以繼承了 ContainerBase 抽象基類,而 ContainerBase 實現了 Container 接口,也繼承了 LifeCycleBase 類,它們的生命周期管理接口和功能接口是分開的,這也符合設計中接口分離的原則。

三、Tomcat 為何打破雙親委派機制

3.1、雙親委派

我們知道 JVM的類加載器加載 Class 的時候基于雙親委派機制,也就是會將加載交給自己的父加載器加載,如果 父加載器為空則查找Bootstrap 是否加載過,當無法加載的時候才讓自己加載。JDK 提供一個抽象類 ClassLoader,這個抽象類中定義了三個關鍵方法。對外使用loadClass(String name) 用于子類重寫打破雙親委派:loadClass(String name, boolean resolve)

public Class<?> loadClass(String name) throws ClassNotFoundException {    return loadClass(name, false);}protected Class<?> loadClass(String name, boolean resolve)    throws ClassNotFoundException{    synchronized (getClassLoadingLock(name)) {// 查找該 class 是否已經被加載過Class<?> c = findLoadedClass(name);// 如果沒有加載過if (c == null) {    // 委托給父加載器去加載,遞歸調用    if (parent != null) {c = parent.loadClass(name, false);    } else {// 如果父加載器為空,查找 Bootstrap 是否加載過c = findBootstrapClassOrNull(name);    }    // 若果依然加載不到,則調用自己的 findClass 去加載    if (c == null) {c = findClass(name);    }}if (resolve) {    resolveClass(c);}return c;    }}protected Class<?> findClass(String name){    //1. 根據傳入的類名 name,到在特定目錄下去尋找類文件,把.class 文件讀入內存    ...//2. 調用 defineClass 將字節數組轉成 Class 對象return defineClass(buf, off, len);}// 將字節碼數組解析成一個 Class 對象,用 native 方法實現protected final Class<?> defineClass(byte[] b, int off, int len){    ...}

JDK 中有 3 個類加載器,另外你也可以自定義類加載器,它們的關系如下圖所示。

  • BootstrapClassLoader是啟動類加載器,由 C 語言實現,用來加載 JVM啟動時所需要的核心類,比如rt.jarresources.jar等。
  • ExtClassLoader是擴展類加載器,用來加載\jre\lib\ext目錄下 JAR 包。
  • AppClassLoader是系統類加載器,用來加載 classpath下的類,應用程序默認用它來加載類。
  • 自定義類加載器,用來加載自定義路徑下的類。

這些類加載器的工作原理是一樣的,區別是它們的加載路徑不同,也就是說 findClass這個方法查找的路徑不同。雙親委托機制是為了保證一個 Java 類在 JVM 中是唯一的,假如你不小心寫了一個與 JRE 核心類同名的類,比如 Object類,雙親委托機制能保證加載的是 JRE里的那個 Object類,而不是你寫的 Object類。這是因為 AppClassLoader在加載你的 Object 類時,會委托給 ExtClassLoader去加載,而 ExtClassLoader又會委托給 BootstrapClassLoaderBootstrapClassLoader發現自己已經加載過了 Object類,會直接返回,不會去加載你寫的 Object類。我們最多只能 獲取到 ExtClassLoader這里注意下。

3.2、Tomcat 熱加載

Tomcat 本質是通過一個后臺線程做周期性的任務,定期檢測類文件的變化,如果有變化就重新加載類。我們來看 ContainerBackgroundProcessor具體是如何實現的。

protected class ContainerBackgroundProcessor implements Runnable {    @Override    public void run() {// 請注意這里傳入的參數是 " 宿主類 " 的實例processChildren(ContainerBase.this);    }    protected void processChildren(Container container) {try {    //1. 調用當前容器的 backgroundProcess 方法。    container.backgroundProcess();    //2. 遍歷所有的子容器,遞歸調用 processChildren,    // 這樣當前容器的子孫都會被處理    Container[] children = container.findChildren();    for (int i = 0; i < children.length; i++) {    // 這里請你注意,容器基類有個變量叫做 backgroundProcessorDelay,如果大于 0,表明子容器有自己的后臺線程,無需父容器來調用它的 processChildren 方法。if (children[i].getBackgroundProcessorDelay() <= 0) {    processChildren(children[i]);}    }} catch (Throwable t) { ... }

Tomcat 的熱加載就是在 Context 容器實現,主要是調用了 Context 容器的 reload 方法。拋開細節從宏觀上看主要完成以下任務:

  • 停止和銷毀 Context 容器及其所有子容器,子容器其實就是 Wrapper,也就是說 Wrapper 里面 Servlet 實例也被銷毀了。
  • 停止和銷毀 Context 容器關聯的 Listener 和 Filter。
  • 停止和銷毀 Context 下的 Pipeline 和各種 Valve。
  • 停止和銷毀 Context 的類加載器,以及類加載器加載的類文件資源。
  • 啟動 Context 容器,在這個過程中會重新創建前面四步被銷毀的資源。

在這個過程中,類加載器發揮著關鍵作用。一個 Context 容器對應一個類加載器,類加載器在銷毀的過程中會把它加載的所有類也全部銷毀。Context 容器在啟動過程中,會創建一個新的類加載器來加載新的類文件。

3.3、Tomcat 的類加載器

Tomcat 的自定義類加載器 WebAppClassLoader打破了雙親委托機制,它首先自己嘗試去加載某個類,如果找不到再代理給父類加載器,其目的是優先加載 Web 應用自己定義的類。具體實現就是重寫 ClassLoader的兩個方法:findClassloadClass

findClass 方法

org.apache.catalina.loader.WebappClassLoaderBase#findClass;

為了方便理解和閱讀,我去掉了一些細節:

public Class<?> findClass(String name) throws ClassNotFoundException {    ...    Class<?> clazz = null;    try {    //1. 先在 Web 應用目錄下查找類    clazz = findClassInternal(name);    }  catch (RuntimeException e) {   throw e;       }    if (clazz == null) {    try {    //2. 如果在本地目錄沒有找到,交給父加載器去查找    clazz = super.findClass(name);    }  catch (RuntimeException e) {   throw e;       }    //3. 如果父類也沒找到,拋出 ClassNotFoundException    if (clazz == null) {throw new ClassNotFoundException(name);     }    return clazz;}

1.先在 Web 應用本地目錄下查找要加載的類。

2.如果沒有找到,交給父加載器去查找,它的父加載器就是上面提到的系統類加載器 AppClassLoader

3.如何父加載器也沒找到這個類,拋出 ClassNotFound異常。

loadClass 方法

再來看 Tomcat 類加載器的 loadClass方法的實現,同樣我也去掉了一些細節:

public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {    synchronized (getClassLoadingLock(name)) {Class<?> clazz = null;//1. 先在本地 cache 查找該類是否已經加載過clazz = findLoadedClass0(name);if (clazz != null) {    if (resolve)resolveClass(clazz);    return clazz;}//2. 從系統類加載器的 cache 中查找是否加載過clazz = findLoadedClass(name);if (clazz != null) {    if (resolve)resolveClass(clazz);    return clazz;}// 3. 嘗試用 ExtClassLoader 類加載器類加載,為什么?ClassLoader javaseLoader = getJavaseClassLoader();try {    clazz = javaseLoader.loadClass(name);    if (clazz != null) {if (resolve)    resolveClass(clazz);return clazz;    }} catch (ClassNotFoundException e) {    // Ignore}// 4. 嘗試在本地目錄搜索 class 并加載try {    clazz = findClass(name);    if (clazz != null) {if (resolve)    resolveClass(clazz);return clazz;    }} catch (ClassNotFoundException e) {    // Ignore}// 5. 嘗試用系統類加載器 (也就是 AppClassLoader) 來加載    try {clazz = Class.forName(name, false, parent);if (clazz != null) {    if (resolve)resolveClass(clazz);    return clazz;}    } catch (ClassNotFoundException e) {// Ignore    }       }    //6. 上述過程都加載失敗,拋出異常    throw new ClassNotFoundException(name);}

主要有六個步驟:

1.先在本地 Cache 查找該類是否已經加載過,也就是說 Tomcat 的類加載器是否已經加載過這個類。

2.如果 Tomcat 類加載器沒有加載過這個類,再看看系統類加載器是否加載過。

3.如果都沒有,就讓ExtClassLoader去加載,這一步比較關鍵,目的 防止 Web 應用自己的類覆蓋 JRE 的核心類。因為 Tomcat 需要打破雙親委托機制,假如 Web 應用里自定義了一個叫 Object 的類,如果先加載這個 Object 類,就會覆蓋 JRE 里面的那個 Object 類,這就是為什么 Tomcat 的類加載器會優先嘗試用 ExtClassLoader去加載,因為 ExtClassLoader會委托給 BootstrapClassLoader去加載,BootstrapClassLoader發現自己已經加載了 Object 類,直接返回給 Tomcat 的類加載器,這樣 Tomcat 的類加載器就不會去加載 Web 應用下的 Object 類了,也就避免了覆蓋 JRE 核心類的問題。

4.如果 ExtClassLoader加載器加載失敗,也就是說 JRE核心類中沒有這類,那么就在本地 Web 應用目錄下查找并加載。

5.如果本地目錄下沒有這個類,說明不是 Web 應用自己定義的類,那么由系統類加載器去加載。這里請你注意,Web 應用是通過Class.forName調用交給系統類加載器的,因為Class.forName的默認加載器就是系統類加載器。

6.如果上述加載過程全部失敗,拋出 ClassNotFound異常。

3.4、Tomcat 類加載器層次

Tomcat 作為 Servlet容器,它負責加載我們的 Servlet類,此外它還負責加載 Servlet所依賴的 JAR 包。并且 Tomcat本身也是也是一個 Java 程序,因此它需要加載自己的類和依賴的 JAR 包。首先讓我們思考這一下這幾個問題:

1.假如我們在 Tomcat 中運行了兩個 Web 應用程序,兩個 Web 應用中有同名的 Servlet,但是功能不同,Tomcat 需要同時加載和管理這兩個同名的 Servlet類,保證它們不會沖突,因此 Web 應用之間的類需要隔離。

2.假如兩個 Web 應用都依賴同一個第三方的 JAR 包,比如 Spring,那 Spring的 JAR 包被加載到內存后,Tomcat要保證這兩個 Web 應用能夠共享,也就是說 Spring的 JAR 包只被加載一次,否則隨著依賴的第三方 JAR 包增多,JVM的內存會膨脹。

3.跟 JVM 一樣,我們需要隔離 Tomcat 本身的類和 Web 應用的類。

1. WebAppClassLoader

Tomcat 的解決方案是自定義一個類加載器 WebAppClassLoader, 并且給每個 Web 應用創建一個類加載器實例。我們知道,Context 容器組件對應一個 Web 應用,因此,每個 Context容器負責創建和維護一個 WebAppClassLoader加載器實例。這背后的原理是,不同的加載器實例加載的類被認為是不同的類,即使它們的類名相同。這就相當于在 Java 虛擬機內部創建了一個個相互隔離的 Java 類空間,每一個 Web 應用都有自己的類空間,Web 應用之間通過各自的類加載器互相隔離。

2.SharedClassLoader

本質需求是兩個 Web 應用之間怎么共享庫類,并且不能重復加載相同的類。在雙親委托機制里,各個子加載器都可以通過父加載器去加載類,那么把需要共享的類放到父加載器的加載路徑下不就行了嗎。

因此 Tomcat 的設計者又加了一個類加載器 SharedClassLoader,作為 WebAppClassLoader的父加載器,專門來加載 Web 應用之間共享的類。如果 WebAppClassLoader自己沒有加載到某個類,就會委托父加載器 SharedClassLoader去加載這個類,SharedClassLoader會在指定目錄下加載共享類,之后返回給 WebAppClassLoader,這樣共享的問題就解決了。

3. CatalinaClassloader

如何隔離 Tomcat 本身的類和 Web 應用的類?

要共享可以通過父子關系,要隔離那就需要兄弟關系了。兄弟關系就是指兩個類加載器是平行的,它們可能擁有同一個父加載器,基于此 Tomcat 又設計一個類加載器 CatalinaClassloader,專門來加載 Tomcat 自身的類。

這樣設計有個問題,那 Tomcat 和各 Web 應用之間需要共享一些類時該怎么辦呢?

老辦法,還是再增加一個 CommonClassLoader,作為 CatalinaClassloaderSharedClassLoader的父加載器。CommonClassLoader能加載的類都可以被 CatalinaClassLoaderSharedClassLoader使用

四、整體架構設計解析收獲總結

通過前面對 Tomcat 整體架構的學習,知道了 Tomcat 有哪些核心組件,組件之間的關系。以及 Tomcat 是怎么處理一個 HTTP 請求的。下面我們通過一張簡化的類圖來回顧一下,從圖上你可以看到各種組件的層次關系,圖中的虛線表示一個請求在 Tomcat 中流轉的過程。

4.1、連接器

Tomcat 的整體架構包含了兩個核心組件連接器和容器。連接器負責對外交流,容器負責內部處理。連接器用 ProtocolHandler接口來封裝通信協議和 I/O模型的差異,ProtocolHandler內部又分為 EndPointProcessor模塊,EndPoint負責底層 Socket通信,Proccesor負責應用層協議解析。連接器通過適配器 Adapter調用容器。

對 Tomcat 整體架構的學習,我們可以得到一些設計復雜系統的基本思路。首先要分析需求,根據高內聚低耦合的原則確定子模塊,然后找出子模塊中的變化點和不變點,用接口和抽象基類去封裝不變點,在抽象基類中定義模板方法,讓子類自行實現抽象方法,也就是具體子類去實現變化點。

4.2、容器

運用了組合模式 管理容器、通過 觀察者模式 發布啟動事件達到解耦、開閉原則。骨架抽象類和模板方法抽象變與不變,變化的交給子類實現,從而實現代碼復用,以及靈活的拓展。使用責任鏈的方式處理請求,比如記錄日志等。

4.3、類加載器

Tomcat 的自定義類加載器 WebAppClassLoader為了隔離 Web 應用打破了雙親委托機制,它首先自己嘗試去加載某個類,如果找不到再代理給父類加載器,其目的是優先加載 Web 應用自己定義的類。防止 Web 應用自己的類覆蓋 JRE 的核心類,使用 ExtClassLoader 去加載,這樣即打破了雙親委派,又能安全加載。

五、實際場景運用

簡單的分析了 Tomcat 整體架構設計,從 【連接器】 到 【容器】,并且分別細說了一些組件的設計思想以及設計模式。接下來就是如何學以致用,借鑒優雅的設計運用到實際工作開發中。學習,從模仿開始。

5.1、責任鏈模式

在工作中,有這么一個需求,用戶可以輸入一些信息并可以選擇查驗該企業的 【工商信息】、【司法信息】、【中登情況】等如下如所示的一個或者多個模塊,而且模塊之間還有一些公共的東西是要各個模塊復用。

這里就像一個請求,會被多個模塊去處理。所以每個查詢模塊我們可以抽象為 處理閥門,使用一個 List 將這些 閥門保存起來,這樣新增模塊我們只需要新增一個閥門即可,實現了開閉原則,同時將一堆查驗的代碼解耦到不同的具體閥門中,使用抽象類提取 “不變的”功能。

具體示例代碼如下所示:

首先抽象我們的處理閥門, NetCheckDTO是請求信息

/** * 責任鏈模式:處理每個模塊閥門 */public interface Valve {    /**     * 調用     * @param netCheckDTO     */    void invoke(NetCheckDTO netCheckDTO);}

定義抽象基類,復用代碼。

public abstract class AbstractCheckValve implements Valve {    public final AnalysisReportLogDO getLatestHistoryData(NetCheckDTO netCheckDTO, NetCheckDataTypeEnum checkDataTypeEnum){// 獲取歷史記錄,省略代碼邏輯    }    // 獲取查驗數據源配置    public final String getModuleSource(String querySource, ModuleEnum moduleEnum){       // 省略代碼邏輯    }}

定義具體每個模塊處理的業務邏輯,比如 【百度負面新聞】對應的處理

@Slf4j@Servicepublic class BaiduNegativeValve extends AbstractCheckValve {    @Override    public void invoke(NetCheckDTO netCheckDTO) {    }}

最后就是管理用戶選擇要查驗的模塊,我們通過 List 保存。用于觸發所需要的查驗模塊

@Slf4j@Servicepublic class NetCheckService {    // 注入所有的閥門    @Autowired    private Map<String, Valve> valveMap;    /**     * 發送查驗請求     *     * @param netCheckDTO     */    @Async("asyncExecutor")    public void sendCheckRequest(NetCheckDTO netCheckDTO) {// 用于保存客戶選擇處理的模塊閥門List<Valve> valves = new ArrayList<>();CheckModuleConfigDTO checkModuleConfig = netCheckDTO.getCheckModuleConfig();// 將用戶選擇查驗的模塊添加到 閥門鏈條中if (checkModuleConfig.getBaiduNegative()) {    valves.add(valveMap.get("baiduNegativeValve"));}// 省略部分代碼.......if (CollectionUtils.isEmpty(valves)) {    log.info("網查查驗模塊為空,沒有需要查驗的任務");    return;}// 觸發處理valves.forEach(valve -> valve.invoke(netCheckDTO));    }}

5.2、模板方法模式

需求是這樣的,可根據客戶錄入的財報 excel 數據或者企業名稱執行財報分析。

對于非上市的則解析 excel -> 校驗數據是否合法->執行計算。

上市企業:判斷名稱是否存在 ,不存在則發送郵件并中止計算-> 從數據庫拉取財報數據,初始化查驗日志、生成一條報告記錄,觸發計算-> 根據失敗與成功修改任務狀態 。

重要的 ”變“ 與 ”不變“,

  • 不變的是整個流程是初始化查驗日志、初始化一條報告、前期校驗數據(若是上市公司校驗不通過還需要構建郵件數據并發送)、從不同來源拉取財報數據并且適配通用數據、然后觸發計算,任務異常與成功都需要修改狀態。
  • 變化的是上市與非上市校驗規則不一樣,獲取財報數據方式不一樣,兩種方式的財報數據需要適配

整個算法流程是固定的模板,但是需要將算法內部變化的部分具體實現延遲到不同子類實現,這正是模板方法模式的最佳場景。

public abstract class AbstractAnalysisTemplate {    /**     * 提交財報分析模板方法,定義骨架流程     * @param reportAnalysisRequest     * @return     */    public final FinancialAnalysisResultDTO doProcess(FinancialReportAnalysisRequest reportAnalysisRequest) {FinancialAnalysisResultDTO analysisDTO = new FinancialAnalysisResultDTO();		// 抽象方法:提交查驗的合法校驗boolean prepareValidate = prepareValidate(reportAnalysisRequest, analysisDTO);log.info("prepareValidate 校驗結果 = {} ", prepareValidate);if (!prepareValidate) {			// 抽象方法:構建通知郵件所需要的數據    buildEmailData(analysisDTO);    log.info("構建郵件信息,data = {}", JSON.toJSONString(analysisDTO));    return analysisDTO;}String reportNo = FINANCIAL_REPORT_NO_PREFIX + reportAnalysisRequest.getUserId() + SerialNumGenerator.getFixLenthSerialNumber();// 生成分析日志initFinancialAnalysisLog(reportAnalysisRequest, reportNo);		// 生成分析記錄initAnalysisReport(reportAnalysisRequest, reportNo);try {    // 抽象方法:拉取財報數據,不同子類實現    FinancialDataDTO financialData = pullFinancialData(reportAnalysisRequest);    log.info("拉取財報數據完成, 準備執行計算");    // 測算指標    financialCalcContext.calc(reportAnalysisRequest, financialData, reportNo);			// 設置分析日志為成功    successCalc(reportNo);} catch (Exception e) {    log.error("財報計算子任務出現異常", e);			// 設置分析日志失敗    failCalc(reportNo);    throw e;}return analysisDTO;    }}

最后新建兩個子類繼承該模板,并實現抽象方法。這樣就將上市與非上市兩種類型的處理邏輯解耦,同時又復用了代碼。

5.3、策略模式

需求是這樣,要做一個萬能識別銀行流水的 excel 接口,假設標準流水包含【交易時間、收入、支出、交易余額、付款人賬號、付款人名字、收款人名稱、收款人賬號】等字段。現在我們解析出來每個必要字段所在 excel 表頭的下標。但是流水有多種情況:

1.一種就是包含所有標準字段。

2.收入、支出下標是同一列,通過正負來區分收入與支出。

3.收入與支出是同一列,有一個交易類型的字段來區分。

4.特殊銀行的特殊處理。

也就是我們要根據解析對應的下標找到對應的處理邏輯算法,我們可能在一個方法里面寫超多 if else 的代碼,整個流水處理都偶合在一起,假如未來再來一種新的流水類型,還要繼續改老代碼。最后可能出現 “又臭又長,難以維護” 的代碼復雜度。

這個時候我們可以用到策略模式,將不同模板的流水使用不同的處理器處理,根據模板找到對應的策略算法去處理。即使未來再加一種類型,我們只要新加一種處理器即可,高內聚低耦合,且可拓展。

定義處理器接口,不同處理器去實現處理邏輯。將所有的處理器注入到 BankFlowDataHandlerdata_processor_map中,根據不同的場景取出對已經的處理器處理流水。

public interface DataProcessor {    /**     * 處理流水數據     * @param bankFlowTemplateDO 流水下標數據     * @param row     * @return     */    BankTransactionFlowDO doProcess(BankFlowTemplateDO bankFlowTemplateDO, List<String> row);    /**     * 是否支持處理該模板,不同類型的流水策略根據模板數據判斷是否支持解析     * @return     */    boolean isSupport(BankFlowTemplateDO bankFlowTemplateDO);}// 處理器的上下文@Service@Slf4jpublic class BankFlowDataContext {    // 將所有處理器注入到 map 中    @Autowired    private List<DataProcessor> processors;    // 找對對應的處理器處理流水    public void process() { DataProcessor processor = getProcessor(bankFlowTemplateDO);      	 for(DataProcessor processor : processors) {   if (processor.isSupport(bankFlowTemplateDO)) {     // row 就是一行流水數據		 processor.doProcess(bankFlowTemplateDO, row);     break;   } }    }}

定義默認處理器,處理正常模板,新增模板只要新增處理器實現 DataProcessor即可。

/** * 默認處理器:正對規范流水模板 * */@Component("defaultDataProcessor")@Slf4jpublic class DefaultDataProcessor implements DataProcessor {    @Override    public BankTransactionFlowDO doProcess(BankFlowTemplateDO bankFlowTemplateDO) {// 省略處理邏輯細節return bankTransactionFlowDO;    }    @Override    public String strategy(BankFlowTemplateDO bankFlowTemplateDO) {      // 省略判斷是否支持解析該流水      boolean isDefault = true;      return isDefault;    }}

通過策略模式,我們將不同處理邏輯分配到不同的處理類中,這樣完全解耦,便于拓展。

使用內嵌 Tomcat 方式調試源代碼:GitHub: https://github.com/UniqueDong/tomcat-embedded

以上就是解析Tomcat架構原理到架構設計的詳細內容,更多關于Tomcat 架構原理 架構設計的資料請關注其它相關文章!

標簽: Tomcat
相關文章:
主站蜘蛛池模板: 又色又爽又黄的视频软件app | 超碰婷婷 | 国产a视频 | 少妇高潮一区二区三区 | 自拍偷拍激情小说 | 国产偷伦在线 | 欧美性受xxxx白人性爽 | 国产98在线传媒麻豆有限公司 | 色老板最新地址 | 狠狠色噜噜狠狠狠狠69 | 亚洲国产精品久久久久制服 | 日韩少妇白浆无码系列 | 免费无码国产v片在线观看 任我撸在线视频 | 色交视频| 无码人妻一区二区三区在线视频 | 国产寡妇色xxⅹ交肉视频 | 伦理片免费完整片在线观看 | 日韩美一区二区三区 | 深夜福利小视频在线观看 | 久久综合久久综合九色 | jizzjizz中国精品麻豆 | 中文字幕人妻熟在线影院 | 国产精品zjzjzj在线观看 | 中文字幕一区在线观看视频 | 1000部精品久久久久久久久 | 日日躁夜夜躁狠狠躁av麻豆 | 草草福利影院 | 亚洲视频色图 | 国产精品7m凸凹视频分类 | 男人天堂网av| 国产黄色大片 | 伊人情人色综合网站 | 人妻少妇精品一区二区三区 | 午夜爱 | 极品少妇被猛得白浆直流草莓视频 | 日本v片做爰免费视频网站 日本www | 国产精品久久久久久久久久99 | 色噜噜狠狠狠狠色综合久一 | 俄罗斯美女真人性做爰 | 成人免费无码大片a毛片抽搐色欲 | 99r精品视频在线观看 | 国产成人综合欧美精品久久 | 毛片内射-百度 | 黑人巨大国产9丨视频 | 欧美另类国产 | 国产美女爆我菊免费观看88av | 国产成人无码视频一区二区三区 | 99热在线观看精品 | 精品国产一区二区三区在线 | 久久不见久久见免费影院视频 | 欧美一性一乱一交一视频 | 18禁肉肉无遮挡无码网站 | 国产免费黄色录像 | 丝袜脚交一区二区三区 | 在线观看免费视频黄 | 4k岛国高清加勒比av | 色a在线 | 成人免费黄色小视频 | 情一色一乱一欲一区二区 | 亚色中文网 | 男人的天堂免费视频 | 国产女人在线观看 | 中国女人内谢69xxxxⅹ视频 | 任你躁国产老女人 | 白丝久久 | 欧美亚洲影院 | 色婷婷网 | 人与禽物交videos另类 | 亚洲欧洲综合av | 99久久人人爽亚洲精品美女 | 我们的2018在线观看免费高清 | 国语对白嫖老妇videos | 亚洲国产精品久久久久久6q | 国产农村妇女毛片精品久久 | 国产超碰人人爽人人做人人爱 | 色偷偷色噜噜狠狠成人免费视频 | 无码人妻精品一区二区蜜桃色欲 | 在线视频亚洲 | 欧美三级欧美一级 | 欧美高清性xxxxhdvideos | 国产精品国产成人国产三级 | 91久久久久久久国产欧美日韩- | 国产极品美女高潮无套嗷嗷叫酒店 | 中文字幕亚洲在线观看 | 337p人体粉嫩胞高清视频 | 天天综合天天操 | 国产精品99久久久久久猫咪 | 伊人涩| 国产成人一区二区三区在线观看 | 粗了大了 整进去好爽视频 色偷偷亚洲男人的天堂 | 欧美亚洲一区二区三区四区 | 中文在线永久免费观看 | 免费的大尺度在线观看网站 | a级毛片 黄 免费a级毛片 | 国内精品久久久久久99蜜桃 | 女女百合国产免费网站 | jiizzyou性欧美老片 | av中文在线播放 | www色综合 | 亚洲女人天堂 | 一本大道久久久久精品嫩草 | 成人免费黄色 | 东京天堂热av | 久久99操| 国产激情久久久久久熟女老人av | 午夜成人亚洲理伦片在线观看 | 欧美z○zo变态重口另类黄 | 国产三级精品三级在专区 | 国产精品对白交换视频 | www.亚色| 国产一级二级视频 | 欧美一级在线看 | 欧美尹人 | 91色多多| 午夜激情四射 | 麻豆视频国产精品 | 欧美亚洲在线观看 | 成年性生交大片免费看 | 狠狠爱俺也去去就色 | 日韩黄色影片 | 久久亚洲精精品中文字幕早川悠里 | 欧美一级片免费 | 国产黄色一区二区 | 国产精品污www一区二区三区 | 伊人tv| 永久免费网站直接看 | av黄色成人 | 国产实拍会所女技师在线观看 | 精品国产一区二区三区av 性色 | 日本艳妓bbw高潮一19 | 午夜婷婷色 | 国产清纯白嫩初高生视频在线观看 | 中国黄色网址 | 免费一级淫片红桃视频 | 久久久青草婷婷精品综合日韩 | 私人毛片免费高清影视院 | 成人性视频免费看的鲁片 | 国产裸体永久免费视频网站 | 日日日日做夜夜夜夜做无码 | 解开乳罩喂领导吃奶 | 77777_亚洲午夜久久多人 | 在线观看h网站 | 国产麻豆免费观看 | 国产一级内谢 | 色屁屁视频 | 真实国产露脸乱 | 男人边吃奶边揉好爽免费视频 | 欧美大片免费看 | 红色假期黑色婚礼2 | 黄色av网页 | 中文字幕.com | 全国最大成人网 | 亚洲综合欧美在线一区在线播放 | 蜜桃色一区二区三区 | 成人欧美一区二区三区黑人免费 | 专干老熟女视频在线观看 | 一级久久久久久久 | 精品国产影院 | 免费又黄又爽1000禁片 | 国产成人片无码视频在线观看 | 日韩a无v码在线播放 | 国产又黄又大又粗的视频 | 国产色视频在线观看免费 | 亚洲成a人片777777久久 | 亚洲风情亚aⅴ在线发布 | 男女羞羞视频软件 | 女女互慰吃奶互揉的视频 | 国产美女免费网站 | 国产欧美一区二区三区视频在线观看 | 午夜视频在线观看吗 | 黑人粗硬进入过程视频 | 国产网站在线看 | 华人少妇被黑人粗大的猛烈进 | 久久精品欧美日韩 | 福利国产视频 | 永久免费看mv网站入口亚洲 | 狠狠色伊人亚洲综合成人 | 国产精品有限公司 | 国产 欧美 在线 | 国产人妖ts重口系列 | 国产xxxx高清在线观看 | 日本爽快片100色毛片视频 | 肮脏的交易在线观看 | xxxtv性欧美| 征服少妇柔佳系列 | 国产精品国产自产拍高清av | 五月天久久久久 | 欧洲s码亚洲m码精品一区 | 国产女人高潮抽搐叫床涩爱小说 | 久久精品国产精品亚洲艾草网 | 伊人网在线观看 | 老女人黄色片 | 99精品免费久久久久久久久 | 蜜桃久久久| 久久精品水蜜桃av综合天堂 | 丰满白嫩欧洲美女图片 | 日韩精品系列 | 永久免费视频 国产 | 国产hsck在线亚洲 | 在线免费av片 | 性色av无码不卡中文字幕 | 欧美性xxxx最大尺码 | 波多野结衣女同 | 日本女人黄色 | 日批| 免费在线观看a视频 | 柠檬福利第一导航在线 | 日韩欧美一区二区三区免费观看 | 亚洲自拍偷拍区 | 久久国产欧美日韩精品图片 | 国内精品久久久久久久影视红豆 | 国产精品性生活 | 强美女免费网站在线视频 | 中文字幕人妻熟女人妻 | 亚洲成熟丰满熟妇高潮xxxxx | 国产亚洲精品岁国产微拍精品 | 99精品国产成人一区二区 | 狠狠色综合网久久久久久 | 少妇高潮喷水在线观看 | 我要看免费毛片 | 日本jizz在线观看 | 邻居少妇张开双腿让我爽一夜图片 | 国产丰满农村老妇女乱 | 日韩一级片一区二区三区 | 中文字幕看片 | 国产手机在线视频 | 涩涩涩涩涩涩涩涩涩涩 | 国产寡妇一级农村野外战 | 国产精品毛片一区视频播 | yjizz国产| www.狠狠操| 国产精品久久精品 | 中文在线观看免费网站 | 99xav| 明星换脸av一区二区三区网站 | 乱大交做爰xxxⅹ性 乱荡少妇xxhd | 久久久久久av | 色噜噜狠狠狠综合曰曰曰 | 亚洲日本三级 | 91视频区| av导航在线| 丁香花小说手机在线观看免费 | 国产精品美女毛片真酒店 | 午夜丰满少妇高清毛片1000部 | 国产成人av一区二区三区不卡 | 波多野结衣不卡 | 色情久久久av熟女人妻网站 | 亚洲成av人片在线观看天堂无码 | 日本成人免费网站 | 亚洲男人天堂2024 | 成人首页 | 人人综合亚洲无线码另类 | 无码av中文一区二区三区桃花岛 | 韩日在线 | 黑人狂躁日本妞videos在哪里 | 破处视频在线观看 | 欧美a级成人淫片免费看 | 亚洲爽爽网| www日本在线播放 | 天堂资源中文 | 日本肉体xxxx裸体137大胆图 | 亚洲骚| 色片在线播放 | 欧美日韩国产激情 | 久久婷婷综合99啪69影院 | 午夜爱精品免费视频一区二区 | 欧美人吸奶水吃奶水 | 免费国产黄网站在线观看视频 | 一区二区精品在线观看 | www视频在线免费观看 | a级特黄的片子 | 久久久www成人免费毛片女 | 毛片网站在线播放 | 欧美刺激性大交 | 亚洲人成未满十八禁网站 | 忘忧草在线社区www中国中文 | 国产一区二区三区av网站 | 免费精品视频在线观看 | 影音先锋天堂网 | 精品成人免费一区二区在线播放 | 99国产精品久久久久 | 成熟妇人a片免费看网站 | 日本熟妇浓毛 | 亚洲成人一二三 | 天天看黄色 | 东北妇女精品bbwbbw | 亚洲不卡在线视频 | 欧美嫩草影院 | 欧美性福利 | 欧美精品91 | 国产又白又嫩又紧又爽18p | 涩涩视频网站在线观看 | 台湾无码一区二区 | 色屁屁www影院入口免费 | 大乳奶水成人吃91 | 深夜福利国产 | 欧美三日本三级少妇三级99观看视频 | 51精品久久久久久久蜜臀 | 国产美女视频免费观看的软件 | 久久久999精品视频 久久久99久久久国产自输拍 | 欧美另类高清zo欧美 | 久久影音先锋 | aaa日本高清在线播放免费观看 | 国产va精品免费观看 | 少妇被多人c夜夜爽爽 | jzzijzzij亚洲成熟少妇18 jzzijzzij亚洲农村妇女 | 国产精品入口尤物 | 欧美性性性性性色大片免费的 | 女人高潮内射99精品 | www视频在线观看免费 | 国色天香一卡2卡三卡4卡乱码 | 精品久久综合1区2区3区激情 | 国产伦理丿天美传媒av | 少妇一边呻吟一边说使劲视频 | 亚洲 欧美 精品 | 大屁股熟女一区二区三区 | 国产丝袜无码一区二区三区视频 | 男人天堂影院 | 97夜夜澡人人爽人人喊中国片 | av网子| 国产国产精品人在线视 | 大江大河第三部50集在线观看旭豪 | yy111111少妇影院免费观看 | www人人草| 无套日出白浆 | 国产av一区二区三区天堂综合网 | 亚洲 欧美 日韩 在线 | 亚洲精品国产精品国自产观看浪潮 | 成人免费视频国产免费网站 | 一级做a毛片 | 免费特级黄毛片 | 99久久国产福利自产拍 | 波多野结衣国产在线 | 亚洲精品偷拍 | 欧美黄色网络 | 国产ts在线播放 | 黑巨人与欧美精品一区 | 五月婷婷色丁香 | 久草在线新时代的视觉体验 | www.天天色| 国产成人精品久久综合 | 亚洲天堂免费av | 日韩精品视 | 国产精品国产三级国产aⅴ9色 | 国产99久久久国产精品免费看 | 嫩草影院一区二区 | 欧美精品一区二区三区四区在线 | av天天在线| 欧美精品与人动性物交免费看 | 熟妇的味道hd中文字幕 | 国产精品免费福利 | 免费一区二区在线观看 | 国内精品视频在线观看 | 久久人人添人人爽添人人88v | 国产福利在线永久视频 | 国产在线视频卡一卡二 | 欧美黑人两根巨大挤入 | 色狠狠一区二区三区香蕉 | 亚洲欧洲日产国码久在线 | 色偷偷av一区二区 | 九九在线视频 | 国产 日韩 欧美 中文 在线播放 | 中文字幕日韩欧美一区二区 | 国产精品久久久久777777 | 国产精品久久久久久久久久久久久久久久久久 | 亚洲精品一区二区三区在线观看 | 黄色小视频链接 | 国产成人久久精品亚洲 | 91污在线观看 | 夫妻性生活黄色大片 | 黄91在线观看 | 亚洲国产精品久久久久秋霞不卡 | 亚洲一级一级一级 | 国产野外作爱视频播放 | 男女啪啪免费 | a黄色片网站 | 国产乱xxxxx987国语对白 | 成人国产精品齐天大性 | 国色天香一区二区 | 性做久久久久久免费观看欧美 | 芭乐视频色| 一级黄网站 | 在线免费精品视频 | 欧美人妖另类aaaaa | 日韩综合网站 | 性欧美在线视频免费观看 | 文中字幕一区二区三区视频播放 | 欧美国产一区二区 | 亚洲欧洲日韩av | 麻豆传媒一区二区 | 成人性生交大片免费看- | 无码人妻丰满熟妇啪啪欧美 | 野花社区视频在线观看 | 老司机午夜精品 | 日本少妇五级床片 | 成人18夜夜网深夜福利网 | 777色婷婷视频二三区 | 欧美亚洲在线视频 | 岛国精品在线播放 | 99热只有 | 田中瞳av| 国内精品久久久久影视 | 欧美无专区| 欧美福利精品 | 亚洲成人网页 | 国产免费黄色小视频 | 91性高湖久久久久久久久_久久99 | 欧美男女交配视频 | 日本在线二区 | 日韩亚洲制服丝袜中文字幕 | 欧美自拍偷拍第一页 | 国产精品成人久久 | 久久国产精品久久精品国产 | 中文字幕在线视频精品 | 国产真实交换配乱淫视频, 国产真实精品久久二三区 国产真实乱免费高清视频 国产制服丝袜一区 | 91视频这里只有精品 | 免费人成视频在线观看网站 | 亚洲午夜精品久久久久久浪潮 | 久久久久香蕉国产线看观看伊 | 五月天堂色 | 中文字幕女同 | 精精国产 | a资源在线观看 | 中国少妇乱子伦视频播放 | 中文字幕一区二区三区视频 | 伊人嫩草久久欧美站 | 欧美爱爱网站 | 艹逼久久 | 成人三级黄色 | 日本少妇做爰全过程二区 | 亚洲天堂手机在线观看 | 欧美成人福利 | 一个人看的www免费视频在线观看 | 精品无码一区二区三区爱欲 | 成人在线观看一区二区 | 日日摸日日碰夜夜爽av | 在线精品亚洲一区二区佐佐木明希 | 国产自产c区 | 国产午夜激情 | 婷婷五月综合激情中文字幕 | 麻豆乱淫一区二区三区 | 午夜老司机福利 | 天天色播 | 亚洲三级久久 | 人人射人人澡 | 午夜视频福利网站 | 国产freexxxx性播放麻豆 | 日韩手机视频 | 有夫之妇3高潮中文字幕 | 男女黄床上色视频 | 国外成人在线视频 | 欧美天堂一区二区三区 | 欧美日韩一区二区三区精品 | 97在线免费观看视频 | 久久大香焦 | 成人午夜精品无码区久久 | 欧美在线性视频 | 免费在线观看日韩av | 荷兰成人性大交视频 | 人人妻人人澡人人爽人人精品浪潮 | www久久亚洲 | 免费国产又色又爽又黄的网站 | 成人一区二 | 久久人妖 | 人人妻人人澡人人爽欧美精品 | 91亚洲国产成人精品一区二三 | 在办公室被c到呻吟的动态图 | 性高朝大尺度少妇大屁股 | 久久婷婷五月综合97色直播 | 亚欧中文字幕 | 蜜桃视频一区二区 | 性中国妓女毛茸茸视频 | 狠狠色丁香婷婷久久综合不卡 | ass色喜ass国模人体 | 日本疯狂做爰xxxⅹ高潮视频 | 日本免费一区二区三区中文字幕 | 亚洲国产999 | 午夜免费在线观看 | www.少妇影院.com | 少妇精xxxxx| 国产97色在线 | 亚洲 | 中文字幕久热 | 久草青青视频 | 国产免费黄色大片 | 少妇人妻一级a毛片 | 全黄性性激高免费视频 | 激情开心成人网 | 精品国产乱码久久久久久绯色 | 国产成人久久久精品免费澳门 | 国产a大片 | 国产目拍亚洲精品区一区 | 欧美日韩免费观看视频 | 久久日韩乱码一二三四区别 | 日本丰满妇人成熟免费中文字幕 | 国产91精品一区二区麻豆亚洲 | 天堂欧美城网站 | 亚洲欧美日韩第一页 | 懂色av粉嫩av色老板 | 成年18网站免费进入夜色 | 色 综合 欧美 亚洲 国产 | 一级黄色片毛片 | 九色视频丨porny丨丝袜 | 亚色中文网 | 综合网在线| 亚洲日韩看片无码超清 | www亚洲一区| 插鸡网站在线播放免费观看 | 免费观看av的网站 | 在线观看亚洲色图 | 91国偷自产一区二区三区蜜臀 | 欧美黑人激情性久久 | 最新的中文字幕 | 美女爽爽爽 | 亚洲香蕉av在线一区二区三区 | 亚洲一级久久 | 极品无码av国模在线观看 | 小草久久久久久久久爱六 | 欧美在线看片a免费观看 | 日本少妇做爰大尺裸体网站 | 久草免费福利 | 欧美自偷自拍 | 久久精品人妻无码一区二区三区 | 四川妇女偷人毛片大全 | 亚洲色图网址 | 国产一区二区三区四区五区 | 亚洲最大免费视频 | 香蕉视频99 | 国产又爽又黄视频 | 久久久久久久 | 亚洲手机av | 三上悠亚人妻中文字幕在线 | 亚洲国产精品一区 | 亚洲 小说区 图片区 都市 | 欧美做爰全过程免费看 | 性生交大片免费全片 | 波多野结衣不打码视频 | 人妻熟女一区 | 久艹伊人| 日本免费a级片 | 中国少妇xxxxxbbbbb | 免费观看全黄做爰大片小说 | 男人的天堂免费视频 | 隔壁人妻偷人bd中字 | 久久久久99精品国产片 | 中文字幕亚洲色图 | 久久影院午夜理论片无码 | 亚洲国产精彩中文乱码av | 欧美性做爰毛片 | 欧美性淫爽ww久久久久无 | 欧美精品色呦呦 | 免费看的一级视频 | 国产日韩在线免费观看 | 美女伦理水蜜桃4 | 久久精品www人人爽人人 | 欧美一区二区三区粗大 | 国产亚洲精品久久久久动 | 中文字幕亚洲无线码在线一区 | 国产三级大片 | 手机在线中文字幕 | 午夜精品久久久久久中宇牛牛影视 | 女人与黑人做爰啪啪 | 黄色一级大片免费看 | 在线麻豆 | 各种高潮超清特写tv | 少妇出轨精品中出一区二区 | 97精品在线观看 | 欧美一级日韩一级 | 免费三片在线视频 | 刘亦菲毛片一区二区三区 | 午夜精品久久久久久久99热额 | 亚洲丝袜av | 亚洲视频在线观看一区二区 | 日本三级中文字幕在线观看 | 动漫av一区二区 | 91亚洲精品久久久中文字幕 | 国产一级性生活片 | 另类小说色 | 巨胸喷奶水视频www免费网站 | 欧美色视频在线 | 亚洲一级免费视频 | 亚洲成人美女xvideos | 先锋影音资源2中文字幕 | 日韩不卡高清视频 | 刘亦菲乱码一区二区三区 | 熟妇人妻无码xxx视频 | 噼里啪啦国语版在线观看 | 欧美高潮在线 | 国产一区日韩二区欧美三区 | 羞羞色视频| www..com色| 成人一级毛片 | 李华月全部毛片 | 韩国伦理中文字幕 | 亚洲一区二区日本 | 欧美成人不卡视频 | 少妇出轨精品中出一区二区 | 亚洲一二三视频 | 亚洲黄色一区二区 | 国产人妻人伦精品1国产盗摄 | 午夜dj在线观看免费视频 | youjizz亚洲女人| youjizz.com国产| 韩国视频一区 | 国产剧情自拍 | 91中文字幕在线 | 亚洲一区二区三区日本久久九 | 亚洲国产无线乱码在线观看 | 伊人久操| 91视频国 | 亚洲码国产精品高潮在线 | 中文字幕日韩三级 | 毛片直接看| 成人在线免费视频观看 | 成人影片麻豆国产影片免费观看 | 成人性生交大片免费看r链接 |