Tomcat 7 的一次請求分析(四)Tomcat 7 閥機制原理

預流發表於2018-02-01

通過這一系列的前三部分看到了一次客戶端連線在 Tomcat 內部被轉換成了請求物件(org.apache.catalina.connector.Request類的例項),並在該請求物件內部將與本次請求相關的 Host、Context、Wrapper 物件的引用。本文主要分析該請求物件在容器內部流轉的經過。

再來看一下 Tomcat 7 內部的元件結構圖:

Tomcat 7 的一次請求分析(四)Tomcat 7 閥機制原理
其實這張圖已經給出了答案,在 Connector 接收到一次連線並轉化成請求( Request )後,會將請求傳遞到 Engine 的管道( Pipeline )的閥( ValveA )中。請求在 Engine 的管道中最終會傳遞到 Engine Valve 這個閥中。接著請求會從 Engine Valve 傳遞到一個 Host 的管道中,在該管道中最後傳遞到 Host Valve 這個閥裡。接著從 Host Valve 傳遞到一個 Context 的管道中,在該管道中最後傳遞到 Context Valve 中。接下來請求會傳遞到 Wrapper C 內的管道所包含的閥 Wrapper Valve 中,在這裡會經過一個過濾器鏈( Filter Chain ),最終送到一個 Servlet 中。

如果你不瞭解上面這段文字描述中所謂的管道( Pipeline )和閥( Valve )的概念,別急,下面會講到這個。先從原始碼層面看下這段文字描述的經過。上面提到的org.apache.catalina.connector.CoyoteAdapter類的 service 方法:

     1	    public void service(org.apache.coyote.Request req,
     2	                        org.apache.coyote.Response res)
     3	        throws Exception {
     4	
     5	        Request request = (Request) req.getNote(ADAPTER_NOTES);
     6	        Response response = (Response) res.getNote(ADAPTER_NOTES);
     7	
     8	        if (request == null) {
     9	
    10	            // Create objects
    11	            request = connector.createRequest();
    12	            request.setCoyoteRequest(req);
    13	            response = connector.createResponse();
    14	            response.setCoyoteResponse(res);
    15	
    16	            // Link objects
    17	            request.setResponse(response);
    18	            response.setRequest(request);
    19	
    20	            // Set as notes
    21	            req.setNote(ADAPTER_NOTES, request);
    22	            res.setNote(ADAPTER_NOTES, response);
    23	
    24	            // Set query string encoding
    25	            req.getParameters().setQueryStringEncoding
    26	                (connector.getURIEncoding());
    27	
    28	        }
    29	
    30	        if (connector.getXpoweredBy()) {
    31	            response.addHeader("X-Powered-By", POWERED_BY);
    32	        }
    33	
    34	        boolean comet = false;
    35	        boolean async = false;
    36	
    37	        try {
    38	
    39	            // Parse and set Catalina and configuration specific
    40	            // request parameters
    41	            req.getRequestProcessor().setWorkerThreadName(Thread.currentThread().getName());
    42	            boolean postParseSuccess = postParseRequest(req, request, res, response);
    43	            if (postParseSuccess) {
    44	                //check valves if we support async
    45	                request.setAsyncSupported(connector.getService().getContainer().getPipeline().isAsyncSupported());
    46	                // Calling the container
    47	                connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
    48	
    49	                if (request.isComet()) {
    50	                    if (!response.isClosed() && !response.isError()) {
    51	                        if (request.getAvailable() || (request.getContentLength() > 0 && (!request.isParametersParsed()))) {
    52	                            // Invoke a read event right away if there are available bytes
    53	                            if (event(req, res, SocketStatus.OPEN)) {
    54	                                comet = true;
    55	                                res.action(ActionCode.COMET_BEGIN, null);
    56	                            }
    57	                        } else {
    58	                            comet = true;
    59	                            res.action(ActionCode.COMET_BEGIN, null);
    60	                        }
    61	                    } else {
    62	                        // Clear the filter chain, as otherwise it will not be reset elsewhere
    63	                        // since this is a Comet request
    64	                        request.setFilterChain(null);
    65	                    }
    66	                }
    67	
    68	            }
    69	            AsyncContextImpl asyncConImpl = (AsyncContextImpl)request.getAsyncContext();
    70	            if (asyncConImpl != null) {
    71	                async = true;
    72	            } else if (!comet) {
    73	                request.finishRequest();
    74	                response.finishResponse();
    75	                if (postParseSuccess &&
    76	                        request.getMappingData().context != null) {
    77	                    // Log only if processing was invoked.
    78	                    // If postParseRequest() failed, it has already logged it.
    79	                    // If context is null this was the start of a comet request
    80	                    // that failed and has already been logged.
    81	                    ((Context) request.getMappingData().context).logAccess(
    82	                            request, response,
    83	                            System.currentTimeMillis() - req.getStartTime(),
    84	                            false);
    85	                }
    86	                req.action(ActionCode.POST_REQUEST , null);
    87	            }
    88	
    89	        } catch (IOException e) {
    90	            // Ignore
    91	        } finally {
    92	            req.getRequestProcessor().setWorkerThreadName(null);
    93	            // Recycle the wrapper request and response
    94	            if (!comet && !async) {
    95	                request.recycle();
    96	                response.recycle();
    97	            } else {
    98	                // Clear converters so that the minimum amount of memory
    99	                // is used by this processor
   100	                request.clearEncoders();
   101	                response.clearEncoders();
   102	            }
   103	        }
   104	
   105	    }
