Java Servlet(十二):Servlet、Listener、Filter之間的執行流程分析 設計模式(九)責任鏈(Chain of Responsibility)

時隔幾年後,看到本系列文章講解的內容缺少了不少內容:週末無事分析了Spring Security是如何被集成到Web Servlet(SpringMVC)時,需要重新理清Filter、Listener、Servlet(SpringMVC#DispatcherServlet)之間的執行順序,於是就有了本篇文章。這個話題是Web Servlet學習中的一個重點,弄清它們之間的執行流程,有助於理解SpringMVC、Spring Security這些框架是否如何與Web Servlet集成到一起。

測試

新建一個web servlet pom.xml項目web-servlet-01

pom.xml引入依賴:

    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>4.0.1</version>
      <scope>compile</scope>
    </dependency>

    <!-- https://mvnrepository.com/artifact/commons-logging/commons-logging -->
    <dependency>
      <groupId>commons-logging</groupId>
      <artifactId>commons-logging</artifactId>
      <version>1.2</version>
    </dependency>

項目結構:

定義HelloServlet.java

package com.dx.test.servlets;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.servlet.*;
import java.io.IOException;

public class HelloServlet implements Servlet {
    private Log log = LogFactory.getLog(HelloServlet.class);

    @Override
    public void init(ServletConfig config) throws ServletException {
        log.info("IndexServlet#init()");
    }

    @Override
    public ServletConfig getServletConfig() {
        return null;
    }

    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        log.info("IndexServlet#service()");
    }

    @Override
    public String getServletInfo() {
        return null;
    }

    @Override
    public void destroy() {
        log.info("IndexServlet#destory()");
    }
}

在HelloServlet中需要在它的init()、service()、destory()方法中書寫日誌,爲後邊測試使用。

定義MyFilter.java

package com.dx.test.filters;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.servlet.*;
import java.io.IOException;

public class MyFilter implements Filter {
    private static Log log= LogFactory.getLog(MyFilter.class);
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("MyFilter#init()");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info("MyFilter#doFilter() before");
        chain.doFilter(request, response);
        log.info("MyFilter#doFilter() after");
    }

    @Override
    public void destroy() {
        log.info("MyFilter#destroy()");
    }
}

注意,在doFilter()方法中添加日誌的方式:

1)在chain.doFilter(request,response)代碼之前添加了日誌;

2)在chain.doFilter(request,response)代碼之後添加日誌。

定義MyServletRequestListener.java

package com.dx.test.listeners;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;

public class MyServletRequestListener implements ServletRequestListener {
    private static Log log = LogFactory.getLog(MyServletRequestListener.class);

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        log.info("MyServletRequestListener#requestInitialized()");
    }

    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
        log.info("MyServletRequestListener#requestDestroyed()");
    }
}

說明:

ServletRequestListener是在每次servlet請求之前都會執行#requestInitialized(),在執行servlet會後執行#requestDestroyed()。

定義MyServletContextListener.java

package com.dx.test.listeners;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

public class MyServletContextListener implements ServletContextListener {
    private static Log log= LogFactory.getLog(MyServletContextListener.class);
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        log.info("MyServletContextListener#contextInitialized()");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        log.info("MyServletContextListener#contextDestroyed()");
    }
}

說明:

ServletContextListener是在每次servlet服務啓動執行init方法,在servlet服務關閉時,執行destory方法。

web.xml配置

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
  <display-name>Archetype Created Web Application</display-name>
  <welcome-file-list>
    <welcome-file>/index</welcome-file>
  </welcome-file-list>
  <filter>
    <filter-name>myFilter</filter-name>
    <filter-class>com.dx.test.filters.MyFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>myFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <listener>
    <listener-class>com.dx.test.listeners.MyServletContextListener</listener-class>
  </listener>

  <listener>
    <listener-class>com.dx.test.listeners.MyServletRequestListener</listener-class>
  </listener>
  
  <servlet>
    <servlet-name>hello_servlet</servlet-name>
    <servlet-class>com.dx.test.servlets.HelloServlet</servlet-class>
    <!--<load-on-startup>1</load-on-startup>-->
  </servlet>
  <servlet-mapping>
    <servlet-name>hello_servlet</servlet-name>
    <url-pattern>/hello</url-pattern>
  </servlet-mapping>
</web-app>

測試日誌:

啓動Servlet服務,輸出日誌:

