Tomcat 總體架構設計
在開始這篇文章的時候,忽然發現上一篇內容的題目不是很合適,不應該叫啟動流程,更確切的應該是叫啟動指令碼。
在最開始,先介紹下 Tomcat 的總體設計,先有一個大概的印象,對 Tomcat 不至於那麼陌生。
先介紹下 Tomcat 的一些基礎元件(以下內容來自劉光瑞老師的「tomcat 架構解析」):
元件名稱 | 介紹 |
---|---|
Server | 這個其實就是 Servlet 容器,一個 Tomcat 中只能有一個 Server |
Service | Service 表示一個或多個 Connector 的集合,這些 Connector 共享同一個 Container 來處理其請求。在同一個 Tomcat 例項內可以包含任意多個 Service 例項,它們彼此獨立 |
Connector | 這個是 Tomcat 的聯結器,用於監聽並轉化 Servlet 的請求,同時將讀取的 Socket 請求轉交由 Container 處理,並且支援不同的協議以及不同的 I/O 方式。 |
Container | 表示能夠執行客戶端請求並返回響應的一類物件。在 Tomcat 中存在不同級別的容器 Engine, Host, Context, Wrapper |
Engine | 表示整個 Servlet 引擎。在 Tomcat 中, Engine 為最高層級的容器物件。儘管 Engine 不是直接處理請求的容器,卻是獲取目標容器的入口。 |
Host | Host 作為一類容器,表示 Servlet 引擎(即 Engine 中的虛擬機器,與一個伺服器的網路名有關,如域名等。客戶端可以使用這個網路名連線伺服器,這個名稱必須要在 DNS 伺服器上註冊 |
Context | Context 作為一類容器,用於表示 Servletcontext ,在 Servlet 規範中,一個 Servletcontext 即表示一個獨立的 Web 應用。 |
Wapper | Wapper 作為一類容器,用於表示 Web 應用中定義的 Servlet。 |
Executor | 表示 Tomcat 元件間可以共享的執行緒池。 |
這個表格大致看一下,瞭解下 Tomcat 的一些元件,無需記憶,先有個印象,後面的內容會慢慢的聊到每一個元件。
作為一款 Web 容器, Tomcat 大體上實現了兩個核心功能:
- 處理
Socket
連線,負責網路位元組流與Request
和Response
物件的轉化。 - 載入並管理
Servlet
,以及處理具體的Request
請求。
所以 Tomcat 設計了兩個核心元件聯結器(Connector)和容器(Container)。聯結器負責對外交流,容器負責內部處理。
Tomcat 為了實現多種支援多種 I/O 模型和應用層協議,將 聯結器(Connector)
和 容器(Container)
進行了分離,
在 Tomcat 中,總會有一個 Service ,一個 Service 包含著多個 Connector 和一個 Container(或者說是它的頂層容器 Engine ) 。
而一個 Container 可以包含多個 Host ,一個 Host 內部又可以有多個 Context ,一個 Context 內部又會有多個 Servlet 。
Tomcat 的設計感覺上和 「俄羅斯套娃」 很像,一層包著一層。
Tomcat 啟動初始化流程
先放一張啟動流程圖,然後我們再從原始碼的角度來驗證這個啟動流程。
從上面這張圖可以清晰的瞭解到 Tomcat 的初始化的核心過程如下:
BootStrap -> Catalina -> Server -> Service -> Excutor -> Container (Engine -> Host -> Context -> Container)
1 BootStrap.main()
首先,整個程式是從 BootStrap 開始啟動的,執行的是 BootStrap.main()
:
public static void main(String args[]) {
synchronized (daemonLock) {
if (daemon == null) {
// Don't set daemon until init() has completed
Bootstrap bootstrap = new Bootstrap();
try {
bootstrap.init();
} catch (Throwable t) {
handleThrowable(t);
t.printStackTrace();
return;
}
daemon = bootstrap;
} else {
// When running as a service the call to stop will be on a new
// thread so make sure the correct class loader is used to
// prevent a range of class not found exceptions.
Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
}
}
try {
String command = "start";
if (args.length > 0) {
command = args[args.length - 1];
}
if (command.equals("startd")) {
args[args.length - 1] = "start";
daemon.load(args);
daemon.start();
} else if (command.equals("stopd")) {
args[args.length - 1] = "stop";
daemon.stop();
} else if (command.equals("start")) {
daemon.setAwait(true);
daemon.load(args);
daemon.start();
if (null == daemon.getServer()) {
System.exit(1);
}
} 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);
}
}
在最開始,執行的是一段 synchronized
的同步程式碼,這裡執行程式碼 bootstrap.init()
初始化了 bootstrap 物件,具體做了什麼跟進去看下:
public void init() throws Exception {
// 初始化類載入器
initClassLoaders();
// 設定執行緒類載入器,將容器的載入器傳入
Thread.currentThread().setContextClassLoader(catalinaLoader);
// 載入安全類
SecurityClassLoad.securityClassLoad(catalinaLoader);
// 初始化日誌
// Load our startup class and call its process() method
if (log.isDebugEnabled())
log.debug("Loading startup class");
// 注意這裡,通過反射載入了 Catalina
Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.getConstructor().newInstance();
// Set the shared extensions class loader
if (log.isDebugEnabled())
log.debug("Setting startup class properties");
// 呼叫了上面載入的 Catalina 中的 setParentClassLoader 方法
String methodName = "setParentClassLoader";
Class<?> paramTypes[] = new Class[1];
// 將類載入器賦值成一個引數
paramTypes[0] = Class.forName("java.lang.ClassLoader");
Object paramValues[] = new Object[1];
// 看名字意思是一個共享載入器
paramValues[0] = sharedLoader;
Method method =
startupInstance.getClass().getMethod(methodName, paramTypes);
// 呼叫了 Catalina 內的 setParentClassLoader 方法對 Catalina 類內的類載入器賦值
method.invoke(startupInstance, paramValues);
catalinaDaemon = startupInstance;
}
註釋都加好了,就不多解釋了,我們接著回到之前的 main
,接下來還有一句是 daemon = bootstrap
,就是把我們剛才初始化過的 bootstrap 賦值給了 daemon 這個變數。
接下來是一大段的判斷,主要是用來判斷輸入引數,這裡面我們關注的就中間那一小段,當輸入引數為 start
的時候,總共做了兩件大事:
- daemon.load(args)
- daemon.start()
先跟進去看下 daemon.load(args)
都幹了點啥:
private void load(String[] arguments) throws Exception {
// Call the load() method
String methodName = "load";
Object param[];
Class<?> paramTypes[];
if (arguments==null || arguments.length==0) {
paramTypes = null;
param = null;
} else {
paramTypes = new Class[1];
paramTypes[0] = arguments.getClass();
param = new Object[1];
param[0] = arguments;
}
// 這裡的 catalinaDaemon 剛才在 init() 方法裡面進行了初始化,所以這裡呼叫的實際上是 Catalina 的 load 方法
Method method =
catalinaDaemon.getClass().getMethod(methodName, paramTypes);
if (log.isDebugEnabled()) {
log.debug("Calling startup class " + method);
}
method.invoke(catalinaDaemon, param);
}
這裡實際上是開啟了整個鏈式呼叫,接著往下追,看下 Catalina 裡面的 load 方法。
在這裡,會有一個方法的過載 load(String args[])
和 load()
,不過關係不大,最終都是會呼叫到那個無參的方法上的。
public void load() {
if (loaded) {
return;
}
loaded = true;
long t1 = System.nanoTime();
initDirs();
// Before digester - it may be needed
initNaming();
// Create and execute our Digester
Digester digester = createStartDigester();
InputSource inputSource = null;
InputStream inputStream = null;
File file = null;
try {
try {
file = configFile();
inputStream = new FileInputStream(file);
inputSource = new InputSource(file.toURI().toURL().toString());
} catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("catalina.configFail", file), e);
}
}
if (inputStream == null) {
try {
inputStream = getClass().getClassLoader()
.getResourceAsStream(getConfigFile());
inputSource = new InputSource
(getClass().getClassLoader()
.getResource(getConfigFile()).toString());
} catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("catalina.configFail",
getConfigFile()), e);
}
}
}
// This should be included in catalina.jar
// Alternative: don't bother with xml, just create it manually.
if (inputStream == null) {
try {
inputStream = getClass().getClassLoader()
.getResourceAsStream("server-embed.xml");
inputSource = new InputSource
(getClass().getClassLoader()
.getResource("server-embed.xml").toString());
} catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("catalina.configFail",
"server-embed.xml"), e);
}
}
}
if (inputStream == null || inputSource == null) {
if (file == null) {
log.warn(sm.getString("catalina.configFail",
getConfigFile() + "] or [server-embed.xml]"));
} else {
log.warn(sm.getString("catalina.configFail",
file.getAbsolutePath()));
if (file.exists() && !file.canRead()) {
log.warn("Permissions incorrect, read permission is not allowed on the file.");
}
}
return;
}
try {
inputSource.setByteStream(inputStream);
digester.push(this);
digester.parse(inputSource);
} catch (SAXParseException spe) {
log.warn("Catalina.start using " + getConfigFile() + ": " +
spe.getMessage());
return;
} catch (Exception e) {
log.warn("Catalina.start using " + getConfigFile() + ": " , e);
return;
}
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
// Ignore
}
}
}
getServer().setCatalina(this);
getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());
// 前面都是在初始化載入各種配置檔案,使用了 Digester
// Stream redirection
initStreams();
// Start the new server
try {
// 開始呼叫的 Server 的初始化方法
// Server 是一個介面,並且繼承了 Lifecycle ,進行生命週期的管理
getServer().init();
} catch (LifecycleException e) {
if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {
throw new java.lang.Error(e);
} else {
log.error("Catalina.start", e);
}
}
long t2 = System.nanoTime();
if(log.isInfoEnabled()) {
log.info("Initialization processed in " + ((t2 - t1) / 1000000) + " ms");
}
}
這一大坨大多數都是在載入各種配置檔案,在這裡,會完成對 server.xml
檔案的解析。
我們關心的其實就只有最後那段 try...catch
裡面的那一句 getServer().init()
,開始呼叫 Server 的初始化方法,這個方法就是開啟一系列容器元件的載入方法。
不過看到最後一句的 log 列印,忽然有印象了,之前在啟動 Tomcat 的時候,在日誌的最後部分,都會看到這句列印,而且可以看到的是,這裡並不是我之前以為的是使用毫秒數算出來的,而是取的納秒數通過除 1000000
計算得出的,難道是這樣算的更準?
getServer().init()
實際上是呼叫了 org.apache.catalina.Server
中的 init()
方法,而 org.apache.catalina.Server
則是一個介面,還繼承了 org.apache.catalina.Lifecycle
進行容器生命週期的管理。
而抽象類 org.apache.catalina.util.LifecycleBase
則是實現了 org.apache.catalina.Lifecycle
介面,我們在 org.apache.catalina.Lifecycle
中開啟 init()
方法:
public final synchronized void init() throws LifecycleException {
if (!state.equals(LifecycleState.NEW)) {
invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
}
try {
setStateInternal(LifecycleState.INITIALIZING, null, false);
initInternal();
setStateInternal(LifecycleState.INITIALIZED, null, false);
} catch (Throwable t) {
handleSubClassException(t, "lifecycleBase.initFail", toString());
}
}
這裡我們的關注點是 initInternal()
這個方法,從名字上來看就是一個初始化方法,點過去一看,結果是一個抽象方法,實現類如下:
我們到 org.apache.catalina.core.StandardServer
中去看下其中的 initInternal()
方法,在這個方法的最後,看到了迴圈 Service
並且呼叫了 service.init()
:
for (Service service : services) {
service.init();
}
因為一個 Server 是可以有多個 Service 的,所以這裡用了一個迴圈,接下來就是一個順序的容器初始化的呼叫過程:
StandardServer -> StandardService -> StandardEngine -> Connector
每個容器都在初始化自身相關設定的同時,將子容器初始化。
Tomcat 在啟動初始化的時候,是通過鏈條式的呼叫,每初始化一個完成一個元件,就會在元件內呼叫下一個元件的初始化方法。
同樣的操作在啟動的時候也是一樣的,不知道各位是否還記得我們在 Bootstrap.main()
中還有另一句程式碼 daemon.start()
,
public void start() throws Exception {
if (catalinaDaemon == null) {
init();
}
Method method = catalinaDaemon.getClass().getMethod("start", (Class [])null);
method.invoke(catalinaDaemon, (Object [])null);
}
操作嘛都是一樣的操作,從這段程式碼開始,呼叫了 Catalina.start()
的方法,然後開啟了另一個鏈式呼叫。
我就不多說了,留給各位讀者自己去翻翻看原始碼吧。