複製程式碼

之前主要分析了第 42 行的程式碼,通過 postParseRequest 方法的呼叫請求物件內儲存了關於本次請求的具體要執行的 Host、Context、Wrapper 元件的引用。

看下第 47 行:

                connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
複製程式碼

雖然只有一行,但呼叫了一堆方法,這裡對這些方法逐個分析一下:

connector.getService() 獲取的是當前 connector 關聯的 Service 元件,預設情況下獲得的就是org.apache.catalina.core.StandardService的物件。其 getContainer 方法獲得的是org.apache.catalina.core.StandardEngine的物件,這段的由來在前面講 Digester 的解析文章時,createStartDigester 方法中的這段程式碼:

        digester.addRuleSet(new EngineRuleSet("Server/Service/"));
複製程式碼

在 EngineRuleSet 類的 addRuleInstances 方法中的這一段程式碼:

    public void addRuleInstances(Digester digester) {
        
        digester.addObjectCreate(prefix + "Engine",
                                 "org.apache.catalina.core.StandardEngine",
                                 "className");
        digester.addSetProperties(prefix + "Engine");
        digester.addRule(prefix + "Engine",
                         new LifecycleListenerRule
                         ("org.apache.catalina.startup.EngineConfig",
                          "engineConfigClass"));
        digester.addSetNext(prefix + "Engine",
                            "setContainer",
                            "org.apache.catalina.Container");
複製程式碼

結合上一段程式碼可以看出 Tomcat 啟動時,如果碰到 server.xml 裡的 Server/Service/Engine 節點,先例項化一個org.apache.catalina.core.StandardEngine物件,在第 11 到 13 行,會以 StandardEngine 物件為入參呼叫org.apache.catalina.core.StandardService的 setContainer 方法。

所以上面 connector.getService().getContainer() 方法得到的實際上是 StandardEngine 物件。緊接著的 getPipeline 方法返回的是 StandardEngine 類的父類org.apache.catalina.core.ContainerBase類的成員變數 pipeline ,看下該類中這個變數的宣告程式碼:

    /**
     * The Pipeline object with which this Container is associated.
     */
    protected Pipeline pipeline = new StandardPipeline(this);
複製程式碼

所以 connector.getService().getContainer().getPipeline() 方法返回的是org.apache.catalina.core.StandardPipeline類的物件,該物件就是本部分開頭部分提到的管道( Pipeline )。

下面講一下 Tomcat 7 中的管道和閥的概念和實現:

所有的管道類都會實現org.apache.catalina.Pipeline這個介面,看下這個介面中定義的方法:

Tomcat 7 的一次請求分析(四)Tomcat 7 閥機制原理
Tomat 7 中一個管道包含多個閥( Valve ),這些閥共分為兩類,一類叫基礎閥(通過 getBasic、setBasic 方法呼叫),一類是普通閥(通過 addValve、removeValve 呼叫)。管道都是包含在一個容器當中,所以 API 裡還有 getContainer 和 setContainer 方法。一個管道一般有一個基礎閥(通過 setBasic 新增),可以有 0 到多個普通閥(通過 addValve 新增)。

所有的閥類都會實現org.apache.catalina.Valve這個介面,看下這個介面中定義的方法:

Tomcat 7 的一次請求分析(四)Tomcat 7 閥機制原理
重點關注 setNext、getNext、invoke 這三個方法,通過setNext設定該閥的下一閥,通過 getNext 返回該閥的下一個閥的引用,invoke 方法則執行該閥內部自定義的請求處理程式碼。

Tomcat 7 裡 Pipeline 的預設實現類是org.apache.catalina.core.StandardPipeline,其內部有三個成員變數:basic、first、container 。

    /**
     * The basic Valve (if any) associated with this Pipeline.
     */
    protected Valve basic = null;

    /**
     * The Container with which this Pipeline is associated.
     */
    protected Container container = null;

    /**
     * The first valve associated with this Pipeline.
     */
    protected Valve first = null;
複製程式碼

看下該類的 addValve 方法:

     1	    public void addValve(Valve valve) {
     2	    
     3	        // Validate that we can add this Valve
     4	        if (valve instanceof Contained)
     5	            ((Contained) valve).setContainer(this.container);
     6	
     7	        // Start the new component if necessary
     8	        if (getState().isAvailable()) {
     9	            if (valve instanceof Lifecycle) {
    10	                try {
    11	                    ((Lifecycle) valve).start();
    12	                } catch (LifecycleException e) {
    13	                    log.error("StandardPipeline.addValve: start: ", e);
    14	                }
    15	            }
    16	        }
    17	
    18	        // Add this Valve to the set associated with this Pipeline
    19	        if (first == null) {
    20	            first = valve;
    21	            valve.setNext(basic);
    22	        } else {
    23	            Valve current = first;
    24	            while (current != null) {
    25	                if (current.getNext() == basic) {
    26	                    current.setNext(valve);
    27	                    valve.setNext(basic);
    28	                    break;
    29	                }
    30	                current = current.getNext();
    31	            }
    32	        }
    33	        
    34	        container.fireContainerEvent(Container.ADD_VALVE_EVENT, valve);
    35	    }
複製程式碼

在第 18 到 32 行,每次給管道新增一個普通閥的時候如果管道內原來沒有普通閥則將新新增的閥作為該管道的成員變數 first 的引用,如果管道內已有普通閥,則把新加的閥加到所有普通閥鏈條末端,並且將該閥的下一個閥的引用設定為管道的基礎閥。這樣管道內的閥結構如下圖所示:

Tomcat 7 的一次請求分析(四)Tomcat 7 閥機制原理
即 Pipeline 內部維護 first 和 basic 兩個閥,其它相關閥通過 getNext 來獲取。

看下 getFirst 方法的實現:

    public Valve getFirst() {
        if (first != null) {
            return first;
        }
        
        return basic;
    }
複製程式碼

如果管道中有普通閥則返回普通閥鏈條最開始的那個,否則就返回基礎閥。

在 Tomcat 7 中所有作為普通閥的類的 invoke 方法實現中都會有這段程式碼:

getNext().invoke(request, response);
複製程式碼

通過這種機制來保證呼叫管道最開頭一端的閥的 invoke 方法,最終會執行完該管道相關的所有閥的 invoke 方法,並且最後執行的必定是該管道基礎閥的 invoke 方法。

再回到connector.getService().getContainer().getPipeline().getFirst().invoke(request, response)這段程式碼的解釋,這裡將會執行 StandardEngine 類的管道中的所有閥(包括普通閥和基礎閥)的 invoke 方法,並且最後會執行基礎閥的 invoke 方法。

Tomcat 7 在預設情況下 Engine 節點沒有普通閥,如果想要新增普通閥的話,可以通過在 server.xml 檔案的 engine 節點下新增 Valve 節點,參加該檔案中的普通閥配置的示例:

<Valve className="org.apache.catalina.authenticator.SingleSignOn" />
複製程式碼

那麼就來看看 StandardEngine 類的管道中的基礎閥的程式碼實現。先看下該基礎閥設定的程式碼,在org.apache.catalina.core.StandardEngine物件的建構函式中:

     1	    public StandardEngine() {
     2	
     3	        super();
     4	        pipeline.setBasic(new StandardEngineValve());
     5	        /* Set the jmvRoute using the system property jvmRoute */
     6	        try {
     7	            setJvmRoute(System.getProperty("jvmRoute"));
     8	        } catch(Exception ex) {
     9	            log.warn(sm.getString("standardEngine.jvmRouteFail"));
    10	        }
    11	        // By default, the engine will hold the reloading thread
    12	        backgroundProcessorDelay = 10;
    13	
    14	    }
複製程式碼

第 4 行即設定基礎閥。所以connector.getService().getContainer().getPipeline().getFirst().invoke(request, response)會執行到 org.apache.catalina.core.StandardEngineValve 類的 invoke 方法:

     1	    public final void invoke(Request request, Response response)
     2	        throws IOException, ServletException {
     3	
     4	        // Select the Host to be used for this Request
     5	        Host host = request.getHost();
     6	        if (host == null) {
     7	            response.sendError
     8	                (HttpServletResponse.SC_BAD_REQUEST,
     9	                 sm.getString("standardEngine.noHost", 
    10	                              request.getServerName()));
    11	            return;
    12	        }
    13	        if (request.isAsyncSupported()) {
    14	            request.setAsyncSupported(host.getPipeline().isAsyncSupported());
    15	        }
    16	
    17	        // Ask this Host to process this request
    18	        host.getPipeline().getFirst().invoke(request, response);
    19	
    20	    }
複製程式碼

第 5 行,從請求物件中取出該請求關聯的 Host(預設情況下是org.apache.catalina.core.StandardHost物件),請求是如何找到關聯的 Host 的請本文之前的部分。經過上述程式碼分析應該可以看出第 18 行會執行 StandardHost 物件的管道內所有的閥的 invoke 方法。

看下 StandardHost 的構造方法的實現:

    public StandardHost() {

        super();
        pipeline.setBasic(new StandardHostValve());

    }
複製程式碼

所以看下org.apache.catalina.core.StandardHostValve類的 invoke 方法:


     1	    public final void invoke(Request request, Response response)
     2	        throws IOException, ServletException {
     3	
     4	        // Select the Context to be used for this Request
     5	        Context context = request.getContext();
     6	        if (context == null) {
     7	            response.sendError
     8	                (HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
     9	                 sm.getString("standardHost.noContext"));
    10	            return;
    11	        }
    12	
    13	        // Bind the context CL to the current thread
    14	        if( context.getLoader() != null ) {
    15	            // Not started - it should check for availability first
    16	            // This should eventually move to Engine, it's generic.
    17	            if (Globals.IS_SECURITY_ENABLED) {
    18	                PrivilegedAction pa = new PrivilegedSetTccl(
    19	                        context.getLoader().getClassLoader());
    20	                AccessController.doPrivileged(pa);                
    21	            } else {
    22	                Thread.currentThread().setContextClassLoader
    23	                        (context.getLoader().getClassLoader());
    24	            }
    25	        }
    26	        if (request.isAsyncSupported()) {
    27	            request.setAsyncSupported(context.getPipeline().isAsyncSupported());
    28	        }
    29	
    30	        // Don't fire listeners during async processing
    31	        // If a request init listener throws an exception, the request is
    32	        // aborted
    33	        boolean asyncAtStart = request.isAsync(); 
    34	        // An async error page may dispatch to another resource. This flag helps
    35	        // ensure an infinite error handling loop is not entered
    36	        boolean errorAtStart = response.isError();
    37	        if (asyncAtStart || context.fireRequestInitEvent(request)) {
    38	
    39	            // Ask this Context to process this request
    40	            try {
    41	                context.getPipeline().getFirst().invoke(request, response);
    42	            } catch (Throwable t) {
    43	                ExceptionUtils.handleThrowable(t);
    44	                if (errorAtStart) {
    45	                    container.getLogger().error("Exception Processing " +
    46	                            request.getRequestURI(), t);
    47	                } else {
    48	                    request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t);
    49	                    throwable(request, response, t);
    50	                }
    51	            }
    52	    
    53	            // If the request was async at the start and an error occurred then
    54	            // the async error handling will kick-in and that will fire the
    55	            // request destroyed event *after* the error handling has taken
    56	            // place
    57	            if (!(request.isAsync() || (asyncAtStart &&
    58	                    request.getAttribute(
    59	                            RequestDispatcher.ERROR_EXCEPTION) != null))) {
    60	                // Protect against NPEs if context was destroyed during a
    61	                // long running request.
    62	                if (context.getState().isAvailable()) {
    63	                    if (!errorAtStart) {
    64	                        // Error page processing
    65	                        response.setSuspended(false);
    66	    
    67	                        Throwable t = (Throwable) request.getAttribute(
    68	                                RequestDispatcher.ERROR_EXCEPTION);
    69	    
    70	                        if (t != null) {
    71	                            throwable(request, response, t);
    72	                        } else {
    73	                            status(request, response);
    74	                        }
    75	                    }
    76	    
    77	                    context.fireRequestDestroyEvent(request);
    78	                }
    79	            }
    80	        }
    81	
    82	        // Access a session (if present) to update last accessed time, based on a
    83	        // strict interpretation of the specification
    84	        if (ACCESS_SESSION) {
    85	            request.getSession(false);
    86	        }
    87	
    88	        // Restore the context classloader
    89	        if (Globals.IS_SECURITY_ENABLED) {
    90	            PrivilegedAction pa = new PrivilegedSetTccl(
    91	                    StandardHostValve.class.getClassLoader());
    92	            AccessController.doPrivileged(pa);                
    93	        } else {
    94	            Thread.currentThread().setContextClassLoader
    95	                    (StandardHostValve.class.getClassLoader());
    96	        }
    97	    }
