前言
上一篇《Tomcat中的聯結器是如何設計的》介紹了Tomcat中聯結器的設計,我們知道聯結器是負責監聽網路埠,獲取連線請求,然後轉換符合Servlet標準的請求,交給容器去處理,那麼我們這篇文章將順著上一篇文章的思路,看看一個請求到了容器,容器是如何請求的。
說明:本文tomcat版本是9.0.21,不建議零基礎讀者閱讀。
從Adapter中說起
我們繼續跟著上篇文章Adapter
的原始碼,繼續分析,上篇文章結尾的原始碼如下:
//原始碼1.類: CoyoteAdapter implements Adapter
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res)
throws Exception {
Request request = (Request) req.getNote(ADAPTER_NOTES);
Response response = (Response) res.getNote(ADAPTER_NOTES);
postParseSuccess = postParseRequest(req, request, res, response);
if (postParseSuccess) {
//check valves if we support async
request.setAsyncSupported(
connector.getService().getContainer().getPipeline().isAsyncSupported());
// Calling the container
connector.getService().getContainer().getPipeline().getFirst().invoke(
request, response);
}
}
複製程式碼
上面的原始碼的主要作用就是獲取到容器,然後呼叫getPipeline()
獲取Pipeline
,最後去invoke
呼叫,我們來看看這個Pipeline
是做什麼的。
//原始碼2.Pipeline介面
public interface Pipeline extends Contained {
public Valve getBasic();
public void setBasic(Valve valve);
public void addValve(Valve valve);
public Valve[] getValves();
public void removeValve(Valve valve);
public Valve getFirst();
public boolean isAsyncSupported();
public void findNonAsyncValves(Set<String> result);
}
//原始碼3. Valve介面
public interface Valve {
public Valve getNext();
public void setNext(Valve valve);
public void backgroundProcess();
public void invoke(Request request, Response response)
throws IOException, ServletException;
public boolean isAsyncSupported();
複製程式碼
我們從字面上可以理解Pipeline
就是管道,而Valve
就是閥門,實際上在Tomcat中的作用也是和字面意思差不多。每個容器都有一個管道,而管道中又有多個閥門。我們通過後面的分析來證明這一點。
管道-閥門(Pipeline-Valve)
我們看到上面的原始碼是Pipeline
和Valve
的介面,Pipeline
主要是設定Valve
,而Valve
是一個連結串列,然後可以進行invoke
方法的呼叫。我們回顧下這段原始碼:
//原始碼4
connector.getService().getContainer().getPipeline().getFirst().invoke(
request, response);
複製程式碼
這裡是直接獲取容器的管道,然後獲取第一個Valve
進行呼叫。我們在之前提到過Valve
是一個連結串列,這裡只呼叫第一個,也就是可以通過Next去呼叫到最後一個。我們再回顧下我們第一篇文章《Tomcat在SpringBoot中是如何啟動的》中提到過,容器是分為4個子容器,分別為Engine
、Host
、Context
、Wrapper
,他們同時也是父級和子級的關係,Engine
>Host
>Context
>Wrapper
。
我之前提到過,每個容器都一個Pipeline
,那麼這個是怎麼體現出來的呢?我們看容器的介面原始碼就可以發現,Pipeline
是容器介面定義的一個基本屬性:
//原始碼5.
public interface Container extends Lifecycle {
//省略其他程式碼
/**
* Return the Pipeline object that manages the Valves associated with
* this Container.
*
* @return The Pipeline
*/
public Pipeline getPipeline();
}
複製程式碼
我們知道了每個容器都有一個管道(Pipeline
),管道中有許多閥門(Valve
),Valve
可以進行鏈式呼叫,那麼問題來了,父容器管道中的Valve
怎麼呼叫到子容器中的Valve
呢?在Pipeline
的實現類StandardPipeline
中,我們發現瞭如下原始碼:
/**
// 原始碼6.
* The basic Valve (if any) associated with this Pipeline.
*/
protected Valve basic = null;
/**
* The first valve associated with this Pipeline.
*/
protected Valve first = null;
public void addValve(Valve valve) {
//省略部分程式碼
// Add this Valve to the set associated with this Pipeline
if (first == null) {
first = valve;
valve.setNext(basic);
} else {
Valve current = first;
while (current != null) {
//這裡迴圈設定Valve,保證最後一個是basic
if (current.getNext() == basic) {
current.setNext(valve);
valve.setNext(basic);
break;
}
current = current.getNext();
}
}
container.fireContainerEvent(Container.ADD_VALVE_EVENT, valve);
}
複製程式碼
根據如上程式碼,我們知道了basic
是一個管道(Pipeline
)中的最後一個閥門,按道理只要最後一個閥門是下一個容器的第一個閥門就可以完成全部的鏈式呼叫了。我們用一個請求debug下看看是不是和我們的猜測一樣,我們在CoyoteAdapter
中的service
方法中打個斷點,效果如下:
這裡我們可以知道,在介面卡呼叫容器的時候,也就是呼叫Engine
的管道,只有一個閥門,也就是basic,值為StandardEngineValve
。我們發現這個閥門的invoke方法如下:
//原始碼7.
public final void invoke(Request request, Response response)
throws IOException, ServletException {
// Select the Host to be used for this Request
Host host = request.getHost();
if (host == null) {
// HTTP 0.9 or HTTP 1.0 request without a host when no default host
// is defined. This is handled by the CoyoteAdapter.
return;
}
if (request.isAsyncSupported()) {
request.setAsyncSupported(host.getPipeline().isAsyncSupported());
}
// Ask this Host to process this request
host.getPipeline().getFirst().invoke(request, response);
}
複製程式碼
我們繼續debug檢視結果如下:
所以這裡的basic
實際上將會呼叫到Host
容器的管道(Pipeline
)和閥門(Valve
),也就是說,每個容器管道中的basic
是負責呼叫下一個子容器的閥門。我用一張圖來表示:
這張圖清晰的描述了,Tomcat內部的容器是如何流轉請求的,從聯結器(Connector
)過來的請求會進入Engine
容器,Engine
通過管道(Pieline
)中的閥門(Valve
)來進行鏈式呼叫,最後的basic
閥門是負責呼叫下一個容器的第一個閥門的,一直呼叫到Wrapper
,然後Wrapper
再執行Servlet
。
我們看看Wrapper
原始碼,是否真的如我們所說:
//原始碼8.
public final void invoke(Request request, Response response)
throws IOException, ServletException {
//省略部分原始碼
Servlet servlet = null;
if (!unavailable) {
servlet = wrapper.allocate();
}
// Create the filter chain for this request
ApplicationFilterChain filterChain =
ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
filterChain.doFilter(request.getRequest(),
response.getResponse());
}
複製程式碼
看到這裡,你可能會說這裡明明只是建立了過濾器(Filter
)並且去呼叫而已,並沒有去呼叫Servlet
,沒錯,這裡確實沒有去呼叫Servlet
,但是我們知道,過濾器(Filter
)是在Servlet
之前執行的,也就是說,filterChain.doFilter
執行完之後變會執行Servlet
。我們看看ApplicationFilterChain
的原始碼是否如我們所說:
//原始碼9.
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
//省略部分程式碼
internalDoFilter(request,response);
}
//原始碼10.
private void internalDoFilter(ServletRequest request,
ServletResponse response)
throws IOException, ServletException {
//省略部分程式碼
// Call the next filter if there is one
if (pos < n) {
//省略部分程式碼
ApplicationFilterConfig filterConfig = filters[pos++];
Filter filter = filterConfig.getFilter();
filter.doFilter(request, response, this);
return;
}
//呼叫servlet
// We fell off the end of the chain -- call the servlet instance
servlet.service(request, response);
複製程式碼
通過原始碼我們發現,在呼叫完所有的過濾器(Filter
)之後,servlet
就開始呼叫service
。我們看看servlet
的實現類
這裡我們熟悉的HttpServlet
和GenericServlet
是Tomcat
包的類,實際上只有HttpServlet
,因為GenericServlet
是HttpServlet
的父類。後面就是移交給了框架去處理了,Tomcat內部的請求已經到此是完成了。
Tomcat的多應用隔離實現
我們知道,Tomcat是支援部署多個應用的,那麼Tomcat是如何支援多應用的部署呢?是怎麼保證多個應用之間不會混淆的呢?要想弄懂這個問題,我們還是要回到介面卡去說起,回到service
方法
//原始碼11.類:CoyoteAdapter
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res)
throws Exception {
//省略部分程式碼
// Parse and set Catalina and configuration specific
// request parameters
//處理URL對映
postParseSuccess = postParseRequest(req, request, res, response);
if (postParseSuccess) {
//check valves if we support async
request.setAsyncSupported(
connector.getService().getContainer().getPipeline().isAsyncSupported());
// Calling the container
connector.getService().getContainer().getPipeline().getFirst().invoke(
request, response);
}
}
複製程式碼
我們在之前的原始碼中只談到了connector.getService().getContainer().getPipeline().getFirst().invoke( request, response)
這段程式碼,這部分程式碼是呼叫容器,但是在呼叫容器之前有個postParseRequest
方法是用來處理對映請求的,我們跟進看看原始碼:
//原始碼12.類:CoyoteAdapter
protected boolean postParseRequest(org.apache.coyote.Request req, Request request,
org.apache.coyote.Response res, Response response) throws IOException, ServletException {
省略部分程式碼
boolean mapRequired = true;
while (mapRequired) {
// This will map the the latest version by default
connector.getService().getMapper().map(serverName, decodedURI,
version, request.getMappingData());
//沒有找到上下文就報404錯誤
if (request.getContext() == null) {
// Don't overwrite an existing error
if (!response.isError()) {
response.sendError(404, "Not found");
}
// Allow processing to continue.
// If present, the error reporting valve will provide a response
// body.
return true;
}
}
複製程式碼
這裡就是迴圈去處理Url對映,如果Context
沒有找到,就返回404錯誤,我們繼續看原始碼:
//原始碼13.類:Mapper
public void map(MessageBytes host, MessageBytes uri, String version,
MappingData mappingData) throws IOException {
if (host.isNull()) {
String defaultHostName = this.defaultHostName;
if (defaultHostName == null) {
return;
}
host.getCharChunk().append(defaultHostName);
}
host.toChars();
uri.toChars();
internalMap(host.getCharChunk(), uri.getCharChunk(), version, mappingData);
}
//原始碼14.類:Mapper
private final void internalMap(CharChunk host, CharChunk uri,
String version, MappingData mappingData) throws IOException {
//省略部分程式碼
// Virtual host mapping 處理Host對映
MappedHost[] hosts = this.hosts;
MappedHost mappedHost = exactFindIgnoreCase(hosts, host);
//省略部分程式碼
if (mappedHost == null) {
mappedHost = defaultHost;
if (mappedHost == null) {
return;
}
}
mappingData.host = mappedHost.object;
// Context mapping 處理上下文對映
ContextList contextList = mappedHost.contextList;
MappedContext[] contexts = contextList.contexts;
//省略部分程式碼
if (context == null) {
return;
}
mappingData.context = contextVersion.object;
mappingData.contextSlashCount = contextVersion.slashCount;
// Wrapper mapping 處理Servlet對映
if (!contextVersion.isPaused()) {
internalMapWrapper(contextVersion, uri, mappingData);
}
}
複製程式碼
由於上面的原始碼比較多,我省略了很多程式碼,保留了能理解主要邏輯的程式碼,總的來說就是處理Url包括三部分,對映Host
,對映Context
和對映Servlet
(為了節省篇幅,具體細節原始碼請感興趣的同學自行研究)。
這裡我們可以發現一個細節,就是三個處理邏輯都是緊密關聯的,只有Host
不為空才會處理Context
,對於Servlet
也是同理。所以這裡我們只要Host
配置不同,那麼後面所有的子容器都是不同的,也就完成了應用隔離的效果。但是對於SpringBoot內嵌Tomcat方式(使用jar包啟動)來說,並不具備實現多應用的模式,本身一個應用就是一個Tomcat。
為了便於理解,我也畫了一張多應用隔離的圖,這裡我們假設有兩個域名admin.luozhou.com
和web.luozhou.com
然後我每個域名下部署2個應用,分別是User
,log
,blog
,shop
。那麼當我去想去新增使用者的時候,我就會請求admin.luozhou.com
域名下的User
的Context
下面的add
的Servlet(說明:這裡例子設計不符合實際開發原則,add這種粒度應該是框架中的controller完成,而不是Servlet)。
總結
這篇文章我們研究了Tomcat中容器是如何處理請求的,我們來回顧下內容:
- 聯結器把請求丟給介面卡適配後呼叫容器(
Engine
) - 容器內部是通過管道(
Pieline
)-閥門(Valve
)模式完成容器的呼叫的,父容器呼叫子容器主要通過一個basic
的閥門來完成的。 - 最後一個子容器
wrapper
完成呼叫後就會構建過濾器來進行過濾器呼叫,呼叫完成後就到了Tomcat內部的最後一步,呼叫servlet。也可以理解我們常用的HttpServlet
,所有基於Servlet
規範的框架在這裡就進入了框架流程(包括SpringBoot)。 - 最後我們還分析了Tomcat是如何實現多應用隔離的,通過多應用的隔離分析,我們也明白了為什麼Tomcat要設計如此多的子容器,多子容器可以根據需要完成不同粒度的隔離級別來實現不同的場景需求。
版權宣告:原創文章,轉載請註明出處。