摘要:最近要做一个将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 的核心是围绕着 Server 类来构建,Server 类继承了 Handler,关联了 Connector 和 Container。Container 是管理 Mbean 的容器。Jetty 的 Server 的扩展主要是实现一个个 Handler 并将 Handler 加到 Server 中,Server 中提供了调用这些 Handler 的访问规则。整个 Jetty 的所有组件的生命周期管理是基于观察者模板设计,实现LifeCycle。

三、Handler 的体系结构

Jetty 主要是基于 Handler 来设计的,Handler 的体系结构影响着整个 Jetty 的方方面面。下面总结了一下 Handler 的种类及作用:

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 的整个流程

六、源码分析

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 运行原理和自定义方法大致梳理完毕。还有许多漏掉的和理解不到位的地方,希望大家可以提出指正。工作中偶尔抽出一点时间读一下源码,既可以提升对所用技术的理解,又可以学习欣赏这些框架的巧妙设计,还是非常有意义的。

点击关注,第一时间了解华为云新鲜技术~

带你梳理Jetty自定义ProxyServlet实现反向代理服务的更多相关文章

  1. [源码分析] 带你梳理 Flink SQL / Table API内部执行流程

    [源码分析] 带你梳理 Flink SQL / Table API内部执行流程 目录 [源码分析] 带你梳理 Flink SQL / Table API内部执行流程 0x00 摘要 0x01 Apac ...

  2. wing带你玩转自定义view系列(2) 简单模仿qq未读消息去除效果

    上一篇介绍了贝塞尔曲线的简单应用 仿360内存清理效果 这一篇带来一个  两条贝塞尔曲线的应用 : 仿qq未读消息去除效果. 转载请注明出处:http://blog.csdn.net/wingicho ...

  3. wing带你玩转自定义view系列(1) 仿360内存清理效果

    本篇是接自 手把手带你做自定义view系列 宗旨都是一样,带大家一起来研究自定义view的实现,与其不同的是本系列省去了简单的坐标之类的讲解,重点在实现思路,用简洁明了的文章,来与大家一同一步步学习. ...

  4. 用POP动画编写带富文本的自定义动画效果

    用POP动画编写带富文本的自定义动画效果 [源码] https://github.com/YouXianMing/UI-Component-Collection [效果] [特点] * 支持富文本 * ...

  5. [原创]java WEB学习笔记42:带标签体的自定义标签,带父标签的自定义标签,el中自定义函数,自定义标签的小结

    本博客为原创:综合 尚硅谷(http://www.atguigu.com)的系统教程(深表感谢)和 网络上的现有资源(博客,文档,图书等),资源的出处我会标明 本博客的目的:①总结自己的学习过程,相当 ...

  6. 带你体验Android自定义圆形刻度罗盘 仪表盘 实现指针动态改变

    带你体验Android自定义圆形刻度罗盘 仪表盘 实现指针动态改变 转 https://blog.csdn.net/qq_30993595/article/details/78915115   近期有 ...

  7. openresty+lua在反向代理服务中的玩法

    openresty+lua在反向代理服务中的玩法 phith0n · 2015/06/02 10:35 0x01 起因 几天前学弟给我介绍他用nginx搭建的反代,代理了谷歌和维基百科. 由此我想到了 ...

  8. Linux 笔记 - 第十九章 配置 Squid 正向代理和反向代理服务

    一.简介 Squid 是一个高性能的代理缓存服务器,对应中文的乌贼,鱿鱼的意思.Squid 支持 FTP,gopher 和 HTTP 协议.和一般的代理缓存软件不同,Squid 用一个单独的,非模块化 ...

  9. nginx 反向代理服务

    目录 Nginx代理服务基本概述 Nginx代理服务常见模式 Nginx代理服务支持协议 Nginx反向代理配置语法 Nginx反向代理场景实践 配置代理实战 在lb01上安装nginx Nginx代 ...

随机推荐

  1. chrome 屏蔽广告的利器

    Adblock Plus https://chrome.google.com/webstore/detail/adblock-plus/cfhdojbkjhnklbpkdaibdccddilifddb ...

  2. 《Linux基础知识及命令》系列分享专栏

    <Linux基础知识及命令>系列分享专栏 本专题详细为大家讲解了Linux入门基础知识,思路清晰,简单易懂.本专题非常适合刚刚学习Linux的小白来学习,通过学习该专题会让你由入门达到中级 ...

  3. Local dimming algorithm in matlab plus 1

    (续)LED局部背光算法MATLAB仿真 在上一篇博客<Local dimming algorithm in matlab>中,我们实现了对一篇论文的算法用matlab仿真.在本篇论文中, ...

  4. java 语言知识

    1.javase 标准版主要用于桌面应用.控制台:javaee 企业版主要用于web应用:javame微缩版主要用于嵌入式. 2.jre是java程序的运行环境,包含jvm(java虚拟机).jdk是 ...

  5. 全面了解Nginx到底能做什么

    最近做项目需要动静分离,便用nginx的反向代理来实现.后来看到一篇好文,记录下. 来自https://www.jianshu.com/p/8bf73d1a758c 前言 本文只针对Nginx在不加载 ...

  6. PAT乙级:1094 谷歌的招聘 (20分)

    PAT乙级:1094 谷歌的招聘 (20分) 题干 2004 年 7 月,谷歌在硅谷的 101 号公路边竖立了一块巨大的广告牌(如下图)用于招聘.内容超级简单,就是一个以 .com 结尾的网址,而前面 ...

  7. PAT乙级:1072开学寄语(20分)

    PAT乙级:1072开学寄语(20分) 题干 下图是上海某校的新学期开学寄语:天将降大任于斯人也,必先删其微博,卸其 QQ,封其电脑,夺其手机,收其 ipad,断其 wifi,使其百无聊赖,然后,净面 ...

  8. 记一次错误:mid=front+(rear-front)>>1;

    设rear=6,front=4,mid=front+(rear-front)>>1; mid应该等于5的,但结果却是3. 错误原因:"+"运算符的优先级高于" ...

  9. AT2390 Games on DAG

    AT2390 Games on DAG 题意 \(1,2\) 号点各一个石头,每次沿边移动一个石头,不能动者输.求所有连边子集中先手胜的情况. 思路 发现对于两个石头的 SG 函数是独立的,输者两个石 ...

  10. Python -- 让程序运行后不立即关闭窗口

    程序运行完毕,窗口也跟着关闭.也就是说还没来得及看结果,程序窗口就关闭了. 试着改改代码,在最后加上以下这行代码: raw_input("Press <enter>") ...