複製程式碼

第 41 行,會呼叫該請求相關的 Context 的管道內所有的閥的 invoke 方法,預設情況下 Context 是org.apache.catalina.core.StandardContext類的物件,其構造方法中設定了管道的基礎閥:

    public StandardContext() {

        super();
        pipeline.setBasic(new StandardContextValve());
        broadcaster = new NotificationBroadcasterSupport();
        // Set defaults
        if (!Globals.STRICT_SERVLET_COMPLIANCE) {
            // Strict servlet compliance requires all extension mapped servlets
            // to be checked against welcome files
            resourceOnlyServlets.add("jsp");
        }
    }
複製程式碼

看下其基礎閥的 invoke 方法程式碼:

     1	    public final void invoke(Request request, Response response)
     2	        throws IOException, ServletException {
     3	
     4	        // Disallow any direct access to resources under WEB-INF or META-INF
     5	        MessageBytes requestPathMB = request.getRequestPathMB();
     6	        if ((requestPathMB.startsWithIgnoreCase("/META-INF/", 0))
     7	                || (requestPathMB.equalsIgnoreCase("/META-INF"))
     8	                || (requestPathMB.startsWithIgnoreCase("/WEB-INF/", 0))
     9	                || (requestPathMB.equalsIgnoreCase("/WEB-INF"))) {
    10	            response.sendError(HttpServletResponse.SC_NOT_FOUND);
    11	            return;
    12	        }
    13	
    14	        // Select the Wrapper to be used for this Request
    15	        Wrapper wrapper = request.getWrapper();
    16	        if (wrapper == null || wrapper.isUnavailable()) {
    17	            response.sendError(HttpServletResponse.SC_NOT_FOUND);
    18	            return;
    19	        }
    20	
    21	        // Acknowledge the request
    22	        try {
    23	            response.sendAcknowledgement();
    24	        } catch (IOException ioe) {
    25	            container.getLogger().error(sm.getString(
    26	                    "standardContextValve.acknowledgeException"), ioe);
    27	            request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, ioe);
    28	            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
    29	            return;
    30	        }
    31	        
    32	        if (request.isAsyncSupported()) {
    33	            request.setAsyncSupported(wrapper.getPipeline().isAsyncSupported());
    34	        }
    35	        wrapper.getPipeline().getFirst().invoke(request, response);
    36	    }