// 啓動tomcat
/opt/apache-tomcat-8.5.49/bin/catalina.sh run
[2020-01-18 12:53:34,440] Artifact web-servlet-01:war: Waiting for server connection to start artifact deployment...
18-Jan-2020 12:53:42.076 信息 [main] org.apache.coyote.AbstractProtocol.init 初始化協議處理器 ["http-nio-8080"]
18-Jan-2020 12:53:42.092 信息 [main] org.apache.tomcat.util.net.NioSelectorPool.getSharedSelector Using a shared selector for servlet write/read
18-Jan-2020 12:53:42.099 信息 [main] org.apache.coyote.AbstractProtocol.init 初始化協議處理器 ["ajp-nio-8009"]
18-Jan-2020 12:53:42.100 信息 [main] org.apache.tomcat.util.net.NioSelectorPool.getSharedSelector Using a shared selector for servlet write/read
18-Jan-2020 12:53:42.100 信息 [main] org.apache.catalina.startup.Catalina.load Initialization processed in 284 ms
18-Jan-2020 12:53:42.116 信息 [main] org.apache.catalina.core.StandardService.startInternal Starting service [Catalina]
18-Jan-2020 12:53:42.116 信息 [main] org.apache.catalina.core.StandardEngine.startInternal Starting Servlet Engine: Apache Tomcat/8.5.49
18-Jan-2020 12:53:42.120 信息 [main] org.apache.coyote.AbstractProtocol.start 開始協議處理句柄["http-nio-8080"]
18-Jan-2020 12:53:42.125 信息 [main] org.apache.coyote.AbstractProtocol.start 開始協議處理句柄["ajp-nio-8009"]
18-Jan-2020 12:53:42.127 信息 [main] org.apache.catalina.startup.Catalina.start Server startup in 26 ms
Connected to server
18-Jan-2020 12:53:42.814 信息 [RMI TCP Connection(2)-127.0.0.1] com.dx.test.listeners.MyServletContextListener.contextInitialized MyServletContextListener#contextInitialized()
18-Jan-2020 12:53:42.822 信息 [RMI TCP Connection(2)-127.0.0.1] com.dx.test.filters.MyFilter.init MyFilter#init()

首次,訪問http://localhost:8080/web_servlet_01_war/hello輸出日誌:

// 第一次訪問 /hello
18-Jan-2020 12:54:10.687 信息 [http-nio-8080-exec-5] com.dx.test.listeners.MyServletRequestListener.requestInitialized MyServletRequestListener#requestInitialized()
18-Jan-2020 12:54:10.687 信息 [http-nio-8080-exec-5] com.dx.test.servlets.IndexServlet.init IndexServlet#init()
18-Jan-2020 12:54:10.688 信息 [http-nio-8080-exec-5] com.dx.test.filters.MyFilter.doFilter MyFilter#doFilter() before
18-Jan-2020 12:54:10.688 信息 [http-nio-8080-exec-5] com.dx.test.servlets.IndexServlet.service IndexServlet#service()
18-Jan-2020 12:54:10.688 信息 [http-nio-8080-exec-5] com.dx.test.filters.MyFilter.doFilter MyFilter#doFilter() after
18-Jan-2020 12:54:10.688 信息 [http-nio-8080-exec-5] com.dx.test.listeners.MyServletRequestListener.requestDestroyed MyServletRequestListener#requestDestroyed()

再次,訪問http://localhost:8080/web_servlet_01_war/hello輸出日誌:

// 第二次訪問 /hello
18-Jan-2020 12:54:29.645 信息 [http-nio-8080-exec-6] com.dx.test.listeners.MyServletRequestListener.requestInitialized MyServletRequestListener#requestInitialized()
18-Jan-2020 12:54:29.645 信息 [http-nio-8080-exec-6] com.dx.test.filters.MyFilter.doFilter MyFilter#doFilter() before
18-Jan-2020 12:54:29.645 信息 [http-nio-8080-exec-6] com.dx.test.servlets.IndexServlet.service IndexServlet#service()
18-Jan-2020 12:54:29.645 信息 [http-nio-8080-exec-6] com.dx.test.filters.MyFilter.doFilter MyFilter#doFilter() after
18-Jan-2020 12:54:29.646 信息 [http-nio-8080-exec-6] com.dx.test.listeners.MyServletRequestListener.requestDestroyed MyServletRequestListener#requestDestroyed()

關閉Servlet服務,輸出日誌:

