Tomcat中的容器是如何處理請求的

前言

上一篇《Tomcat中的鏈接器是如何設計的》介紹了Tomcat中鏈接器的設計,咱們知道鏈接器是負責監聽網絡端口,獲取鏈接請求,而後轉換符合Servlet標準的請求,交給容器去處理,那麼咱們這篇文章將順着上一篇文章的思路,看看一個請求到了容器,容器是如何請求的。web

說明:本文tomcat版本是9.0.21,不建議零基礎讀者閱讀。apache

從Adapter中提及

咱們繼續跟着上篇文章Adapter的源碼,繼續分析,上篇文章結尾的源碼以下:tomcat

//源碼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是作什麼的。bash

//源碼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)

咱們看到上面的源碼是PipelineValve的接口,Pipeline主要是設置Valve,而Valve是一個鏈表,而後能夠進行invoke方法的調用。咱們回顧下這段源碼:app

//源碼4
connector.getService().getContainer().getPipeline().getFirst().invoke(
                        request, response);
複製代碼

這裏是直接獲取容器的管道,而後獲取第一個Valve進行調用。咱們在以前提到過Valve是一個鏈表,這裏只調用第一個,也就是能夠經過Next去調用到最後一個。咱們再回顧下咱們第一篇文章《Tomcat在SpringBoot中是如何啓動的》中提到過,容器是分爲4個子容器,分別爲EngineHostContextWrapper,他們同時也是父級和子級的關係,Engine>Host>Context>Wrapper框架

我以前提到過,每一個容器都一個Pipeline,那麼這個是怎麼體現出來的呢?咱們看容器的接口源碼就能夠發現,Pipeline是容器接口定義的一個基本屬性:async

//源碼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中,咱們發現了以下源碼:ide

/**
// 源碼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方法中打個斷點,效果以下:post

這裏咱們能夠知道,在適配器調用容器的時候,也就是調用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的實現類

這裏咱們熟悉的HttpServletGenericServletTomcat包的類,實際上只有HttpServlet,由於GenericServletHttpServlet的父類。後面就是移交給了框架去處理了,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.comweb.luozhou.com 而後我每一個域名下部署2個應用,分別是User,log,blog,shop。那麼當我去想去添加用戶的時候,我就會請求admin.luozhou.com域名下的UserContext下面的add的Servlet(說明:這裏例子設計不符合實際開發原則,add這種粒度應該是框架中的controller完成,而不是Servlet)。

總結

這篇文章咱們研究了Tomcat中容器是如何處理請求的,咱們來回顧下內容:

  • 鏈接器把請求丟給適配器適配後調用容器(Engine)
  • 容器內部是經過管道(Pieline)-閥門(Valve)模式完成容器的調用的,父容器調用子容器主要經過一個basic的閥門來完成的。
  • 最後一個子容器wrapper完成調用後就會構建過濾器來進行過濾器調用,調用完成後就到了Tomcat內部的最後一步,調用servlet。也能夠理解咱們經常使用的HttpServlet,全部基於Servlet規範的框架在這裏就進入了框架流程(包括SpringBoot)。
  • 最後咱們還分析了Tomcat是如何實現多應用隔離的,經過多應用的隔離分析,咱們也明白了爲何Tomcat要設計如此多的子容器,多子容器能夠根據須要完成不一樣粒度的隔離級別來實現不一樣的場景需求。
相關文章
相關標籤/搜索