複製程式碼

最後的第 35 行,從請求中取出關聯的 wrapper 物件後呼叫其管道內所有閥的 invoke 方法。wrapper 物件預設是org.apache.catalina.core.StandardWrapper類的例項,同樣是在該類的構造方法中設定的基礎閥:

    public StandardWrapper() {

        super();
        swValve=new StandardWrapperValve();
        pipeline.setBasic(swValve);
        broadcaster = new NotificationBroadcasterSupport();

    }
複製程式碼

有興趣可以看下基礎閥org.apache.catalina.core.StandardWrapperValve的 invoke 方法,在這裡最終會呼叫請求的 url 所匹配的 Servlet 相關過濾器( filter )的 doFilter 方法及該 Servlet 的 service 方法(這段實現都是在過濾器鏈 ApplicationFilterChain 類的 doFilter 方法中),這裡不再貼出程式碼分析。

這裡可以看出容器內的 Engine、Host、Context、Wrapper 容器元件的實現的共通點:

  1. 這些元件內部都有一個成員變數 pipeline ,因為它們都是從org.apache.catalina.core.ContainerBase類繼承來的,pipeline 就定義在這個類中。所以每一個容器內部都關聯了一個管道。

  2. 都是在類的構造方法中設定管道內的基礎閥。

  3. 所有的基礎閥的實現最後都會呼叫其下一級容器(直接從請求中獲取下一級容器物件的引用,在上面的分析中已經設定了與該請求相關的各級具體元件的引用)的 getPipeline().getFirst().invoke() 方法,直到 Wrapper 元件。因為 Wrapper 是對一個 Servlet 的包裝,所以它的基礎閥內部呼叫的過濾器鏈的 doFilter 方法和 Servlet 的 service 方法。

正是通過這種管道和閥的機制及上述的 3 點前提,使得請求可以從聯結器內一步一步流轉到具體 Servlet 的 service 方法中。這樣,關於Tomcat 7 中一次請求的分析介紹完畢,從中可以看出在瀏覽器發出一次 Socket 連線請求之後 Tomcat 容器內運轉處理的大致流程。

相關文章