1. 程式人生 > >Tomcat詳解系列(3) - 原始碼分析準備和分析入口

Tomcat詳解系列(3) - 原始碼分析準備和分析入口

# Tomcat - 原始碼分析準備和分析入口 > 上文我們介紹了Tomcat的架構設計,接下來我們便可以下載原始碼以及尋找原始碼入口了。@pdai ## 原始碼下載和編譯 首先是去官網下載Tomcat的原始碼和二進位制安裝包,我這裡分析最新的[Tomcat9.0.39穩定版本](https://tomcat.apache.org/download-90.cgi)https://tomcat.apache.org/download-90.cgi ### 下載二進位制包和原始碼 > 下載二進位制包的主要目的在於,讓我們回顧一下包中的內容;其次,在我們後面通過原始碼包編譯後,以方便和二進位制包進行對比。 + 下載兩個包 ![](https://pdai.tech/_images/tomcat/tomcat-x-sourcecode-2.png) + 檢視二進位制包中主要模組 ![](https://pdai.tech/_images/tomcat/tomcat-x-sourcecode-3.png) ### 編譯原始碼 + 匯入IDEA ![](https://pdai.tech/_images/tomcat/tomcat-x-sourcecode-4.png) + 使用ant編譯 ![](https://pdai.tech/_images/tomcat/tomcat-x-sourcecode-1.png) ### 理解編譯後模組 > 這裡有兩點要注意下:第一:在編譯完之後,編譯輸出到哪裡了呢?第二:編譯後的結果是不是和我們下載的二進位制檔案對的上呢? + 編譯的輸出在哪裡 ![](https://pdai.tech/_images/tomcat/tomcat-x-sourcecode-5.png) + 編譯的輸出結果是否對的上,很顯然和上面的二進位制包一致 ![](https://pdai.tech/_images/tomcat/tomcat-x-sourcecode-6.png) ## 從啟動指令碼定位Tomcat原始碼入口 > 好了,到這裡我們基本上已經有準備好程式碼了,接下來便是尋找程式碼入口了。@pdai ### startup.bat > 當我們初學tomcat的時候, 肯定先要學習怎樣啟動tomcat. 在tomcat的bin目錄下有兩個啟動tomcat的檔案, 一個是startup.bat, 它用於windows環境下啟動tomcat; 另一個是startup.sh, 它用於linux環境下tomcat的啟動. 兩個檔案中的邏輯是一樣的, 我們只分析其中的startup.bat. + startup.bat的原始碼: **startup.bat檔案實際上就做了一件事情: 啟動catalina.bat.** ```bash @echo off rem Licensed to the Apache Software Foundation (ASF) under one or more rem contributor license agreements. See the NOTICE file distributed with rem this work for additional information regarding copyright ownership. rem The ASF licenses this file to You under the Apache License, Version 2.0 rem (the "License"); you may not use this file except in compliance with rem the License. You may obtain a copy of the License at rem rem http://www.apache.org/licenses/LICENSE-2.0 rem rem Unless required by applicable law or agreed to in writing, software rem distributed under the License is distributed on an "AS IS" BASIS, rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. rem See the License for the specific language governing permissions and rem limitations under the License. rem --------------------------------------------------------------------------- rem Start script for the CATALINA Server rem --------------------------------------------------------------------------- setlocal rem Guess CATALINA_HOME if not defined set "CURRENT_DIR=%cd%" if not "%CATALINA_HOME%" == "" goto gotHome set "CATALINA_HOME=%CURRENT_DIR%" if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome cd .. set "CATALINA_HOME=%cd%" cd "%CURRENT_DIR%" :gotHome if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome echo The CATALINA_HOME environment variable is not defined correctly echo This environment variable is needed to run this program goto end :okHome set "EXECUTABLE=%CATALINA_HOME%\bin\catalina.bat" rem Check that target executable exists if exist "%EXECUTABLE%" goto okExec echo Cannot find "%EXECUTABLE%" echo This file is needed to run this program goto end :okExec rem Get remaining unshifted command line arguments and save them in the set CMD_LINE_ARGS= :setArgs if ""%1""=="""" goto doneSetArgs set CMD_LINE_ARGS=%CMD_LINE_ARGS% %1 shift goto setArgs :doneSetArgs call "%EXECUTABLE%" start %CMD_LINE_ARGS% :end ``` + 當然如果你感興趣,不妨也可以看下上面指令碼的含義 + .bat檔案中@echo是列印指令, 用於控制檯輸出資訊, rem是註釋符. + 跳過開頭的註釋, 我們來到配置CATALINA_HOME的程式碼段, 執行startup.bat檔案首先會設定CATALINA_HOME. ```bash set "CURRENT_DIR=%cd%" if not "%CATALINA_HOME%" == "" goto gotHome set "CATALINA_HOME=%CURRENT_DIR%" if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome cd .. set "CATALINA_HOME=%cd%" cd "%CURRENT_DIR%" :gotHome if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome echo The CATALINA_HOME environment variable is not defined correctly echo This environment variable is needed to run this program goto end :okHome ``` + 先通過set指令把當前目錄設定到一個名為CURRENT_DIR的變數中, + 如果系統中配置過CATALINA_HOME則跳到gotHome程式碼段. 正常情況下我們的電腦都沒有配置CATALINA_HOME, 所以往下執行, 把當前目錄設定為CATALINA_HOME. + 然後判斷CATALINA_HOME目錄下是否存在catalina.bat檔案, 如果存在就跳到okHome程式碼塊. + 在okHome中, 會把catalina.bat檔案的的路徑賦給一個叫EXECUTABLE的變數, 然後會進一步判斷這個路徑是否存在, 存在則跳轉到okExec程式碼塊, 不存在的話會在控制檯輸出一些錯誤資訊. + 在okExec中, 會把setArgs程式碼塊的返回結果賦值給CMD_LINE_ARGS變數, 這個變數用於儲存啟動引數. + setArgs中首先會判斷是否有引數, (if ""%1""==""""判斷第一個引數是否為空), 如果沒有引數則相當於引數項為空. 如果有引數則迴圈遍歷所有的引數(每次拼接第一個引數). + 最後執行call "%EXECUTABLE%" start %CMD_LINE_ARGS%, 也就是說執行catalina.bat檔案, 如果有引數則帶上引數. > 這樣看來, 在windows下啟動tomcat未必一定要通過startup.bat, 用catalina.bat start也是可以的. ### catalina.bat > catalina的指令碼有點多,我們分開看: + 跳過開頭的註釋, 我們來到下面的程式碼段: ```bash setlocal rem Suppress Terminate batch job on CTRL+C if not ""%1"" == ""run"" goto mainEntry if "%TEMP%" == "" goto mainEntry if exist "%TEMP%\%~nx0.run" goto mainEntry echo Y>"%TEMP%\%~nx0.run" if not exist "%TEMP%\%~nx0.run" goto mainEntry echo Y>"%TEMP%\%~nx0.Y" call "%~f0" %* <"%TEMP%\%~nx0.Y" rem Use provided errorlevel set RETVAL=%ERRORLEVEL% del /Q "%TEMP%\%~nx0.Y" >
NUL 2>&1 exit /B %RETVAL% :mainEntry del /Q "%TEMP%\%~nx0.run" >NUL 2>&1 ``` + 大多情況下我們啟動tomcat都沒有設定引數, 所以直接跳到mainEntry程式碼段, 刪除了一個臨時檔案後, 繼續往下執行. ```bash rem Guess CATALINA_HOME if not defined set "CURRENT_DIR=%cd%" if not "%CATALINA_HOME%" == "" goto gotHome set "CATALINA_HOME=%CURRENT_DIR%" if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome cd .. set "CATALINA_HOME=%cd%" cd "%CURRENT_DIR%" :gotHome if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome echo The CATALINA_HOME environment variable is not defined correctly echo This environment variable is needed to run this program goto end :okHome rem Copy CATALINA_BASE from CATALINA_HOME if not defined if not "%CATALINA_BASE%" == "" goto gotBase set "CATALINA_BASE=%CATALINA_HOME%" ``` + 可以看到這段程式碼與startup.bat中開頭的程式碼相似, 在確定CATALINA_HOME下有catalina.bat後把CATALINA_HOME賦給變數CATALINA_BASE. ```bash rem Get standard environment variables if not exist "%CATALINA_BASE%\bin\setenv.bat" goto checkSetenvHome call "%CATALINA_BASE%\bin\setenv.bat" goto setenvDone :checkSetenvHome if exist "%CATALINA_HOME%\bin\setenv.bat" call "%CATALINA_HOME%\bin\setenv.bat" :setenvDone rem Get standard Java environment variables if exist "%CATALINA_HOME%\bin\setclasspath.bat" goto okSetclasspath echo Cannot find "%CATALINA_HOME%\bin\setclasspath.bat" echo This file is needed to run this program goto end :okSetclasspath call "%CATALINA_HOME%\bin\setclasspath.bat" %1 if errorlevel 1 goto end rem Add on extra jar file to CLASSPATH rem Note that there are no quotes as we do not want to introduce random rem quotes into the CLASSPATH if "%CLASSPATH%" == "" goto emptyClasspath set "CLASSPATH=%CLASSPATH%;" :emptyClasspath set "CLASSPATH=%CLASSPATH%%CATALINA_HOME%\bin\bootstrap.jar" ``` >
上面這段程式碼依次執行了setenv.bat和setclasspath.bat檔案, 目的是獲得CLASSPATH, 相信會Java的同學應該都會在配置環境變數時都配置過classpath, 系統拿到classpath路徑後把它和CATALINA_HOME拼接在一起, 最終定位到一個叫bootstrap.jar的檔案. 雖然後面還有很多程式碼, 但是這裡必須暫停提示一下: bootstrap.jar將是我們啟動tomcat的環境. + 接下來從gotTmpdir程式碼塊到noEndorsedVar程式碼塊進行了一些配置, 由於不是主要內容暫且跳過. ```bash echo Using CATALINA_BASE: "%CATALINA_BASE%" echo Using CATALINA_HOME: "%CATALINA_HOME%" echo Using CATALINA_TMPDIR: "%CATALINA_TMPDIR%" if ""%1"" == ""debug"" goto use_jdk echo Using JRE_HOME: "%JRE_HOME%" goto java_dir_displayed :use_jdk echo Using JAVA_HOME: "%JAVA_HOME%" :java_dir_displayed echo Using CLASSPATH: "%CLASSPATH%" set _EXECJAVA=%_RUNJAVA% set MAINCLASS=org.apache.catalina.startup.Bootstrap set ACTION=start set SECURITY_POLICY_FILE= set DEBUG_OPTS= set JPDA= if not ""%1"" == ""jpda"" goto noJpda set JPDA=jpda if not "%JPDA_TRANSPORT%" == "" goto gotJpdaTransport set JPDA_TRANSPORT=dt_socket :gotJpdaTransport if not "%JPDA_ADDRESS%" == "" goto gotJpdaAddress set JPDA_ADDRESS=8000 :gotJpdaAddress if not "%JPDA_SUSPEND%" == "" goto gotJpdaSuspend set JPDA_SUSPEND=n :gotJpdaSuspend if not "%JPDA_OPTS%" == "" goto gotJpdaOpts set JPDA_OPTS=-agentlib:jdwp=transport=%JPDA_TRANSPORT%,address=%JPDA_ADDRESS%,server=y,suspend=%JPDA_SUSPEND% :gotJpdaOpts shift :noJpda if ""%1"" == ""debug"" goto doDebug if ""%1"" == ""run"" goto doRun if ""%1"" == ""start"" goto doStart if ""%1"" == ""stop"" goto doStop if ""%1"" == ""configtest"" goto doConfigTest if ""%1"" == ""version"" goto doVersion ``` + 接下來, 我們能看到一些重要的資訊, 其中的重點是: ```bash set _EXECJAVA=%_RUNJAVA%, 設定了jdk中bin目錄下的java.exe檔案路徑. set MAINCLASS=org.apache.catalina.startup.Bootstrap, 設定了tomcat的啟動類為Bootstrap這個類. (後面會分析這個類) set ACTION=start設定tomcat啟動 ``` >
大家可以留意這些引數, 最後執行tomcat的啟動時會用到. ```bash if not ""%1"" == ""jpda"" goto noJpda set JPDA=jpda if not "%JPDA_TRANSPORT%" == "" goto gotJpdaTransport set JPDA_TRANSPORT=dt_socket :gotJpdaTransport if not "%JPDA_ADDRESS%" == "" goto gotJpdaAddress set JPDA_ADDRESS=8000 :gotJpdaAddress if not "%JPDA_SUSPEND%" == "" goto gotJpdaSuspend set JPDA_SUSPEND=n :gotJpdaSuspend if not "%JPDA_OPTS%" == "" goto gotJpdaOpts set JPDA_OPTS=-agentlib:jdwp=transport=%JPDA_TRANSPORT%,address=%JPDA_ADDRESS%,server=y,suspend=%JPDA_SUSPEND% :gotJpdaOpts shift :noJpda if ""%1"" == ""debug"" goto doDebug if ""%1"" == ""run"" goto doRun if ""%1"" == ""start"" goto doStart if ""%1"" == ""stop"" goto doStop if ""%1"" == ""configtest"" goto doConfigTest if ""%1"" == ""version"" goto doVersion ``` + 接著判斷第一個引數是否是jpda, 是則進行一些設定. 而正常情況下第一個引數是start, 所以跳過這段程式碼. 接著會判斷第一個引數的內容, 根據判斷, 我們會跳到doStart程式碼段. (有餘力的同學不妨看下debug, run等啟動方式) ```bash :doStart shift if "%TITLE%" == "" set TITLE=Tomcat set _EXECJAVA=start "%TITLE%" %_RUNJAVA% if not ""%1"" == ""-security"" goto execCmd shift echo Using Security Manager set "SECURITY_POLICY_FILE=%CATALINA_BASE%\conf\catalina.policy" goto execCmd ``` + 可以看到doStart中無非也是設定一些引數, 最終會跳轉到execCmd程式碼段 ```bash :execCmd rem Get remaining unshifted command line arguments and save them in the set CMD_LINE_ARGS= :setArgs if ""%1""=="""" goto doneSetArgs set CMD_LINE_ARGS=%CMD_LINE_ARGS% %1 shift goto setArgs :doneSetArgs ``` > 可以看到這段程式碼也是在拼接引數, 把引數拼接到一個叫CMD_LINE_ARGS的變數中, 接下來就是catalina最後的一段程式碼了. ```bash rem Execute Java with the applicable properties if not "%JPDA%" == "" goto doJpda if not "%SECURITY_POLICY_FILE%" == "" goto doSecurity %_EXECJAVA% %LOGGING_CONFIG% %LOGGING_MANAGER% %JAVA_OPTS% %CATALINA_OPTS% %DEBUG_OPTS% -D%ENDORSED_PROP%="%JAVA_ENDORSED_DIRS%" -classpath "%CLASSPATH%" -Dcatalina.base="%CATALINA_BASE%" -Dcatalina.home="%CATALINA_HOME%" -Djava.io.tmpdir="%CATALINA_TMPDIR%" %MAINCLASS% %CMD_LINE_ARGS% %ACTION% goto end :doSecurity %_EXECJAVA% %LOGGING_CONFIG% %LOGGING_MANAGER% %JAVA_OPTS% %CATALINA_OPTS% %DEBUG_OPTS% -D%ENDORSED_PROP%="%JAVA_ENDORSED_DIRS%" -classpath "%CLASSPATH%" -Djava.security.manager -Djava.security.policy=="%SECURITY_POLICY_FILE%" -Dcatalina.base="%CATALINA_BASE%" -Dcatalina.home="%CATALINA_HOME%" -Djava.io.tmpdir="%CATALINA_TMPDIR%" %MAINCLASS% %CMD_LINE_ARGS% %ACTION% goto end :doJpda if not "%SECURITY_POLICY_FILE%" == "" goto doSecurityJpda %_EXECJAVA% %LOGGING_CONFIG% %LOGGING_MANAGER% %JAVA_OPTS% %JPDA_OPTS% %CATALINA_OPTS% %DEBUG_OPTS% -D%ENDORSED_PROP%="%JAVA_ENDORSED_DIRS%" -classpath "%CLASSPATH%" -Dcatalina.base="%CATALINA_BASE%" -Dcatalina.home="%CATALINA_HOME%" -Djava.io.tmpdir="%CATALINA_TMPDIR%" %MAINCLASS% %CMD_LINE_ARGS% %ACTION% goto end :doSecurityJpda %_EXECJAVA% %LOGGING_CONFIG% %LOGGING_MANAGER% %JAVA_OPTS% %JPDA_OPTS% %CATALINA_OPTS% %DEBUG_OPTS% -D%ENDORSED_PROP%="%JAVA_ENDORSED_DIRS%" -classpath "%CLASSPATH%" -Djava.security.manager -Djava.security.policy=="%SECURITY_POLICY_FILE%" -Dcatalina.base="%CATALINA_BASE%" -Dcatalina.home="%CATALINA_HOME%" -Djava.io.tmpdir="%CATALINA_TMPDIR%" %MAINCLASS% %CMD_LINE_ARGS% %ACTION% goto end :end ``` + 跳過前面兩行判斷後, 來到了關鍵語句: ```bash %_EXECJAVA% %LOGGING_CONFIG% %LOGGING_MANAGER% %JAVA_OPTS% %CATALINA_OPTS% %DEBUG_OPTS% -D%ENDORSED_PROP%="%JAVA_ENDORSED_DIRS%" -classpath "%CLASSPATH%" -Dcatalina.base="%CATALINA_BASE%" -Dcatalina.home="%CATALINA_HOME%" -Djava.io.tmpdir="%CATALINA_TMPDIR%" %MAINCLASS% %CMD_LINE_ARGS% %ACTION% ``` > _EXECJAVA也就是_RUNJAVA, 也就是平時說的java指令, 但在之前的doStart程式碼塊中把_EXECJAVA改為了start "%TITLE%" %_RUNJAVA%, 所以系統會另啟一個命令列視窗, 名字叫Tomcat. 在拼接一系列引數後, 我們會看見%MAINCLASS%, 也就是org.apache.catalina.startup.Bootstrap啟動類, 拼接完啟動引數後, 最後拼接的是%ACTION%, 也就是start. **總結**: + **catalina.bat最終執行了Bootstrap類中的main方法**. + 我們可以通過設定不同的引數讓tomcat以不同的方式執行. 在ide中我們是可以選擇debug等模式啟動tomcat的, 也可以為其配置引數, 在catalina.bat中我們看到了啟動tomcat背後的運作流程. ## 參考文章 + https://www.cnblogs.com/tanshaoshenghao/p/10932306.html *相關文章* + [Tomcat - 如何設計一個簡單的web容器](https://pdai.tech/md/framework/tomcat-x-design-web-container.html) + 在學習Tomcat前,很多人先入為主的對它的認知是巨複雜的;所以第一步,在學習它之前,要打破這種觀念,我們通過學習如何設計一個最基本的web容器來看它需要考慮什麼;進而在真正學習Tomcat時,多把重點放在它的頂層設計上,而不是某一塊程式碼上, 思路永遠比具體實現重要的多。 + [Tomcat - 理解Tomcat架構設計](https://pdai.tech/md/framework/tomcat/tomcat-x-arch.html) + 前文我們已經介紹了一個簡單的Servlet容器是如何設計出來,我們就可以開始正式學習Tomcat了,在學習開始,我們有必要站在高點去看看Tomcat的架構設計。 + [Tomcat - 原始碼分析準備和分析入口](https://pdai.tech/md/framework/tomcat/tomcat-x-sourcecode.html) + 上文我們介紹了Tomcat的架構設計,接下來我們便可以下載原始碼以及尋找原始碼入口了。 + [Tomcat - 啟動過程:初始化和啟動流程](https://pdai.tech/md/framework/tomcat/tomcat-x-start.html) + 在有了Tomcat架構設計和原始碼入口以後,我們便可以開始真正讀原始碼了。 + [Tomcat - 啟動過程:類載入機制詳解](https://pdai.tech/md/framework/tomcat/tomcat-x-classloader.html) + 上文我們講了Tomcat在初始化時會初始化classLoader。本文將具體分析Tomcat的類載入機制,特別是區別於傳統的`雙親委派模型`的載入機制。 + [Tomcat - 啟動過程:Catalina的載入](https://pdai.tech/md/framework/tomcat/tomcat-x-catalina.html) + 通過前兩篇文章,我們知道了[Tomcat的類載入機制](https://pdai.tech/md/framework/tomcat/tomcat-x-classloader.html)和[整體的元件載入流程](https://pdai.tech/md/framework/tomcat/tomcat-x-start.html);我們也知道通過Bootstrap初始化的catalinaClassLoader載入了Catalina,那麼進而引入了一個問題就是Catalina是如何載入的呢?載入了什麼呢?本文將帶你進一步分析。 + [Tomcat - 元件生命週期管理:LifeCycle](https://pdai.tech/md/framework/tomcat/tomcat-x-lifecycle.html) + 上文中,我們已經知道Catalina初始化了Server(它呼叫了 Server 類的 init 和 start 方法來啟動 Tomcat);你會發現Server是Tomcat的配置檔案server.xml的頂層元素,那這個階段其實我們已經進入到Tomcat內部元件的詳解;這時候有一個問題,這麼多元件是如何管理它的生命週期的呢? + [Tomcat - 元件拓展管理:JMX和MBean](https://pdai.tech/md/framework/tomcat/tomcat-x-jmx.html) + 我們在前文中講Lifecycle以及元件,怎麼會突然講JMX和MBean呢?本文通過承接上文Lifecycle講Tomcat基於JMX的實現。 + [Tomcat - 事件的監聽機制:觀察者模式](https://pdai.tech/md/framework/tomcat/tomcat-x-listener.html) + 本文承接上文中Lifecycle中實現,引出Tomcat的監聽機制。 + [Tomcat - Server的設計和實現: StandardServer](https://pdai.tech/md/framework/tomcat/tomcat-x-server.html) + 基於前面的幾篇文章,我們終於可以總體上梳理Server的具體實現了,這裡體現在StandardServer具體的功能實現上。 + [Tomcat - Service的設計和實現: StandardService](https://pdai.tech/md/framework/tomcat/tomcat-x-service.html) + 上文講了Server的具體實現了,本文主要講Service的設計和實現;我們從上文其實已經知道Server中包含多個service了。 + [Tomcat - 執行緒池的設計與實現:StandardThreadExecutor](https://pdai.tech/md/framework/tomcat/tomcat-x-executor.html) + 上文中我們研究了下Service的設計和實現,StandardService中包含Executor的呼叫;這個比較好理解,Tomcat需要併發處理使用者的請求,自然而言就想到執行緒池,那麼Tomcat中執行緒池(Executor)具體是如何實現的?本文帶你繼續深度解析。 + [Tomcat - Request請求處理: Container設計](https://pdai.tech/md/framework/tomcat/tomcat-x-container.html) + 在理解了Server,Service和Executor後,我們可以進入Request處理環節了。我們知道客戶端是可以發起多個請求的,Tomcat也是可以支援多個webapp的,有多個上下文,且一個webapp中可以有多個Servlet...等等,那麼Tomcat是如何設計元件來支撐請求處理的呢?本節文將介紹Tomcat的Container設計。 + [Tomcat - Container容器之Engine:StandardEngine](https://pdai.tech/md/framework/tomcat/tomcat-x-container-engine.html) + 上文已經知道Container的整體結構和設計,其中Engine其實就是Servlet Engine,負責處理request的頂層容器。 + [Tomcat - Container的管道機制:責任鏈模式](https://pdai.tech/md/framework/tomcat/tomcat-x-container-pipline.html) + 上文中介紹了Engine的設計,其中有Pipline相關內容沒有介紹,本文將向你闡述Tomcat的管道機制以及它要解決的問題。 + [Tomcat - Request請求處理過程:Connector](https://pdai.tech/md/framework/tomcat/tomcat-x-connector.html) + 本文主要介紹request請求的處理