// 關閉tomcat
/opt/apache-tomcat-8.5.49/bin/catalina.sh stop
18-Jan-2020 12:58:54.440 信息 [main] org.apache.catalina.core.StandardServer.await A valid shutdown command was received via the shutdown port. Stopping the Server instance.
18-Jan-2020 12:58:54.441 信息 [main] org.apache.coyote.AbstractProtocol.pause Pausing ProtocolHandler ["http-nio-8080"]
18-Jan-2020 12:58:54.455 信息 [main] org.apache.coyote.AbstractProtocol.pause Pausing ProtocolHandler ["ajp-nio-8009"]
18-Jan-2020 12:58:54.463 信息 [main] org.apache.catalina.core.StandardService.stopInternal 正在停止服務[Catalina]
18-Jan-2020 12:58:54.465 信息 [localhost-startStop-2] com.dx.test.servlets.IndexServlet.destroy IndexServlet#destory()
18-Jan-2020 12:58:54.465 信息 [localhost-startStop-2] com.dx.test.filters.MyFilter.destroy MyFilter#destroy()
18-Jan-2020 12:58:54.465 信息 [localhost-startStop-2] com.dx.test.listeners.MyServletContextListener.contextDestroyed MyServletContextListener#contextDestroyed()
18-Jan-2020 12:58:54.476 信息 [main] org.apache.coyote.AbstractProtocol.stop 正在停止ProtocolHandler ["http-nio-8080"]
18-Jan-2020 12:58:54.479 信息 [main] org.apache.coyote.AbstractProtocol.stop 正在停止ProtocolHandler ["ajp-nio-8009"]
18-Jan-2020 12:58:54.479 信息 [main] org.apache.coyote.AbstractProtocol.destroy 正在摧毀協議處理器 ["http-nio-8080"]
18-Jan-2020 12:58:54.480 信息 [main] org.apache.coyote.AbstractProtocol.destroy 正在摧毀協議處理器 ["ajp-nio-8009"]
Disconnected from server

結論

執行流程圖說明:

1)用戶通過在瀏覽器中輸出網址http://localhost:8080/web_servlet_01_war/hello,訪問web容器中部署的servlet服務;

2)用戶請求信息被web容器監聽到,web容器會瀏覽器請求信息進行封裝HttpServletRequest、響應信息會被封裝到HttpServletResponse,並找到對應的servlet容器;

3)請求進入servlet容器會被listener監聽到,listener分爲兩類:ServletRequestListener、ServletContextListener。

ServletRequestListener會在每次servlet請求過程中,都會執行它的#requestInitialized方法,然後交給filter去執行;

ServletContextListener會在servlet服務啓動時,調用它的#contextInitialized()方法;在servlet服務關閉時,調用它的#contextDestroyed()方法。

4)然後請求被filter執行,調用filter#doFilter() before方法,filter的初始化時刻:“Servlet服務啓動” 或 “第一次訪問初始化”,根據依據<load-on-startup>1</load-on-startup>參數;

5)請求交給servlet#service()方法;

6)servlet#service()方法執行完成後,會回到filter#doFilter() after方法;

7)執行ServletRequestListener#requestDestroyed()方法;

8)將請求響應反饋給瀏覽器;

9)當服務關閉時,會執行servlet#destory()、filter#destory()、ServletContextListener#contextDestroyed()

源碼分析

Tomcat接收到請求後,會在容器(Engine、Host、Context、Wrapper各級組件)中匹配,並且在它們的管道中流轉,最終會適配到一個StandardWrapper的基礎閥的-org.apache.catalina.core.StandardWrapperValve 的invoke方法。

下邊具體看下請求匹配到最基礎的 StandardWrapper 組件的管道中,之後是如何處理的:

StandardWrapperValve閥的#invoke方法:

final class StandardWrapperValve extends ValveBase {
    ...
    @Override
    public final void invoke(Request request, Response response)
        throws IOException, ServletException {

        // Initialize local variables we may need
        boolean unavailable = false;
        Throwable throwable = null;
        // This should be a Request attribute...
        long t1=System.currentTimeMillis();
        requestCount.incrementAndGet();
        StandardWrapper wrapper = (StandardWrapper) getContainer();
        Servlet servlet = null;
        Context context = (Context) wrapper.getParent();

        // Check for the application being marked unavailable
        if (!context.getState().isAvailable()) {
            response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, sm.getString("standardContext.isUnavailable"));
            unavailable = true;
        }

