帶你梳理Jetty自定義ProxyServlet實現反向代理服務

華為雲開發者社群發表於2021-08-24
摘要:最近要做一個將K8s中的某元件UI通過反向代理對映到自定義規則的連結地址上,提供給使用者訪問的需求。所以順便研究了一下Jetty的ProxyServlet。

本文分享自華為雲社群《Jetty自定義ProxyServlet實現反向代理服務(含原始碼分析)》,作者: 小焱 。

一、背景概述

最近要做一個將K8s中的某元件UI通過反向代理對映到自定義規則的連結地址上,提供給使用者訪問的需求。所以順便研究了一下Jetty的ProxyServlet。在這裡做一下分析,如果有理解不到位的還希望可以補充指正。

二、Jetty 的基本架構

Jetty 是一個Servlet 引擎,它的架構比較簡單,也是一個可擴充套件性和非常靈活的應用伺服器,它有一個基本資料模型,這個資料模型就是 Handler,所有可以被擴充套件的元件都可以作為一個 Handler,新增到 Server 中,Jetty 就是幫你管理這些 Handler。

整個 Jetty 的核心元件由 Server 和 Connector 兩個元件構成,整個 Server 元件是基於 Handler 容器工作的,Jetty 中另外一個比不可少的元件是 Connector,它負責接受客戶端的連線請求,並將請求分配給一個處理佇列去執行。Jetty 中還有一些可有可無的元件,我們可以在它上做擴充套件。如 JMX,我們可以定義一些 Mbean 把它加到 Server 中,當 Server 啟動的時候,這些 Bean 就會一起工作。

帶你梳理Jetty自定義ProxyServlet實現反向代理服務

整個 Jetty 的核心是圍繞著 Server 類來構建,Server 類繼承了 Handler,關聯了 Connector 和 Container。Container 是管理 Mbean 的容器。Jetty 的 Server 的擴充套件主要是實現一個個 Handler 並將 Handler 加到 Server 中,Server 中提供了呼叫這些 Handler 的訪問規則。整個 Jetty 的所有元件的生命週期管理是基於觀察者模板設計,實現LifeCycle。

帶你梳理Jetty自定義ProxyServlet實現反向代理服務帶你梳理Jetty自定義ProxyServlet實現反向代理服務

三、Handler 的體系結構

Jetty 主要是基於 Handler 來設計的,Handler 的體系結構影響著整個 Jetty 的方方面面。下面總結了一下 Handler 的種類及作用:

帶你梳理Jetty自定義ProxyServlet實現反向代理服務

Jetty 主要提供了兩種 Handler 型別,一種是 HandlerWrapper,它可以將一個 Handler 委託給另外一個類去執行,如我們要將一個 Handler 加到 Jetty 中,那麼就必須將這個 Handler 委託給 Server 去呼叫。配合 ScopeHandler 類我們可以攔截 Handler 的執行,在呼叫 Handler 之前或之後,可以做一些另外的事情,類似於 Tomcat 中的 Valve;另外一個 Handler 型別是 HandlerCollection,這個 Handler 類可以將多個 Handler 組裝在一起,構成一個 Handler 鏈,方便我們做擴充套件。

四、編碼設計

這裡我提供一個設計框架,具體內容可以根據需要自定義。

public class RestApi {
    private static final Logging LOGGER = Logging.getLogging(RestApi.class.getName());
​
    private Server server;
​
    /**
     * 啟動方法,需要在程式啟動時呼叫該方法
     */
    public void start() {
        try {
            ContextHandlerCollection collection = new ContextHandlerCollection();
            WebAppContext appContext = new WebAppContext();
            appContext.setContextPath("/");
            // 設定資原始檔地址,可略
            appContext.setResourceBase("/opt/appHome/myDemo/webapp");
            // 設定web.xml,可在裡面進行一些Servlet配置,可略
            appContext.setDescriptor("/opt/appHome/myDemo/webapp/WEB-INF/web.xml"); 
            appContext.setParentLoaderPriority(true);
            collection.addHandler(appContext);
            addProxyHandler(collection);
​
            server = new Server(8080);
            server.setHandler(collection);
            server.start();
        } catch (Throwable t) {
            LOGGER.error("Start RESTful API server failed", t);
        }
    }
 
