Tomcat剖析(五):Tomcat 容器
- 1. Tomcat剖析(一):一個簡單的Web服務器
- 2. Tomcat剖析(二):一個簡單的Servlet服務器
- 3. Tomcat剖析(三):連接器(1)
- 4. Tomcat剖析(三):連接器(2)
- 5. Tomcat剖析(四):Tomcat默認連接器(1)
- 6. Tomcat剖析(四):Tomcat默認連接器(2)
- 7. Tomcat剖析(五):容器
第一部分:概述
這一節基於《深度剖析Tomcat》第五章:容器 總結而成
一定要先到我的github上下載本書相關的代碼,同時去上網下載這本書。
看了不懂的歡迎加我QQ一起探討或者在博客下評論發問。
時隔兩個多月,中間包括了學校的期末考試月和一些瑣粹的事情。
這一部分會先給大家先總結一下Tomcat連接器的相關內容,然后是 Tomcat 容器的總體介紹,最后在下一部分進入本章的主題: Tomcat 容器 的詳細講解。
Tomcat 連接器總結
我們都知道 Catalina 的核心組件包括 連接器和容器,那連接器是干嘛的呢?前面講過,總的來說就是等待用戶的請求,接收用戶請求,解析請求,封裝成請求對象和響應對象,然后將請求和響應對象交給容器處理。
整個過程的細節包括了創建ServerSocket對象,創建處理器池,在每個處理器內創建請求和響應對象,然后啟動所有處理器線程(阻塞,因為沒有請求到達,無法處理),然后等待用戶請求(阻塞), 用戶請求到來時,獲取其中一個處理器線程,將socket交給處理器處理,以喚醒處理器線程(利用available變量),讓處理器線程解析請求,具體方法是解析端口,解析請求頭等信息,解析完后交給容器真正處理請求的內容(如servlet,就是調用我們常說的invoke方法),處理完后就講處理器的所有變量恢復到未被使用前,最后回收回線程池。
connector.getContainer().invoke(request, response);
Tomcat 容器概述
前面幾節我們都在學習 Tomcat連接器是如何實現的,也學到了它實現的巧妙之處,而這一節我們要開始學習Tomcat的另一個組件:容器。和連接器一樣,容器有着不可忽略的作用,我們看到連接器從頭到尾都是在做一些基礎的工作,從等待請求到解析請求,卻沒有參與到處理用戶真正想要的東西(如servlet要進行某些操作),所以說容器是連接器基礎工作做完后,做真正的請求處理操作(加載servlet)。看到我們上面的連接器總結沒,invoke方法就是本節我們要講解的重中之重----獲取容器執行invoke方法。
容器是一個處理用戶 servlet 請求並返回對象給 web 用戶的模塊。org.apache.catalina.Container 接口定義了容器的形式,有四種容器: Engine(引擎) , Host(主機) , Context(上下文) , 和 Wrapper(包裝器)。
我也按照書上的邏輯,先介紹Context和Wrapper,其它兩個留以后講解。
對於 Catalina 的容器首先需要注意的是它一共有四種不同的容器:
- Engine:表示整個 Catalina 的 servlet 引擎
- Host:表示一個擁有數個上下文的虛擬主機
- Context:表示一個 Web 應用,一個 context 包含一個或多個wrapper
- Wrapper:表示一個獨立的 servlet
它們的標准實現是 StandardEngine,StandardHost, StandardContext, and StandardWrapper,它們都是org.apache.catalina.core 包的一部分,這些類都擴展了抽象類 ContainerBase。
要實現一個容器,首先必須要實現 org.apache.catalina.Container 接口。從上一節的連接器講解中我們也看到了ex04.pyrmont.core.SimpleContainer 也實現了Container接口,實現了簡單的servlet的功能
public class SimpleContainer implements Container {
public void invoke(Request request, Response response) throws IOException,
ServletException {
//中間省略
}
}
同時,我們需要知道的是並不是每一個功能都需要用到四種容器,下面的程序中會看到。
一個容器可以有一個或多個低層次上的子容器,例如,一個 Context 有一個或多個 wrapper,而 wrapper 作為容器層次中的最底層,不能包含子容器。可以使用在 Container 接口中定義的 addChild()方法,wrapper調用會拋出異常。
容器的整個處理流程通過一個叫做Pipeline(流水線)的接口實現,這個接口的功能是放置一些Valve(閥門),這些閥門的功能是執行一些基礎功能操作,比如記錄日志,記錄ip等,像流水線一樣,通過閥門上下文接口(ValveContext)調用下一個閥門,最后進入一個基礎閥門,實現用戶真正的請求。我估計如果之前沒學過容器的相關內容是看不懂我再說什么的,第二部分再講解。
Tomcat 容器核心接口:
Pipeline: 一個 pipeline 包含了改容器要喚醒的所有任務。每一個閥門表示了一個特定的任務。一個容器的流水線有一個基本的閥門,但是你可以添加任意你想要添加的閥門。閥門的數目定義為添加的閥門的個數(不包括基本閥門)。
Valve:閥門接口表示一個閥門,該組件負責處理請求。
ValveContext: 用於切換到下一個閥門
Contained:一個閥門可以選擇性的實現 org.apache.catalina.Contained 接口。該接口定義了其實現類跟一個容器相關聯。包含 getContainer和setContainer 方法
Wrapper:org.apache.catalina.Wrapper 接口表示了一個包裝器。一個包裝器是表示一個獨立 servlet 定義的容器。包裝器繼承了 Container 接口,並且添加了幾個方法。包裝器的實現類負責管理其下層 servlet 的生命周期,包括 servlet 的 init,service,和 destroy 方法。
Context:一個 context 在容器中表示一個 web 應用。一個 context 通常含有一個或多個包裝器作為其子容器。
希望大家看到不懂的不要沮喪,通過下面的講解,大家應該都能明白吧。 這里省略了所有連接器的內容,從invoke方法開始講起。
第二部分:詳細講解
為了模擬容器的實現,容器,流水線等都是用ex05.pyrmont包下相關的類,如SimpleWrapper等,而不是去查看StandardWrapper源代碼
啟動類Bootstrap1.java
同樣的我們先接觸啟動類ex05.pyrmont.startup.Bootstrap1.java,這個類是對單個servlet的處理,所以可以看到只用了wrapper,沒有用到context。如果我們要訪問,可以用localhost:8080/ModernServlet
public final class Bootstrap1 {
public static void main(String[] args) {
HttpConnector connector = new HttpConnector();
//定義一個包裝器類(容器),實際上connector.getContainer().invoke(request, response);
//就是調用SimpleWrapper對象的invoke方法
Wrapper wrapper = new SimpleWrapper();
wrapper.setServletClass("ModernServlet");
//定義一個類加載器
Loader loader = new SimpleLoader();
//下面就是兩個閥門
Valve valve1 = new HeaderLoggerValve();
Valve valve2 = new ClientIPLoggerValve();
//容器綁定類加載器
wrapper.setLoader(loader);
//容器添加閥門
((Pipeline) wrapper).addValve(valve1);
((Pipeline) wrapper).addValve(valve2);
//連接器綁定容器,就是connector.getContainer().invoke時要將請求交給容器處理
connector.setContainer(wrapper);
try {
//下面兩個方法是連接器相關內容,不再重復
connector.initialize();
connector.start();
// make the application wait until we press a key.
System.in.read();
}
catch (Exception e) {
e.printStackTrace();
}
}
}
當然,直接看注釋是沒用的,因為我們不清楚我們為什么要這樣做。不急,下面慢慢講解。記得不斷調回這個類整理邏輯哦
SimpleWrapper.java
這個類算是本節比較重要的類了,大部分的接口都可以在這里看到。 SimpleWrapper是invoke出現的地方。
補充說明在代碼后面
下面貼上比較重要的代碼和相應注釋
public class SimpleWrapper implements Wrapper, Pipeline {
// the servlet instance
private Servlet instance = null;//加載到的servlet實例
private String servletClass;
private Loader loader;//類加載器
private String name;
//從一開始就綁定創建的流水線是該容器
private SimplePipeline pipeline = new SimplePipeline(this);
protected Container parent = null;
public SimpleWrapper() {
//其實我們在創建SimpleWrapper對象時就設置流水線的基礎閥門,通過setBasic
pipeline.setBasic(new SimpleWrapperValve());
}
public synchronized void addValve(Valve valve) {
//為流水線添加其他Valve
pipeline.addValve(valve);
}
//這個方法的作用是加載Servlet,獲取到Servlet實例
public Servlet allocate() throws ServletException {
// Load and initialize our instance if necessary
if (instance==null) {
try {
instance = loadServlet();
}
catch (ServletException e) {
throw e;
}
catch (Throwable e) {
throw new ServletException("Cannot allocate a servlet instance", e);
}
}
return instance;
}
//具體加載servlet的方法
private Servlet loadServlet() throws ServletException {
if (instance!=null)
return instance;
Servlet servlet = null;
String actualClass = servletClass;
if (actualClass == null) {
throw new ServletException("servlet class has not been specified");
}
Loader loader = getLoader();//獲取到我們定義的類加載器類SimpleLoader,定義和我們需要的加載方式
// Acquire an instance of the class loader to be used
if (loader==null) {
throw new ServletException("No loader.");
}
ClassLoader classLoader = loader.getClassLoader();
// Load the specified servlet class from the appropriate class loader
Class classClass = null;
try {
if (classLoader!=null) {
classClass = classLoader.loadClass(actualClass);//加載servlet
}
}
catch (ClassNotFoundException e) {
throw new ServletException("Servlet class not found");
}
// Instantiate and initialize an instance of the servlet class itself
try {
servlet = (Servlet) classClass.newInstance();//創建servlet實例
}
catch (Throwable e) {
throw new ServletException("Failed to instantiate servlet");
}
// Call the initialization method of this servlet
try {
//這里調用了servlet的初始化方法,所有跟我們以前學過的知識一樣,init方法只調用一次
servlet.init(null);
}
catch (Throwable f) {
throw new ServletException("Failed initialize servlet.");
}
return servlet;
}
//就是調用這個invoke方法
public void invoke(Request request, Response response)
throws IOException, ServletException {
pipeline.invoke(request, response);
}
這個類里面有幾個比較關心的方法:
- invoke方法:我們講解的入口
- addValve方法:添加閥門,其中基礎閥門在創建SimpleWrapper實例時就設置了
- allocate方法:獲取類加載器,獲取servlet實例
invoke方面里面的是pipeline.invoke(request, response),前面我們說到,Pineline的作用是負責處理多個任務,
有的人就認為為什么不直接調用我們的Valve,甚至說不直接去掉Pineline,這里我要說一下,我們不能簡簡單單的認為容器就只是處理我們要做的任務,還有很多任務要做的,比如記錄是什么IP操作過的,這些就像我們程序中用的攔截器一樣,需要先經過很多的中間步驟。所以回到Bootstrap1.java中的addValve操作,和setLoader方法,可以理解了吧。當然,addValve要真正理解需要繼續看下去
那流水線的invoke是如何實現多個任務按順序處理執行的呢?
SimplePipeline.java
SimpleWrapper.java中一開始就創建流水線對象,並綁定容器為SimpleWrapper,因為invoke中要用到
pipeline.invoke(request, response);
下面查看部分關鍵代碼
public class SimplePipeline implements Pipeline {
public void invoke(Request request, Response response)
throws IOException, ServletException {
// Invoke the first Valve in this pipeline for this request
(new SimplePipelineValveContext()).invokeNext(request, response);
}
//添加閥門
public void addValve(Valve valve) {
if (valve instanceof Contained)
((Contained) valve).setContainer(this.container);
synchronized (valves) {
Valve results[] = new Valve[valves.length +1];
System.arraycopy(valves, 0, results, 0, valves.length);
results[valves.length] = valve;
valves = results;
}
}
public void setBasic(Valve valve) {
this.basic = valve;
((Contained) valve).setContainer(container);
}
//SimplePipeline的內部類,實現ValveContext接口,負責切換到下一個閥門
protected class SimplePipelineValveContext implements ValveContext {
protected int stage = 0;
public String getInfo() {
return null;
}
public void invokeNext(Request request, Response response)
throws IOException, ServletException {
int subscript = stage;
stage = stage + 1;
// Invoke the requested Valve for the current request thread
if (subscript < valves.length) {//先調用設置的閥門
valves[subscript].invoke(request, response, this);
}
else if ((subscript == valves.length) && (basic != null)) {
basic.invoke(request, response, this);//最后才是basic閥門
}
else {
throw new ServletException("No valve");
}
}
} // end of inner class
}
需要注意的是:我專門加setBasic方法作為關鍵代碼貼出來,是為了提醒大家SimplePineline上在SimpleWrapper中已經設置了基本Valve,
我們看到SimplePipeline的invoke方法里面是創建一個內部類SimplePipelineValveContext對象:這個對象負責流水線上閥門Valve的切換,這個內部類實現了ValveContext接口,ValveContext接口中其中一個方法是invokeNext方法,用來調取下一個Valve,具體的實現也不難
- invokeNext 方法使用下標(subscript)和級別( stage)記住哪個閥門被喚醒。當第一次喚醒的時候,下標的值是 0,級的值是 1。第一次,第一個閥門被喚醒,流水線的閥門獲得 ValveContext 實例調用它的 invokeNext 方法。這時下標的值是 1 所以下一個閥門被喚醒,然后一步步的進行。
下一小節,我們先選擇其中一個閥門講解
HeaderLoggerValve.java
這個類實現了Valve接口和Contained接口,我們知道Valve的接口功能是負責處理請求,所以接口里面有invoke方法
public void invoke(Request request, Response response,
ValveContext context)
throws IOException, ServletException;
HeaderLoggerValve.java是用來輸出請求頭的內容。
public class HeaderLoggerValve implements Valve, Contained {
protected Container container;
public void invoke(Request request, Response response, ValveContext valveContext)
throws IOException, ServletException {
// Pass this request on to the next valve in our pipeline
valveContext.invokeNext(request, response);//執行下一個閥門
System.out.println("Header Logger Valve");
ServletRequest sreq = request.getRequest();
if (sreq instanceof HttpServletRequest) {
HttpServletRequest hreq = (HttpServletRequest) sreq;
Enumeration headerNames = hreq.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement().toString();
String headerValue = hreq.getHeader(headerName);
System.out.println(headerName + ":" + headerValue);
}
}
else
System.out.println("Not an HTTP Request");
System.out.println("------------------------------------");
}
}
invoke(Request request, Response response, ValveContext context)方法中需要注意的是第三個參數ValveContext context, 我們會看到SimplePipeline里面傳遞了this引用,在某個閥門調用context的invokeNext方法時
valveContext.invokeNext(request, response);
以保證下標(subscript)和級別(stage)的值是正確的(因為是同一個引用),下一次執行的閥門就是valve[1], 也就是ClientIPLoggerValve。
最后調用的是基礎閥門
basic.invoke(request, response, this);//最后才是basic閥門
SimpleWrapperValve.java
SimpleWrapperValve.java也是一個閥門,所以也實現了Valve和Contained接口
public class SimpleWrapperValve implements Valve, Contained {
protected Container container;
public void invoke(Request request, Response response, ValveContext valveContext)
throws IOException, ServletException {
SimpleWrapper wrapper = (SimpleWrapper) getContainer();
ServletRequest sreq = request.getRequest();
ServletResponse sres = response.getResponse();
Servlet servlet = null;
HttpServletRequest hreq = null;
//看到沒,這里強制轉換為HttpServletRequest和HttpServletResponse
if (sreq instanceof HttpServletRequest)
hreq = (HttpServletRequest) sreq;
HttpServletResponse hres = null;
if (sres instanceof HttpServletResponse)
hres = (HttpServletResponse) sres;
// Allocate a servlet instance to process this request
try {
//加載我們想訪問的真正的servlet,這個方法上面講了
servlet = wrapper.allocate();
if (hres!=null && hreq!=null) {
servlet.service(hreq, hres);//這里調用了service
}
else {
servlet.service(sreq, sres);
}
}
catch (ServletException e) {
}
}
public String getInfo() {
return null;
}
public Container getContainer() {
return container;
}
public void setContainer(Container container) {
this.container = container;
}
}
SimpleWrapperValve作為基礎閥門,終於找到我們夢寐已久的service方法
其中SimpleWrapper中我們看到了這個容器是如何加載servlet的,加載的時候調用了servlet的init方法,
也就是基礎閥門的invoke方法中調用加載初始化(wrapper.allocate())
接下來調用了servlet的service方法,
我們看到servlet是加載一次的而已,所以我們可以得到結論:Servlet不是線程安全的(這次不再是只看結論了哦,我們是推出來的,實在不信,可以看看Standard*的源代碼)
第三部分:小結
講了這么多,將所有的重要接口都混合在代碼里面講解了,不知道大家認識到多少
我感覺 Tomcat容器 理解代碼還是次要的 最重要的是明白 Tomcat 為什么要這樣進行模塊設計,我們可以好好學習這種思想,為什么要分這些接口, 不管讀什么,讀源代碼就是這樣,在理解是如何本身是如何實現的基礎上,理解設計思想。
Point: 讀同一份源代碼,一萬個人有一萬中感悟
附
相應代碼可以在我的github上找到下載,拷貝到eclipse,然后打開對應包的代碼即可。
如發現編譯錯誤,可能是由於jdk不同版本對編譯的要求不同導致的,可以不管,供學習研究使用。