        // Check for the servlet being marked unavailable
        if (!unavailable && wrapper.isUnavailable()) {
            container.getLogger().info(sm.getString("standardWrapper.isUnavailable", wrapper.getName()));
            long available = wrapper.getAvailable();
            if ((available > 0L) && (available < Long.MAX_VALUE)) {
                response.setDateHeader("Retry-After", available);
                response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, sm.getString("standardWrapper.isUnavailable", wrapper.getName()));
            } else if (available == Long.MAX_VALUE) {
                response.sendError(HttpServletResponse.SC_NOT_FOUND, sm.getString("standardWrapper.notFound", wrapper.getName()));
            }
            unavailable = true;
        }

        // 分配一個servlet實例用來處理請求
        // Allocate a servlet instance to process this request
        try {
            if (!unavailable) {
                // 調用StandardWrapper#allocate()方法,獲取到servlet實例
            servlet = wrapper.allocate();
            }
        } catch (UnavailableException e) {
            ...
        } catch (ServletException e) {
            ...
        } catch (Throwable e) {
            ...
        }

        ...

        // 爲當前請求創建一個過濾器鏈
      // Create the filter chain for this request
        ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);

        // 爲當前請求調用過濾器鏈,注意:這也會調用servlet實例的service()方法
        // Call the filter chain for this request. NOTE: This also calls the servlet's service() method
        try {
            if ((servlet != null) && (filterChain != null)) {
                // Swallow output if needed
                if (context.getSwallowOutput()) {
                    try {
                        SystemLogHandler.startCapture();
                        if (request.isAsyncDispatching()) {
                            request.getAsyncContextInternal().doInternalDispatch();
                        } else {
                            filterChain.doFilter(request.getRequest(), response.getResponse());
                        }
                    } finally {
                        String log = SystemLogHandler.stopCapture();
                        if (log != null && log.length() > 0) {
                            context.getLogger().info(log);
                        }
                    }
                } else {
                    if (request.isAsyncDispatching()) {
                        request.getAsyncContextInternal().doInternalDispatch();
                    } else {
                        filterChain.doFilter(request.getRequest(), response.getResponse());
                    }
                }

            }
        } catch (ClientAbortException | CloseNowException e) {
            ...
        } catch (IOException e) {
            ...
        } catch (UnavailableException e) {
            ...
        } catch (ServletException e) {
            ...
        } catch (Throwable e) {
            ...
        } finally {
            // Release the filter chain (if any) for this request
            if (filterChain != null) {
                filterChain.release();
            }

            // Deallocate the allocated servlet instance
            try {
                if (servlet != null) {
                    wrapper.deallocate(servlet);
                }
            } catch (Throwable e) {
                ...
            }

            // If this servlet has been marked permanently unavailable,
            // unload it and release this instance
            try {
                if ((servlet != null) &&
                    (wrapper.getAvailable() == Long.MAX_VALUE)) {
                    wrapper.unload();
                }
            } catch (Throwable e) {
                ...
            }

            long t2=System.currentTimeMillis();
            long time=t2-t1;
            processingTime += time;
            if( time > maxTime) maxTime=time;
            if( time < minTime) minTime=time;
        }
    }
}

上邊代碼主要包含
1)servlet = wrapper.allocate(); 調用StandardWrapper#allocate()方法,獲取到servlet實例
2)ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);爲當前請求創建一個過濾鏈,(非異步情況下)並調用filterChain.doFilter(request.getRequest(), response.getResponse());
3)filter#doFilter()、servlet#service()的執行是在filterChain.doFilter(request.getRequest(), response.getResponse());代碼內部執行的。

ApplicationFilterChain#doFilter內部代碼:

public final class ApplicationFilterChain implements FilterChain {
    ...

    @Override
    public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
        if( Globals.IS_SECURITY_ENABLED ) {
            final ServletRequest req = request;
            final ServletResponse res = response;
            try {
                java.security.AccessController.doPrivileged(
                    new java.security.PrivilegedExceptionAction<Void>() {
                        @Override
                        public Void run()
                            throws ServletException, IOException {
                            internalDoFilter(req,res);
                            return null;
                        }
                    }
                );
            } catch( PrivilegedActionException pe) {
                ...
            }
        } else {
            internalDoFilter(request,response);
        }
    }

    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++];
            try {
                Filter filter = filterConfig.getFilter();

                if (request.isAsyncSupported() && "false".equalsIgnoreCase(filterConfig.getFilterDef().getAsyncSupported())) {
                    request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
                }
                if( Globals.IS_SECURITY_ENABLED ) {
                    final ServletRequest req = request;
                    final ServletResponse res = response;
                    Principal principal = ((HttpServletRequest) req).getUserPrincipal();

                    Object[] args = new Object[]{req, res, this};
                    SecurityUtil.doAsPrivilege ("doFilter", filter, classType, args, principal);
                } else {
                    filter.doFilter(request, response, this);
                }
            } catch (IOException | ServletException | RuntimeException e) {
                throw e;
            } catch (Throwable e) {
                ...
            }
            return;
        }

        // We fell off the end of the chain -- call the servlet instance
        try {
            if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
                lastServicedRequest.set(request);
                lastServicedResponse.set(response);
            }

            if (request.isAsyncSupported() && !servletSupportsAsync) {
                request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
            }
            // Use potentially wrapped request from this point
            if ((request instanceof HttpServletRequest) && (response instanceof HttpServletResponse) && Globals.IS_SECURITY_ENABLED ) {
                final ServletRequest req = request;
                final ServletResponse res = response;
                Principal principal = ((HttpServletRequest) req).getUserPrincipal();
                Object[] args = new Object[]{req, res};
                SecurityUtil.doAsPrivilege("service", servlet, classTypeUsedInService, args, principal);
            } else {
                servlet.service(request, response);
            }
        } catch (IOException | ServletException | RuntimeException e) {
            throw e;
        } catch (Throwable e) {
            e = ExceptionUtils.unwrapInvocationTargetException(e);
            ExceptionUtils.handleThrowable(e);
            throw new ServletException(sm.getString("filterChain.servlet"), e);
        } finally {
            if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
                lastServicedRequest.set(null);
                lastServicedResponse.set(null);
            }
        }
    }

    ...
}