    private static void addProxyHandler(ContextHandlerCollection collection) {
        ProxyServlet proxyServlet = new WebProxyServlet();      // 新增自定義ProxyServlet
​
        ServletHolder holder = new ServletHolder(proxyServlet);
​
        holder.setInitParameter("idleTimeout", 120000);         // 設定空閒釋放時間
        holder.setInitParameter("timeout", 300000);             // 設定超時時間
        holder.setInitParameter("maxConnections", 256);         // 設定最大連線數
​
        ServletContextHandler contextHandler = new ServletContextHandler();
        contextHandler.addServlet(holder, "/proxy/*");
        contextHandler.setContextPath("/demo");
        collection.addHandler(contextHandler);
    }
}

自定義 ProxyServlet,在此列出一部分常用的重寫方法,還有很多方法可以查詢文件自行重寫

public class WebProxyServlet extends ProxyServlet {
    private static final Logging LOGGING = Logging.getLogging(WebProxyServlet.class);
​
    /**
     * 自定義目標地址重寫方法
     */
    @Override
    protected String rewriteTarget(HttpServletRequest request) { }      
 
    /**
     * 自定義重寫錯誤處理方法
     */
    @Override
    protected void onProxyRewriteFailed(HttpServletRequest clientRequest, HttpServletResponse clientResponse) { }
 
    /**
     * 自定義response錯誤處理方法
     */
    @Override
    protected void onProxyResponseFailure(
        HttpServletRequest clientRequest,
        HttpServletResponse proxyResponse,
        Response serverResponse,
        Throwable failure) { }
 
    /**
     * 自定義response頭filter
     */
    @Override
    protected String filterServerResponseHeader(
        HttpServletRequest clientRequest,
        Response serverResponse,
        String headerName,
        String headerValue) { }
 
    /**
     * 自定義頭XForwarded設定
     */
    @Override
    protected void addXForwardedHeaders(HttpServletRequest clientRequest, Request proxyRequest) { }
}

五、請求處理過程

下面通過跟蹤原始碼初步梳理了一下,從 request 請求進入到返回 response 的整個流程

帶你梳理Jetty自定義ProxyServlet實現反向代理服務

六、原始碼分析

1、Request 轉發部分

當 Jetty 接收到一個請求時,Jetty 就把這個請求交給在 Server 中註冊的 ContextHandlerCollection 去執行,檢視 Service 的 handle 方法原始碼

 public void handle(HttpChannel channel) throws IOException, ServletException {
        String target = channel.getRequest().getPathInfo();
        Request request = channel.getRequest();
        Response response = channel.getResponse();
        if (LOG.isDebugEnabled()) {
            LOG.debug("{} {} {} on {}", new Object[]{request.getDispatcherType(), request.getMethod(), target, channel});
        }
​
        if (!HttpMethod.OPTIONS.is(request.getMethod()) && !"*".equals(target)) {
            this.handle(target, request, request, response);
        } else if (!HttpMethod.OPTIONS.is(request.getMethod())) {
            request.setHandled(true);
            response.sendError(400);
        } else {
            this.handleOptions(request, response);
            if (!request.isHandled()) {
                this.handle(target, request, request, response);
            }
        }
​
        if (LOG.isDebugEnabled()) {
            LOG.debug("handled={} async={} committed={} on {}", new Object[]{request.isHandled(), request.isAsyncStarted(), 
                                                                             response.isCommitted(), channel});
        }
    }

這裡呼叫的 this.handle(target, request, request, response) 方法其實是父類 HandlerWrapper 的 handle 方法

 public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) 
        throws IOException, ServletException {
        Handler handler = this._handler;
        if (handler != null) {
            handler.handle(target, baseRequest, request, response);
        }
    }

