非尋常方式學習ApacheTomcat架構及10.0.12源碼編譯
概述
開啟部落格分享已近三個月,感謝所有花時間精力和小編一路學習和成長的夥伴們,有你們的支援,我們繼續再接再厲
**本人部落格網站 **IT小神 www.itxiaoshen.com
定義
Apache Tomcat軟體是Jakarta Servlet、Jakarta Server Pages、Jakarta Expression Language、Jakarta WebSocket、Jakarta annotation和Jakarta Authentication規範的開源實現。簡單來說也是一個基於Servlet、JSP規範的輕量級Web應用伺服器,在中小型系統和並發訪問用戶不是很多的場合下被普遍使用,也是大部分Java技術棧開發和調試Web程式的首選,目前官方最新版本為10.0.12 Released,而10.1.0-M6 Released還是alpha階段。
Tomcat最初是由Sun的軟體架構師詹姆斯·鄧肯·戴維森開發的,後來他幫助將其變為開源項目,並由Sun貢獻給Apache軟體基金會,成為Apache的定級項目
Tomcat、Jetty、Undertow這幾個都是非常有名實現Servlet規範的應用伺服器,Tomcat本身也是業界上非常優秀的中間件,簡單可可以將Tomcat看成是一個Http伺服器+Servlet容器,Servlet 容器是管理和運行 Servlet 的;相信大家對這隻三腳貓Logo都是非常熟悉了,不管是在學校和還是工作都經常使用它,特別是Java程式設計師最初學習編程都經歷過在Idea或Eclipse中配置Tomcat啟動Web項目,當然還可以通過基於tomcat maven插件或者SpringBoot內嵌web容器(我們在前面《Spring Boot淺聊入門v2.5.3文章》說到Spring Boot內嵌Tomcat容器其實就是New 創建Tomcat的實例)方式調試運行。
Servlet簡介
大家都非常了解Java Web的三大組件Servlet,Filter,Listener,但我們本篇決定不展開聊Servlet規範部分,當前面試官也會讓你談談對於Servlet的理解,我們這裡只是簡單提下後續有時間再針對Servlet相關知道再來深入研究分析,這裡主要為了學習tomcat架構實現而簡單說下Servlet容器工作流程:
- Web客戶向Servlet容器(比如tomcat)發出Http請求;
- Servlet容器解析Web客戶的Http請求;
- Servlet容器創建一個HttpRequest對象,在這個對象中封裝Http請求資訊;
- Servlet容器創建一個HttpResponse對象;
- Servlet容器調用HttpServlet的service方法,把HttpRequest和HttpResponse對象作為service方法的參數傳給HttpServlet對象;
- HttpServlet事實上是servlet的一種子類實例也是最一般的實例。當編寫一個servlet時,必須直接或間接實現servlet介面,最可能實現的方法就是擴展javax.servlet.genericservlet或javax.servlet.http.httpservlet,其中genericservlet類提供了servlet介面的基本實現,httpservlet類擴展了genericservlet並且提供了servlet介面中具體於http的實現。
- HttpServlet調用HttpRequest的有關方法,獲取HTTP請求資訊;
- HttpServlet調用HttpResponse的有關方法,生成響應數據;
- 一般通過HttpServletRequest和HttpServletResponse獲取HTTP請求資訊和返迴響應。事實上servlet理論上可以處理多種形式的請求響應形式 http只是其中之一 所以HttpServletRequest HttpServletResponse分別是ServletRequest和ServletResponse的子類。一般,HttpServlet對應HttpServletRequest和HttpServletResponse。
- Servlet容器把HttpServlet的響應結果傳給Web客戶。
官方用戶指南
Tomcat10.0官方用戶指南 //tomcat.apache.org/tomcat-10.0-doc/index.html
簡單安裝
#安裝Tomcat需要先保證有安裝JDK環境,建議JDK版本為8以上,JDK17現在都已可用了
#解壓文件
unzip apache-tomcat-10.0.12.zip
#進入目錄
cd apache-tomcat-10.0.12/bin
#啟動tomcat
./startup.sh
#查看默認配置8080埠服務是否啟動
netstat -lntp | grep 8080
架構源碼剖析
整體架構
Tomcat源碼中大量使用模板方法和適配器的設計模式,封裝很多的組件,組件之間呈現出明顯的層級關係,一層套著一層,這就是經典的套娃式架構設計;個人推薦從tomca配置文件開始理解其架構這個也是基於套娃式架構設計的優點得來。Tomcat 模組分層結構及相關模組的功能說明如下圖:
Tomcat 核心組件架構圖如下所示:
上面有些功能我們可以通過上面的1.3章節提供的官方用戶指南查閱到相關資訊,這裡簡單羅列幾條說明
- Listener 組件
- 可以在 Tomcat 生命周期中完成某些容器相關的監聽器。
- JNDI
- JNDI是 Java 命名與目錄介面,是屬於 J2EE 規範的,Tomcat 對其進行了實現。JNDI 在 J2EE 中的角色就是「交換機」,即 J2EE 組件在運行時間接地查找其他組件、資源或服務的通用機制(你可以簡單理解為給資源取個名字,再根據名字來找資源)。
- Cluster 組件
- 提供了集群功能,可以將對應容器需要共享的數據同步到集群中的其他 Tomcat 實例中。
- Realm 組件
- 提供了容器級別的用戶-密碼-許可權的數據對象,配合資源認證模組使用。
- Loader 組件
- Web 應用載入器,用於載入 Web 應用的資源,它要保證不同 Web 應用之間的資源隔離。
- Manager 組件
- Servlet 映射器,它屬於 Context 內部的路由映射器,只負責該 Context 容器的路由導航。
Catalina 是 Tomcat 中的一個重要組件,它負責的是解析 Tomcat 的配置文件(server.xml),以此來創建伺服器 Server 組件並進行管理。下面是Tomcat的conf目錄下大名鼎鼎server.xml核心配置的內容結構,Server-Service-(Connector+Engine),在GlobalNamingResouce 域中可以定義全局資源 tomcat-user.xml,在web.xml文件中也有常見如session-config配置session超時時間默認是30分鐘。
<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
<Listener className="org.apache.catalina.startup.VersionLoggerListener" />
<GlobalNamingResources>
<Resource name="UserDatabase" auth="Container"
type="org.apache.catalina.UserDatabase"
description="User database that can be updated and saved"
factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
pathname="conf/tomcat-users.xml" />
</GlobalNamingResources>
<Service name="Catalina">
<Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
maxThreads="150" minSpareThreads="4"/>
<Connector executor="tomcatThreadPool"
port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
</Connector>
<Engine name="Catalina" defaultHost="localhost">
<Realm className="org.apache.catalina.realm.LockOutRealm">
<Realm className="org.apache.catalina.realm.UserDatabaseRealm"
resourceName="UserDatabase"/>
</Realm>
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true">
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
</Host>
</Engine>
</Service>
</Server>
- Server :代表了一個 Tomcat 實例,包含了 Servlet 容器以及其他組件,負責組裝並啟動 Servlet 引擎、Tomcat 連接器。每個Tomcat都只有一個Server,表示整個服務環境。一個Server中可以有多個Service。Server就管理多個Service。tomcat源碼中服務繼承自LifeCycle,tomcat服務組件頂層介面,有唯一實現StandardServer。
- Service:服務是 Server 內部的組件,一個Server中可以有多個Service,它將若干個 Connector 組件綁定到一個 Container,每一個Service中可以有多個Connector和一個Container(Engine)。
- Connector主要用來接收請求,解析請求內容,封裝request和response,然後將準備好的數據交給Container處理。
- Container就是我們常說的容器,裡面可以有多個Host,一個host表示一個虛擬主機,就是一個對應一個WebApps. 最後Container處理完請求之後會將響應內容返回給Connecter,再由Connecter返回給客戶端。
- Executor 執行器:tomcat執行緒池的配置,提供給Connector。
- Connecter:負責處理用戶的 servlet 請求,並返回對象給 web 用戶的模組
我們知道tomcat是處理http的請求,意味著tomcat使用的是 HTTP 協議進行數據傳輸,而HTTP 協議是一種應用層協議,其本質就是一種瀏覽器與伺服器之間約定好的通訊格式,因此tomcat作為一個Http伺服器需要包含接受連接、解析請求數據、處理請求和發送響應這幾大塊功能。在這裡我們再分析tomcat最核心的兩個功能:
- 處理 Socket 連接,負責網路位元組流與 Request 和 Response 對象的轉化。
- 載入和管理 Servlet,由 Servlet 具體負責處理 Request 請求。
Tomcat 最底層使用的是Socket進行連接的,所以這裡也涉及網路IO模型,通過socket監聽本機埠,接收和處理監聽埠的網路請求,Request和Response是按照Http協議來封裝的,所以Connector同時需要實現TCP/IP協議和Http協議;基於核心功能Tomcat 設計了兩個核心組件連接器(Connector)和容器(Container)來分別實現,連接器複雜對外接收和解析請求,封裝request和response,最後把數據交給Containner,容器負責內部處理。
- 網路通訊。
- 應用層協議解析。
- Tomcat Request/Response 與 ServletRequest/ServletResponse 的轉化。
連接器
連接器主要需要完成以下三個核心功能:
- socket 通訊,也就是網路通訊。
- 應用層協議解析,解析處理應用層協議,封裝成一個 Request 對象。
- 將 Request 轉換為 ServletRequest,將 Response 轉換為 ServletResponse。
Tomcat 通過 EndPoint、Processor 和 Adapter這 3 個組件來實現連接器,這三個組件之間通過抽象介面進行交互。從一個請求的正向流程來看, Endpoint 負責提供請求位元組流給 Processor,Processor 負責提供 Tomcat 定義的 Request 對象給 Adapter,Adapter 負責提供標準的 ServletRequest 對象給 Servlet 容器:
- Endpoint 和 Processor 可以自由組合,放在一起抽象成了 ProtocolHandler 組件,連接器用 ProtocolHandler 來處理網路連接和應用層協議。Connector中具體用事件處理器來處理請求【ProtocoHandler】;不同的ProtocoHandler代表不同的連接類型【所以一個Service中可以有多個Connector】 例如:Http11Protocol使用普通的Socket來連接的,Http11NioProtocol使用NioSocket連接。
- EndPoint:對接 I/O 模型,提供位元組流給Processor,監聽通訊埠,是對傳輸層的抽象,處理底層Socket的網路連接,用來實現 TCP/IP 協議的。 是一個介面,對應的抽象類為AbstractEndPoint,有很多實現類,比如NioEndPoint,JIoEndPoint等。在其中有兩個組件,一個 是Acceptor,另外一個是SocketProcessor。 Acceptor用於監聽Socket連接請求,SocketProcessor用於處理接收到的Socket請求。EndPoint 接收到 Socket 連接後,生成一個 SocketProcessor 任務提交到執行緒池去處理,SocketProcessor 的 Run 方法會調用 Processor 組件去解析應用層協議,Processor 通過解析生成 Request 對象後,會調用 Adapter 的 Service 方法。
- Processor:對接應用層協議,提供Tomcat Request對象給Adapter,Processor是用於實現HTTP協議的,也就是說Processor是針對應用層協議的抽象。 Processor接受來自EndPoint的Socket,然後解析成Tomcat Request和Tomcat Response對象,最後通過Adapter 提交給容器。 對應的抽象類為AbstractProcessor,有很多實現類,比如AjpProcessor、Http11Processor等。
- Adapter:遵循 Servlet 規範,提供ServletRequest給容器,ProtocolHandler介面負責解析請求並生成 Tomcat Request 類。 需要把這個 Request 對象轉換成 ServletRequest。 Tomcat 引入CoyoteAdapter,這是適配器模式的經典運用,連接器調用 CoyoteAdapter 的 sevice 方法,傳入的是 Tomcat Request 對象,CoyoteAdapter 負責將 Tomcat Request 轉成 ServletRequest,再調用容器的 service 方 法,將請求適配到Servlet容器 Container 架構。
容器
容器主要用於封裝和管理Servlet以及具體處理Request請求。Tomcat 中設計四大Servlet容器組件,分別是Engine、Host、Context、Wrapper—>Servlet,這 4 種容器不是平行關係,而是父子關係。在tomcat源碼org.apache.catalina.core.StandardEngine四個都有其標準實現,每一個容器都有一個 Pipeline 對象。
- Engine:整個 Catalina 的 Servlet 容器引擎,用來管理多個虛擬站點,一個Service最多只能有一個Engine,但是一個引擎可包含多個 Host。
- Host:代表的是一個虛擬主機,或者說一個站點,可以給 Tomcat 配置多個虛擬主機地址,而一個虛擬主機下可包含多個 Context。
- Context:表示一個 Web 應用程式,相當於我們在webapp下的應用,一個 Web 應用通常有多個 Servlet,一個Web應用可包含多個 Wrapper子容器。
- Wrapper:servlet包裝類,包裝Servlet負責管理整個 Servlet 的生命周期,包括裝載、初始化、資源回收等,每一個Wraper都包裝著Servlet。
PipeLine和Valve(管道和閥門)
Engine、Host、Context、Wrapper容器都繼承ContainerBase,而ContainerBase有StandardPipeLine對象,每個容器構造方法setBasic設置默認Valve(StandardEngineValve),PipeLine和Valve組件的init、start。
Connector使用容器Container處理請求時,connector.getService().getContainer().getPipeLine().getFirst().invoke(request,response);
啟動流程
整個 Tomcat 其實就是一個 Catalina 實例,Tomcat 啟動的時候會初始化這個實例,Catalina 實例通過載入server.xml 完成其他實例的創建,創建並管理一個 Server,Server 創建並管理多個服務, 每個服務又可以有多個Connector 和一個 Container。
我們知道啟動tomcat是執行startup.sh腳本文件,這個腳本文件里又執行catalina.sh最終是執行org.apache.catalina.startup.Bootstrap類。
因此得知Tomcat從Bootstrap類main方法開始,以鏈的方式逐級調用各模組的init方法進行初始化, 待各個模組都初始化後, 又會逐級調用各個模組的start()方法啟動各個模組;commonLoader、catalinaLoader、sharedLoader這三個類載入器打破雙親委派。下面是tomcat啟動流程時序圖:
內嵌示例
接下來我們演示內嵌tomcat運行示例,創建maven項目,pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="//maven.apache.org/POM/4.0.0"
xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="//maven.apache.org/POM/4.0.0 //maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-extend</artifactId>
<groupId>com.itxs</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>tomcat-test</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>8.5.28</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper</artifactId>
<version>8.5.28</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.3.9</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
創建一個可以測試Servlet,MyFirstServlet.java
package com.itxs;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class MyFirstServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.getWriter().print("hello tomcat servlet,access success!!!");
}
}
TomcatApplication.java文件
package com.itxs;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.core.AprLifecycleListener;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.startup.Tomcat;
import javax.servlet.ServletException;
import java.io.File;
public class TomcatApplication {
public static int TOMCAT_PORT = 8080;
public static String TOMCAT_HOSTNAME = "127.0.0.1";
public static String WEBAPP_PATH = "src/main";
public static void main(String[] args) throws LifecycleException {
TomcatApplication.run();
}
public static void run() throws LifecycleException {
Tomcat tomcat = new Tomcat();
tomcat.setPort(TomcatApplication.TOMCAT_PORT);
tomcat.setHostname(TomcatApplication.TOMCAT_HOSTNAME);
tomcat.setBaseDir("."); // tomcat 資訊保存在項目下
StandardContext myContext = null;
try {
myContext = (StandardContext) tomcat.addWebapp("/itxs", System.getProperty("user.dir") + File.separator + TomcatApplication.WEBAPP_PATH);
myContext.setReloadable(false);
// 上下文監聽器
myContext.addLifecycleListener(new AprLifecycleListener());
// 註冊servlet
tomcat.addServlet("/itxs", "myFirstServlet",new MyFirstServlet());
// servlet mapping
myContext.addServletMappingDecoded("/first.do", "myFirstServlet");
tomcat.start();
tomcat.getServer().await();
} catch (ServletException e) {
e.printStackTrace();
}
}
}
運行main方法
並訪問//127.0.0.1:8080/itxs/first.do,收到結果頁面
源碼編譯
官方下載tomcat源碼,目前最新穩定版本為10.0.12
#由於tomcat源碼依賴jakartaee-migration模組,而jakartaee-migration未發布到maven repository,我們需要git clone到本地,再mvn install來部署解決tomcat源碼的編譯問題,//github.com/apache/tomcat-jakartaee-migration
#解壓文件,進入到tomcat-jakartaee-migration-main目錄,地址欄上輸入cmd進入命令行窗口並且自動進入當前目錄
#執行maven安裝到本地倉庫
mvn clean install
#加壓apache-tomcat-10.0.12-src.zip的文件,Tomcat源碼並非maven項目結構,但可以通過pom指定java程式碼目錄(無需按照src/main結構來),項目目錄下創建pom.xml文件,內容如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="//maven.apache.org/POM/4.0.0"
xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="//maven.apache.org/POM/4.0.0 //maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.github.sources</groupId>
<artifactId>source-tomcat</artifactId>
<version>10.0-SNAPSHOT</version>
<name>source-tomcat</name>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>org.apache.ant</groupId>
<artifactId>ant</artifactId>
<version>1.10.1</version>
</dependency>
<dependency>
<groupId>wsdl4j</groupId>
<artifactId>wsdl4j</artifactId>
<version>1.6.2</version>
</dependency>
<dependency>
<groupId>javax.xml</groupId>
<artifactId>jaxrpc</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>org.eclipse.jdt</groupId>
<artifactId>org.eclipse.jdt.core</artifactId>
<version>3.25.0</version>
</dependency>
<dependency>
<groupId>org.eclipse.jdt.core.compiler</groupId>
<artifactId>ecj</artifactId>
<version>4.6.1</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>jakartaee-migration</artifactId>
<version>0.2.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>biz.aQute.bnd</groupId>
<artifactId>biz.aQute.bndlib</artifactId>
<version>5.2.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>Tomcat10.0</finalName>
<sourceDirectory>java</sourceDirectory>
<testSourceDirectory>test</testSourceDirectory>
<resources>
<resource>
<directory>java</directory>
</resource>
</resources>
<testResources>
<testResource>
<directory>test</directory>
</testResource>
</testResources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.1</version>
<configuration>
<encoding>UTF-8</encoding>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
#由於源碼apache-tomcat-10.0.12的pom文件里的jakartaee-migration的版本為0.2.1-SNAPSHOT,我們改為上面tomcat-jakartaee-migration-main的項目的版本1.0.1-SNAPSHOT
#我們在JDTCompiler里注釋下面三行源碼, 不能會報沒有 CompilerOptions.VERSION_16
// settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_16);
// settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_16);
// settings.put(CompilerOptions.OPTION_Compliance, CompilerOptions.VERSION_16);
前面章節我們已提到tomcat main函數的入口在org.apache.catalina.startup.Bootstrap里,知道main函數
會遇到測試類的報錯如下,可以嘗試運行忽略測試類,或者直接刪除maven compile時產生的test文件夾,我們這裡直接刪除test文件夾
由於目前tomcat啟動找不到配置文件,因此我們在源碼根目錄下創建source文件夾,並將conf和webapps這兩個目錄轉移到source文件夾中
然後在運行設置里添加如下vm參數
-Dcatalina.home=F:\develop\apache-tomcat-10.0.12-src\source
-Dcatalina.base=F:\develop\apache-tomcat-10.0.12-src\source
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
-Djava.util.logging.config.file=F:\develop\apache-tomcat-10.0.12-src\source\conf\logging.properties
配置好指定的catalina.home和catalina.base以及日誌相關jvm參數後,啟動Tomcat,到這裡,我們是可以啟動tomcat程式,但是訪問//localhost:8080/ 會報500錯誤,啟動Tomcat BootStrap時未載入JSP編譯器JasperInitializer
#在org.apache.catalina.startup.ContextConfig#configureStart中
webConfig(); // 這句下面添加如下初始化語句
context.addServletContainerInitializer(new JasperInitializer(), null);
重新啟動Tomcat,查看//localhost:8080,出現tomcat的頁面
性能調優
關於性能調優方面可以考慮一下幾個:
- 記憶體:jvm參數,這個是實際生產中用的較多的。
- 並發優化:connector開啟執行緒池及執行緒池優化;
- 快取優化:connector 壓縮和客戶端連接超時;
- IO優化:protocol 選擇BIO NIO NIO2(AIO);
- 組件優化:APR sendfile epoll openssl Tomcat native;
我們本篇只是簡單理解tomcat架構和搭建tomcat源碼調試環境,後續有時間我們再對tomcat源碼和設計模式做進一步剖析和分享