注:本文源碼分析基於 tomcat 9.0.43,源碼的gitee倉庫倉庫地址:https://gitee.com/funcy/tomcat.
1. 示例demo
本文是tomcat
源碼分析的第二篇,在idea下搭建 tomcat9 源碼調試環境一文中,我們搭建好了tomcat
的源碼調試環境,接下來我們就在裏面添加示例代碼,然後進行源碼分析了。
我們將代碼都放在test
目錄下,包名爲org.apache.tomcat.demo
:
1.1 準備servlet
我們先準備一個servet
,內容如下:
public class MyHttpServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.getWriter().println("<h1>hello world!!!</h1>");
System.out.println("hello world");
}
}
這個servlet
比較簡單,就只是向頁面與控制檯打印了一句:hello world
。
1.2 實現ServletContainerInitializer
這個是servlet 3.0
規範,本文的demo
中主要用來代替 web.xml
進行servlet
註冊的,都到2021年了,就不整web.xml
的方式了,代碼如下:
public class MyServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> clsSet, ServletContext servletContext)
throws ServletException {
MyHttpServlet servlet = new MyHttpServlet();
ServletRegistration.Dynamic registration
= servletContext.addServlet("servlet", servlet);
// loadOnStartup 設置成 -1 時,只有在第一次請求時,纔會調用 init 方法
registration.setLoadOnStartup(-1);
registration.addMapping("/*");
}
}
關於servlet 3.0
規範及ServletContainerInitializer
的作用,本文就不展開了,想了解的 小夥伴可自行百度。
1.3 創建spi
文件
爲了MyServletContainerInitializer
能被tomcat
加載到,我們還需要創建一個spi
文件:
如上圖,我們需要在test
的同級目錄下創建test資源包,我這裏將其命名爲testRes
,需要標記爲Test Resources Root
,然後在其下創建兩個目錄META-INF/service
,再創建一個文本文件,將其命名爲javax.servlet.ServletContainerInitializer
(就是ServletContainerInitializer
的全限定名了),文件內容如下:
org.apache.tomcat.demo.MyServletContainerInitializer
裏面的內容就是我們自己實現的ServletContainerInitializer
了。tomcat在啓動時,會找到META-INF/service/javax.servlet.ServletContainerInitializer
文件,讀取內容後,就能加載MyServletContainerInitializer
了,我們後面會從源碼的角度來分析這個過程。
1.4 主類
接下來是主類:
public class Demo01 {
@Test
public void test() throws Exception {
Tomcat tomcat = new Tomcat();
// 創建連接器
Connector connector = new Connector();
connector.setPort(8080);
connector.setURIEncoding("UTF-8");
tomcat.getService().addConnector(connector);
// 創建 context
String docBase = System.getProperty("java.io.tmpdir");
Context context = tomcat.addContext("", docBase);
// 得到 lifecycleListener 實際類型爲 ContextConfig
LifecycleListener lifecycleListener = (LifecycleListener)
Class.forName(tomcat.getHost().getConfigClass())
.getDeclaredConstructor().newInstance();
context.addLifecycleListener(lifecycleListener);
tomcat.start();
tomcat.getServer().await();
}
}
這個類還是比較簡單的,先創建了一個Connector
,然後向tomcat
中添加了一個Context
,最後就是啓動tomcat
了。
運行,然後在瀏覽器訪問http://localhost:8080
,結果如下:
可以看到,MyHttpServlet
可以正常對外訪問了。
2. tomcat
架構體系
在正式分析tomcat
源碼前,我們首先要對tomcat
有個整體的認識,tomcat
的整個架構體系如下:
Server
:整個Tomcat
服務器,一個Tomcat
只有一個Server
,標準實現爲StandardServer
;Service
:Server
中的一個邏輯功能層, 一個Server
可以包含多個Service
(他們是彼此完全獨立,只共享基本的JVM和系統路徑上的類),一個Service
負責維護一個或多個Connector
和一個Container
,標準實現爲StandardService
;Connector
:稱作連接器,是Service
的核心組件之一,一個Service
可以有多個Connector
,用於接受請求並將請求封裝成Request
和Response
,然後交給Container
進行處理,Container
處理完之後再交給Connector
返回給客戶端;Container
:Service
的另一個核心組件,它由四個子容器組件構成,分別是:Engine
、Host
、Context
、Wrapper
,這四個組件不是平行的,而是父子關係,Engine
包含Host
,Host
包含Context
,Context
包含Wrapper
;Jasper
:JSP
引擎;Session
:會話管理.
關於Container
的進一步說明:
Container
由四個子容器組件構成:Engine
、Host
、Context
、Wrapper
,關係如下:
Engine
:一個Service
中有 多個Connector
和 一個Engine
,Engine
表示整個Servlet
引擎,一個Engine
下面可以包含一個或者多個Host
,標準實現爲StandardEngine
;Host
:代表一個站點,也可以叫虛擬主機,一個Host
可以配置多個Context
,標準實現爲StandardHost
;Context
:代表Servlet
的Context
,它具備了Servlet
運行的基本環境,理論上只要有Context
就能運行Servlet
了,簡單的Tomcat
可以沒有Engine
和Host
,標準實現爲StandardContext
;Wrapper
:Wrapper
代表一個Servlet
,它負責管理一個Servlet
,包括的Servlet
的裝載、初始化、執行以及資源回收,Wrapper
是最底層的容器,它沒有子容器了,標準實現爲StandardWrapper
.
瞭解了tomcat這一層層的結構後,接下來我們就正式從源碼上來分析tomat
的啓動流程了。
3. Tomcat#start()
方法
tomcat 的啓動方法爲Tomcat#start()
,我們跟進去:
public void start() throws LifecycleException {
getServer();
server.start();
}
我們先看看getServer()
:
public Server getServer() {
if (server != null) {
return server;
}
System.setProperty("catalina.useNaming", "false");
// 創建標準的 server
server = new StandardServer();
...
// 添加 Service
Service service = new StandardService();
service.setName("Tomcat");
server.addService(service);
return server;
}
這個方法幹了兩件事:
- 創建
StandardServer
- 創建
StandardService
讓我們再回到Tomcat#start()
方法,server
對象有了,接下來就是server.start()
方法了,StandardServer
是LifecycleBase
的子類,讓我們進入LifecycleBase#start
方法:
public final synchronized void start() throws LifecycleException {
// 省略了部分代碼
...
if (state.equals(LifecycleState.NEW)) {
// 初始化
init();
} else if (state.equals(LifecycleState.FAILED)) {
stop();
} else if (!state.equals(LifecycleState.INITIALIZED) &&
!state.equals(LifecycleState.STOPPED)) {
invalidTransition(Lifecycle.BEFORE_START_EVENT);
}
try {
setStateInternal(LifecycleState.STARTING_PREP, null, false);
// 啓動
startInternal();
// 省略了部分代碼
...
} catch (Throwable t) {
handleSubClassException(t, "lifecycleBase.startFail", toString());
}
}
這裏我們省略了部分代碼,LifecycleBase#start
就只幹了兩件事:init()
(初始化)與startInternal()
(啓動),我們先看初始化方法LifecycleBase#init
:
@Override
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()
方法,這是個抽象方法,StandardService
重寫了它,我們進入StandardService#initInternal
方法:
protected void initInternal() throws LifecycleException {
super.initInternal();
// 1. 初始化 engine
if (engine != null) {
// 調用初始化方法
engine.init();
}
// 2. 初始化 Executor
for (Executor executor : findExecutors()) {
if (executor instanceof JmxEnabled) {
((JmxEnabled) executor).setDomain(getDomain());
}
executor.init();
}
// 3. 初始化 mapperListener
mapperListener.init();
// 4. 初始化 Connectors
synchronized (connectorsLock) {
for (Connector connector : connectors) {
connector.init();
}
}
}
這個方法裏會初始化engine
、Executor
、mapperListener
、Connectors
,我們先來看看engine.init()
,點進去,發現它到了LifecycleBase#init
方法!我們來看看LifecycleBase
的實現類:
可以看到,前面介紹的tomcat各組件都是LifecycleBase
的子類,上圖未列出的:
Connector
:
/** 繼承了 LifecycleMBeanBase */
public class Connector extends LifecycleMBeanBase {
}
/** 而 LifecycleMBeanBase 又繼承了 LifecycleBase */
public abstract class LifecycleMBeanBase extends LifecycleBase
implements JmxEnabled {
}
從源碼上可以看到,Tomcat#start()
方法裏執行了Server#start()
方法,Server#start()
又會執行Engine.start()
,Host#start()
方法的,一直到Wrapper#start()
,tomcat
正是以這種套娃般的方式進行啓動的,同樣的方法還有LifecycleBase#init
,也是這樣層層執行下去的,整個流程如圖:
StandardServer
、StandardService
、Connector
等組件都有重寫initInternal()
、startInternal()
方法,想要了解某個組件初始化或啓動時所做的工作,直接進入該類查看這兩個方法就可以了。
需要注意的是幾個Container
類,StandardEngine
、StandardHost
、StandardContext
、StandardWrapper
都是ContainerBase
的子類:
我們重點來看看ContainerBase
的startInternal(...)
方法:
protected synchronized void startInternal() throws LifecycleException {
...
// 啓動子容器
Container children[] = findChildren();
List<Future<Void>> results = new ArrayList<>();
for (Container child : children) {
// 在線程池中處理 啓動操作
results.add(startStopExecutor.submit(new StartChild(child)));
}
MultiThrowable multiThrowable = null;
for (Future<Void> result : results) {
try {
result.get();
} catch (Throwable e) {
...
}
}
...
}
/**
* 啓動子容器的類,實現了 Callable 接口
*/
private static class StartChild implements Callable<Void> {
private Container child;
public StartChild(Container child) {
this.child = child;
}
@Override
public Void call() throws LifecycleException {
// 啓動操作,調用的是 LifecycleBase#start 方法
child.start();
return null;
}
}
ContainerBase
的startInternal(...)
方法會先獲取到當前Container
的子Container
,然後調用LifecycleBase#start()
,這個啓動操作會被包裝成StartChild
對象,放在線程池中執行。注意,LifecycleBase#start(...)
操作時,會先調用init()
方法再調用start()
,因此Container
類的啓動與初始化是在這裏同一個方法裏進行的。
舉例來說,
StandardEngine
調用startInternal(...)
方法,找到的子Container
爲StandardHost
,再就調用StandardHost#init(...)
與StandardHost#start(...)
方法;- 調用
StandardHost#start(...)
方法時,又會調用ContainerBase#startInternal(...)
方法,再又找到StandardHost
的子Container
爲StandardContext
,然後又執行它的init(...)
與start(...)
方法; - 直到運行
StandardWrapper#start(...)
,由於StandardWrapper
沒有子Container
了,這種套娃式的運行就結束了。
實際上,前面的組件都沒做什麼實質性工作,只是爲了初始化或啓動下一個組件,真正幹活的是最後的組件,如
Context
:會在這裏處理servlet
的加載Connector
:處理http連接,開啓端口監聽
這些內容在分析具體功能時再展開分析吧。
4. 總結
本文作爲tomcat源碼正式分析的第一篇,主要是準備了一個源碼分析demo,然後介紹了tomcat的各大組件,最後闡述了tomcat的整個啓動流程,旨在讓大家對tomcat有一個全局上的認識。
限於作者個人水平,文中難免有錯誤之處,歡迎指正!原創不易,商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。
本文首發於微信公衆號 Java技術探祕,如果您喜歡本文,歡迎關注該公衆號,讓我們一起在技術的世界裏探祕吧!