建立 server 時曾呼叫過 server.setHandler(collection) ,所以這裡就呼叫到了 ContextHandlerCollection 的 handle 方法

  public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) 
        throws IOException, ServletException {
        ContextHandlerCollection.Mapping mapping = (ContextHandlerCollection.Mapping)this._handlers.get();
        if (mapping != null) {
            Handler[] handlers = mapping.getHandlers();
            if (handlers != null && handlers.length != 0) {
                if (handlers.length == 1) {
                    handlers[0].handle(target, baseRequest, request, response);
                } else {
                    HttpChannelState async = baseRequest.getHttpChannelState();
                    if (async.isAsync()) {
                        ContextHandler context = async.getContextHandler();
                        if (context != null) {
                            Handler branch = (Handler)mapping._contextBranches.get(context);
                            if (branch == null) {
                                context.handle(target, baseRequest, request, response);
                            } else {
                                branch.handle(target, baseRequest, request, response);
                            }
​
                            return;
                        }
                    }
​
                    int limit;
                    if (target.startsWith("/")) {
                        Trie<Entry<String, ContextHandlerCollection.Branch[]>> pathBranches = mapping._pathBranches;
                        if (pathBranches == null) {
                            return;
                        }
​
                        int l;
                        for(limit = target.length() - 1; limit >= 0; limit = l - 2) {
                            Entry<String, ContextHandlerCollection.Branch[]> branches = 
                                (Entry)pathBranches.getBest(target, 1, limit);
                            if (branches == null) {
                                break;
                            }
​
                            l = ((String)branches.getKey()).length();
                            if (l == 1 || target.length() == l || target.charAt(l) == '/') {
                                ContextHandlerCollection.Branch[] var12 = 
                                    (ContextHandlerCollection.Branch[])branches.getValue();
                                int var13 = var12.length;
​
                                for(int var14 = 0; var14 < var13; ++var14) {
                                    ContextHandlerCollection.Branch branch = var12[var14];
                                    branch.getHandler().handle(target, baseRequest, request, response);
                                    if (baseRequest.isHandled()) {
                                        return;
                                    }
                                }
                            }
                        }
                    } else {
                        Handler[] var17 = handlers;
                        limit = handlers.length;
​
                        for(int var19 = 0; var19 < limit; ++var19) {
                            Handler handler = var17[var19];
                            handler.handle(target, baseRequest, request, response);
                            if (baseRequest.isHandled()) {
                                return;
                            }
                        }
                    }
                }
            }
        }
    }

從上面的原始碼可以看出 ContextHandlerCollection 的 handle 方法繼續呼叫了 collection.addHandler 設定進來 ServletContextHandler 的 handle 方法,通過跟蹤,可以找到其實這裡呼叫了父類 ScopedHandler 的 handle --> doScope --> nextScope

  public final void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) 
        throws IOException, ServletException {
        if (this.isStarted()) {
            if (this._outerScope == null) {
                this.doScope(target, baseRequest, request, response);
            } else {
                this.doHandle(target, baseRequest, request, response);
            }
        }
    }
​
    public void doScope(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) 
        throws IOException, ServletException {
        this.nextScope(target, baseRequest, request, response);
    }
​
    public final void nextScope(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) 
        throws IOException, ServletException {
        if (this._nextScope != null) {
            this._nextScope.doScope(target, baseRequest, request, response);
        } else if (this._outerScope != null) {
            this._outerScope.doHandle(target, baseRequest, request, response);
        } else {
            this.doHandle(target, baseRequest, request, response);
        }
    }

檢視 ServletContextHandler 可以找到主要註冊了以下三個 handler,均為 ScopedHandler 的子類,也就是 nextScope 方法中的 this._nextScope

protected SessionHandler _sessionHandler;
protected SecurityHandler _securityHandler;
protected ServletHandler _servletHandler;

