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

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

再來看一下 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 方法:bash

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 組件的引用。session

看下第 47 行:app

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

雖然只有一行,但調用了一堆方法,這裏對這些方法逐個分析一下:jvm

connector.getService() 獲取的是當前 connector 關聯的 Service 組件,默認狀況下得到的就是org.apache.catalina.core.StandardService的對象。其 getContainer 方法得到的是org.apache.catalina.core.StandardEngine的對象,這段的由來在前面講 Digester 的解析文章時,createStartDigester 方法中的這段代碼:jsp

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

在 EngineRuleSet 類的 addRuleInstances 方法中的這一段代碼:async

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 ,看下該類中這個變量的聲明代碼:oop

/**
     * 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這個接口,看下這個接口中定義的方法:

Tomat 7 中一個管道包含多個閥( Valve ),這些閥共分爲兩類,一類叫基礎閥(經過 getBasic、setBasic 方法調用),一類是普通閥(經過 addValve、removeValve 調用)。管道都是包含在一個容器當中,因此 API 裏還有 getContainer 和 setContainer 方法。一個管道通常有一個基礎閥(經過 setBasic 添加),能夠有 0 到多個普通閥(經過 addValve 添加)。

全部的閥類都會實現org.apache.catalina.Valve這個接口,看下這個接口中定義的方法:

重點關注 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 的引用,若是管道內已有普通閥,則把新加的閥加到全部普通閥鏈條末端,而且將該閥的下一個閥的引用設置爲管道的基礎閥。這樣管道內的閥結構以下圖所示:

即 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 容器內運轉處理的大體流程。

相關文章
相關標籤/搜索