1)上邊代碼會遞歸調用 ApplicationFilterChain#doFilter(...);
2)遞歸執行到最底層doFiler,之後會調用上邊代碼中 servlet.service() 或者 SecurityUtil.doAsPrivilege("service", servlet, classTypeUsedInService, args, principal);,也即是執行 servlet 的 service() 方法。然後一次從 底層 到 頂層 返回遞歸調用代碼出,結束調用。這也就是之職責鏈的模式的應用,具體請參考《設計模式(九)責任鏈(Chain of Responsibility)

ServletRequestListener 觸發:

org.apache.catalina.core.StandardHostValve#invoke方法:

final class StandardHostValve extends ValveBase {
    ...

    @Override
    public final void invoke(Request request, Response response)
        throws IOException, ServletException {

        // Select the Context to be used for this Request
        Context context = request.getContext();
        if (context == null) {
            return;
        }

        if (request.isAsyncSupported()) {
            request.setAsyncSupported(context.getPipeline().isAsyncSupported());
        }

        boolean asyncAtStart = request.isAsync();

        try {
            context.bind(Globals.IS_SECURITY_ENABLED, MY_CLASSLOADER);

            if (!asyncAtStart && !context.fireRequestInitEvent(request.getRequest())) {
                // Don't fire listeners during async processing (the listener fired for the request that called startAsync()).
                // If a request init listener throws an exception, the request is aborted.
                return;
            }

            // Ask this Context to process this request. Requests that are already in error must have been routed here to check for
            // application defined error pages so DO NOT forward them to the the application for processing.
            try {
                if (!response.isErrorReportRequired()) {
                    context.getPipeline().getFirst().invoke(request, response);
                }
            } catch (Throwable t) {
                ExceptionUtils.handleThrowable(t);
                container.getLogger().error("Exception Processing " + request.getRequestURI(), t);
                // If a new error occurred while trying to report a previous error allow the original error to be reported.
                if (!response.isErrorReportRequired()) {
                    request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t);
                    throwable(request, response, t);
                }
            }

            // Now that the request/response pair is back under container control lift the suspension so that the error handling can
            // complete and/or the container can flush any remaining data
            response.setSuspended(false);

            Throwable t = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);

            // Protect against NPEs if the context was destroyed during a long running request.
            if (!context.getState().isAvailable()) {
                return;
            }

            // Look for (and render if found) an application level error page
            if (response.isErrorReportRequired()) {
                // If an error has occurred that prevents further I/O, don't waste time producing an error report that will never be read
                AtomicBoolean result = new AtomicBoolean(false);
                response.getCoyoteResponse().action(ActionCode.IS_IO_ALLOWED, result);
                if (result.get()) {
                    if (t != null) {
                        throwable(request, response, t);
                    } else {
                        status(request, response);
                    }
                }
            }

            if (!request.isAsync() && !asyncAtStart) {
                context.fireRequestDestroyEvent(request.getRequest());
            }
        } finally {
            // Access a session (if present) to update last accessed time, based on a strict interpretation of the specification
            if (ACCESS_SESSION) {
                request.getSession(false);
            }

            context.unbind(Globals.IS_SECURITY_ENABLED, MY_CLASSLOADER);
        }
    }

    ...
}

說明:
1)context.fireRequestInitEvent(request.getRequest()):就是調用 StandardContext#fireRequestInitEvent(...) 觸發 ServletRequestListener#requestInitialized(event)

2)context.fireRequestDestroyEvent(request.getRequest()):就是調用 StandardContext#fireRequestDestroyEvent(...) 觸發 ServletRequestListener#requestDestroyed(event)

參考資料:

Tomcat系列源碼分析

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章