SessionHandler 是對 ServletHandler 進行了一層包裝(裝飾器模式),用於一些session的預處理什麼的,而SecurityHandler從名字分析是做一些安全相關的,這兩個具體就不分析了,直接來看 ServletHandler 的 doScope 方法

   public void doScope(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) 
        throws IOException, ServletException {
        String old_servlet_path = baseRequest.getServletPath();
        String old_path_info = baseRequest.getPathInfo();
        DispatcherType type = baseRequest.getDispatcherType();
        ServletHolder servletHolder = null;
        Scope oldScope = null;
        MappedResource<ServletHolder> mapping = this.getMappedServlet(target);
        if (mapping != null) {
            servletHolder = (ServletHolder)mapping.getResource();
            if (mapping.getPathSpec() != null) {
                PathSpec pathSpec = mapping.getPathSpec();
                String servletPath = pathSpec.getPathMatch(target);
                String pathInfo = pathSpec.getPathInfo(target);
                if (DispatcherType.INCLUDE.equals(type)) {
                    baseRequest.setAttribute("javax.servlet.include.servlet_path", servletPath);
                    baseRequest.setAttribute("javax.servlet.include.path_info", pathInfo);
                } else {
                    baseRequest.setServletPath(servletPath);
                    baseRequest.setPathInfo(pathInfo);
                }
            }
        }
​
        if (LOG.isDebugEnabled()) {
            LOG.debug("servlet {}|{}|{} -> {}", 
                      new Object[]{baseRequest.getContextPath(), 
                                   baseRequest.getServletPath(), 
                                   baseRequest.getPathInfo(), 
                                   servletHolder});
        }
​
        try {
            oldScope = baseRequest.getUserIdentityScope();
            baseRequest.setUserIdentityScope(servletHolder);
            this.nextScope(target, baseRequest, request, response);
        } finally {
            if (oldScope != null) {
                baseRequest.setUserIdentityScope(oldScope);
            }
​
            if (!DispatcherType.INCLUDE.equals(type)) {
                baseRequest.setServletPath(old_servlet_path);
                baseRequest.setPathInfo(old_path_info);
            }
        }
    }

這裡對 baseRequest 做了一些設定,將註冊進來的 ServletHolder set 進了 baseRequest,之後又繼續呼叫了 this.nextScope(target, baseRequest, request, response) ,根據上面的 nextScope 方法,所有 scope 執行完,則執行 doHandle 方法,繼續跳過 SessionHandler 和 SecurityHandler,來看下ServletHandler 的 doHandle 方法

 public void doHandle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) 
        throws IOException, ServletException {
        ServletHolder servletHolder = (ServletHolder)baseRequest.getUserIdentityScope();
        FilterChain chain = null;
        if (servletHolder != null && this._filterMappings != null && this._filterMappings.length > 0) {
            chain = this.getFilterChain(baseRequest, target.startsWith("/") ? target : null, servletHolder);
        }
​
        if (LOG.isDebugEnabled()) {
            LOG.debug("chain={}", new Object[]{chain});
        }
​
        try {
            if (servletHolder == null) {
                this.notFound(baseRequest, request, response);
            } else {
                ServletRequest req = request;
                if (request instanceof ServletRequestHttpWrapper) {
                    req = ((ServletRequestHttpWrapper)request).getRequest();
                }
​
                ServletResponse res = response;
                if (response instanceof ServletResponseHttpWrapper) {
                    res = ((ServletResponseHttpWrapper)response).getResponse();
                }
​
                servletHolder.prepare(baseRequest, (ServletRequest)req, (ServletResponse)res);
                if (chain != null) {
                    chain.doFilter((ServletRequest)req, (ServletResponse)res);
                } else {
                    servletHolder.handle(baseRequest, (ServletRequest)req, (ServletResponse)res);
                }
            }
        } finally {
            if (servletHolder != null) {
                baseRequest.setHandled(true);
            }
        }
    }

