Java後端自頂向下方法——Tomcat初步
(一)瞭解Tomcat
講了這麼多,終於講到我們的Tomcat(不就是湯姆貓?我也不知道爲啥要起這個名字)了。首先我們要明白Tomcat到底是什麼?Tomcat其實就是就是一個Servlet容器。一個Servlet容器對於發送到每個Servlet的HTTP請求會完成以下事情:
(1)當Servlet 第一次被調用的時候,加載了Servlet類並調用它的init方法(僅調用一次)
(2)響應每次請求的時候,構建ServletRequest和ServletResponse實例。
(3)調用Servlet的service方法,將ServletRequest對象和ServletResponse對象當作參數傳入。
(4)當servlet類關閉的時候,調用Servlet的destroy方法,並卸載Servlet類。
因此,Tomcat和Servlet是密不可分的,瞭解Servlet才能幫助我們更好的瞭解Tomcat。首先我先把Tomcat的原本的配置文件(註釋已經都去掉了)寫在這,方便參考:
<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
<Listener className="org.apache.catalina.startup.VersionLoggerListener" />
<Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
<Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
<Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
<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">
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
<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>
(二)Tomcat頂層結構
我們先來看這張圖,裏面有Server、Service、Connector、Container之類的東西,我們就挑選一些重要的來講。
1. Server
Server是服務器的意思,代表整個Tomcat服務器,一個Tomcat只有一個Server。Server是Tomcat最頂層的容器,Server中包含至少一個Service組件,用於提供具體服務。Tomcat中其標準實現是:org.apache.catalina.core.StandardServer類。
我們可以思考一下,我們的Tomcat服務器主要要實現哪些功能。首先是接收客戶端的請求,然後解析請求,完成相應的業務邏輯,然後把處理後的結果返回給客戶端。服務器一般需要提供兩個基本方法,一個start打開服務Socket連接,監聽服務端口,一個stop停止服務釋放網絡資源。
但是這種將監聽請求可處理請求放在一起的思路會影響擴展性,比如我們不想用HTTP了,我們想換一個網絡協議,那我們的Tomcat就需要大量的修改,顯然是很不方便的。所以,Tomcat開發者將監聽請求和處理請求分開,這就有了我們的Connector模塊和Container模塊。
2. Connector
Connector是連接器,用於監聽請求並將請求封裝成ServletRequest和ServletResponse對象,然後交給Container進行處理,Container處理完之後在交給Connector返回給客戶端。一個Connector會監聽一個獨立的端口來處理來自客戶端的請求。server.xml中默認配置了一個Connector,端口號爲8080。
其實Connector結構非常複雜,不過我們暫時不需要了解這麼多,我們只需要知道他的功能即可,詳細的東西我會在Tomcat源碼分析中講。
3. Container
我們可以看到,Container實際上是一個Engine,一個Engine裏面可以有多個Host,一個Host裏面有多個Context,每個Context裏面又有多個Wrapper,而每個Wrapper裏面都有一個Servlet。這就是Container大體的結構。下面我們來看看細節:
首先我們來看Engine,Engine表示整個Servlet引擎,一個Engine下面可以包含一個或者多個Host,即一個Tomcat實例可以配置多個虛擬主機,默認的情況下server.xml配置文件中<Engine name=“Catalina” defaultHost=“localhost”> 定義了一個名爲Catalina的Engine。
然後是Host,代表一個站點,也可以叫虛擬主機,一個Host可以配置多個Context,在server.xml文件中的默認配置爲<Host name=“localhost” appBase=“webapps” unpackWARs=“true” autoDeploy=“true”>。一個Engine包含多個Host的設計,使得一個服務器實例可以承擔多個域名的服務,是很靈活的設計。
緊接着是最重要的Context。Context代表一個應用程序,也就是我們開發的應用,或者一個WEB-INF目錄以及下面的web.xml文件,換句話說每一個運行的webapp最終都是以Context的形式存在,每個Context都有一個根路徑和請求路徑。Context與Host的區別是Context代表一個應用,默認配置下webapps下的每個目錄都是一個應用,其中ROOT目錄中存放主應用,其他目錄存放別的子應用,而整個webapps是一個站點,也就是Host。
最後是Wrapper,在Tomcat中Servlet被稱爲Wrapper,至於爲什麼要用Wrapper,這與Tomcat的處理機制有關,現在暫時不用瞭解。
4. Service
很顯然,Tomcat可以有多個Connector,同時Container也可以有多個。那這就存在一個問題,哪個Connector對應哪個Container?相信看過server.xml文件的人已經知道了Tomcat是怎麼處理的了。
沒錯,Service就是這樣來的。在server.xml文件中,可以看到Service組件包含了Connector組件和Engine組件(Engine就是一種Container),即Service相當於Connector和Engine組件的包裝器,將一個或者多個Connector和一個Engine建立關聯關係。
在默認的配置文件中,定義了一個叫Catalina的service,它將HTTP/1.1和AJP/1.3這兩個Connector與一個名爲Catalina的Engine關聯起來。一個Server可以包含多個Service,它們相互獨立,一個Service負責維護多個Connector和一個Container。
下面兩張圖幫我們將Tomcat結構總結一下:
我們要記住的就是各個模塊的作用,以及所有模塊的層次關係,這樣在Tomcat出現問題的時候我們才能快速定位到錯誤的位置和原因。
(三)Tomcat啓動流程
你一定會很好奇Tomcat是如何啓動的,爲什麼在idea裏面鼠標一點就可以啓動我們的服務器?在這段時間中Tomcat又爲我們做了什麼?想了解這些,我們有必要來看看Tomcat的啓動流程。
因爲具體講啓動流程會涉及到源碼,但初學階段不建議查閱源碼,我們就用圖形大概的看看就行。以後我會出Tomcat源碼分析來具體講這個問題。
由於Tomcat組件之間的包含關係,一個組件中可以包含多個子組件,且這些組件都實現了Lifecycle接口。因此只要啓動最上層的server組件,它包含的所有組件也會跟着啓動了,例如啓動server,所有子service都會跟着啓動,service啓動了,它的Container和Connector等也會跟着啓動。因此,tomat要啓動,只需要啓動server就可以了。那我們的Server如何啓動呢?這就需要Catalina類。
Catalina是整個Tomcat的管理類,他有三個方法load、start、stop分別用來管理整個服務器的生命週期。load方法用於加載Tomcat/conf目錄下的server.xml配置文件,用來創建Server並調用Server的init方法進行初始化操作,start用於啓動服務器,stop用於停止服務器,start和stop方法在內部分別調用Server的start和stop方法,load方法在內部調用了Server的init方法,這三個方法都會按層次分逐層調用相應的方法。Catalina的啓動又依賴Bootstrap。
Bootstrap是main方法所在地,是Tomcat真正的入口類,我們可以簡單看一下main方法裏面有什麼:
public static void main(String args[]) {
//首先判斷Bootstrap daemon 是否爲空,就是創建一個Bootstrap實例daemon
if (daemon == null) {
// Don't set daemon until init() has completed
Bootstrap bootstrap = new Bootstrap();
try {
bootstrap.init(); //初始化ClassLoader,並用ClassLoader創建了一個Catalina實例,並將這個實例賦值給了cataLinaDaemon
} catch (Throwable t) {
handleThrowable(t);
t.printStackTrace();
return;
}
daemon = bootstrap;
} else {
Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
}
try {
String command = "start"; //指定默認的執行指令是start
if (args.length > 0) {
command = args[args.length - 1];
}
if (command.equals("startd")) {
args[args.length - 1] = "start";
daemon.load(args); //BootStrap的load方法,其實是調用Calatina的load方法
daemon.start();
} else if (command.equals("stopd")) {
args[args.length - 1] = "stop";
daemon.stop();
} else if (command.equals("start")) {
daemon.setAwait(true); //setWait方法 主要是爲了設置Server啓動完成後是否進入等待狀態的標誌,如果爲true則進入,否則不進入
daemon.load(args); //load方法用於加載配置文件,創建並初始化Server
daemon.start(); //start方法用於啓動服務器
} else if (command.equals("stop")) {
daemon.stopServer(args);
} else if (command.equals("configtest")) {
daemon.load(args);
if (null == daemon.getServer()) {
System.exit(1);
}
System.exit(0);
} else {
log.warn("Bootstrap: command \"" + command + "\" does not exist.");
}
} catch (Throwable t) {
// Unwrap the Exception for clearer error reporting
if (t instanceof InvocationTargetException &&
t.getCause() != null) {
t = t.getCause();
}
handleThrowable(t);
t.printStackTrace();
System.exit(1);
}
}
這段代碼基本人人都能看得懂,就是創建了一個Bootstrap對象並藉助init方法初始化了Catalina對象,接着將這個Bootstrap對象賦值給daemon。然後通過判斷main函數的參數來決定調用daemon的哪個方法。很顯然,如果main函數的參數爲start,那麼就會調用daemon的start方法,不用看源碼都知道,這個方法肯定幫助我們調用了Catalina的start方法,然後啓動整個服務器。具體的代碼我會在Tomcat源碼分析裏面詳細說明。
2020年5月27日