doHandle 主要是取出註冊的 FilterChain ServletHolder,如果存在 Filter,先執行 chain.doFilter 方法,否則執行 servletHolder.handle 我沒有設定 filter 所有就直接看 ServletHolder 的 handle 方法了

 public void handle(Request baseRequest, ServletRequest request, ServletResponse response) 
        throws ServletException, UnavailableException, IOException {
        try {
            Servlet servlet = this.getServletInstance();
            if (servlet == null) {
                throw new UnavailableException("Servlet Not Initialized");
            }
​
            servlet.service(request, response);
        } catch (UnavailableException var5) {
            this.makeUnavailable(var5).service(request, response);
        }
    }

這裡呼叫了 ServletHolder 中 Servlet 的 service 方法,也就是走到了我們自定義類 WebProxyServlet 類,因為沒有重寫,所以這裡呼叫的是 ProxyServlet 的 service 方法

 protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        int requestId = this.getRequestId(request);
        String rewrittenTarget = this.rewriteTarget(request);
        if (this._log.isDebugEnabled()) {
            StringBuffer uri = request.getRequestURL();
            if (request.getQueryString() != null) {
                uri.append("?").append(request.getQueryString());
            }
​
            if (this._log.isDebugEnabled()) {
                this._log.debug("{} rewriting: {} -> {}", new Object[]{requestId, uri, rewrittenTarget});
            }
        }
​
        if (rewrittenTarget == null) {
            this.onProxyRewriteFailed(request, response);
        } else {
            Request proxyRequest = this.newProxyRequest(request, rewrittenTarget);
            this.copyRequestHeaders(request, proxyRequest);
            this.addProxyHeaders(request, proxyRequest);
            AsyncContext asyncContext = request.startAsync();
            asyncContext.setTimeout(0L);
            proxyRequest.timeout(this.getTimeout(), TimeUnit.MILLISECONDS);
            if (this.hasContent(request)) {
                if (this.expects100Continue(request)) {
                    DeferredContentProvider deferred = new DeferredContentProvider(new ByteBuffer[0]);
                    proxyRequest.content(deferred);
                    proxyRequest.attribute(CONTINUE_ACTION_ATTRIBUTE, () -> {
                        try {
                            ContentProvider provider = this.proxyRequestContent(request, response, proxyRequest);
                            (new ProxyServlet.DelegatingContentProvider(request, proxyRequest, 
                                                                        response, provider, deferred)).iterate();
                        } catch (Throwable var6) {
                            this.onClientRequestFailure(request, proxyRequest, response, var6);
                        }
​
                    });
                } else {
                    proxyRequest.content(this.proxyRequestContent(request, response, proxyRequest));
                }
            }
            this.sendProxyRequest(request, response, proxyRequest);
        }
    }

至此呼叫到了我們重寫的最關鍵的方法 rewriteTarget 此方法可以自定義邏輯將 request 的地址解析,返回要代理到的目標地址,使用目標地址組成proxyRequest 最後呼叫 sendProxyRequest 實現代理轉發。

2、Response 接收部分

如果繼續跟 sendProxyRequest 會看到建立了一個 ProxyResponseListener,這裡具體就不詳細跟蹤了,主要講一下流程,有興趣的可以自行動手看一下。Response 返回會通過反射機制觸發 onHeader 方法 ProxyServlet 重寫了該方法並跳轉到了 onServerResponseHeaders 方法

  public void onHeaders(Response proxyResponse) {
        ProxyServlet.this.onServerResponseHeaders(this.request, this.response, proxyResponse);
    }

這個方法是設定 Response 的 header 內容的,其中有獲取 headerValue 呼叫了 this.filterServerResponseHeader 方法,我們也可以通過重寫此方法自定義返回體的 headerValue。

七、總結

到這裡 Jetty 的 ProxyServlet 執行原理和自定義方法大致梳理完畢。還有許多漏掉的和理解不到位的地方,希望大家可以提出指正。工作中偶爾抽出一點時間讀一下原始碼,既可以提升對所用技術的理解,又可以學習欣賞這些框架的巧妙設計,還是非常有意義的。

 

點選關注,第一時間瞭解華為雲新鮮技術~

相關文章