Java 内存马(八):Jetty Handler 与 Undertow HttpHandler

在 Tomcat 中排查 Pipeline 和 Valve,换成 Jetty 或 Undertow 后,这套对象名全部失效。Jetty 把请求组件组织成 Handler 树,Undertow 则从 Root HttpHandler 开始,通过 next 引用和条件分支组成执行链。

两者的共同点是:原生 Handler 可以位于 Servlet 之前。如果它提前完成请求,应用 Filter、Servlet 和 Controller 都不会执行。

本文使用两个独立实验:Jetty 12 jetty-server 与 Undertow 2.x undertow-core。代码只增加固定响应头,不执行系统命令。

一、两种 Handler 模型有什么不同

1
2
3
4
5
6
7
8
9
10
11
Jetty
Server
└─ Handler.Wrapper
└─ ContextHandler
└─ ServletHandler 或业务 Handler

Undertow
Listener
→ Root HttpHandler
→ Path / Predicate / Security Handler
→ Servlet Handler 或业务 Handler
对比项 Jetty Undertow
核心接口 Handler HttpHandler
组织形式 组件树、Wrapper、Sequence next 链和条件分支
请求对象 Request、Response、Callback HttpServerExchange
完成语义 返回 handled 状态并完成 Callback 调用 next、dispatch 或结束 Exchange
生命周期 Handler 同时属于 Jetty 组件生命周期 主要由持有它的服务或部署管理
首要证据 Server component dump、父子关系 Root Handler、next/分支、Deployment

这不是类名差异,而是对象图差异。Jetty 要找“父节点持有哪些子 Handler”,Undertow 要找“根对象沿哪些引用到达下一个 Handler”。

版本也必须分开:Jetty 10、11、12 的 Handler 签名和 Servlet EE 模块不同;嵌入式 Undertow 与 WildFly 管理的 Undertow 又不是同一套装配入口。先确认中间件和大版本,再选择枚举方式。

二、Jetty Handler 树与动态 Wrapper

Jetty 12 的 Handler 使用异步 Callback 结束请求。下面的包装器先增加标记,再调用子 Handler;如果子 Handler 返回 false,它会撤销响应修改,遵守 Jetty 的未处理语义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package lab.jetty;

import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.Callback;

public final class LabMarkerHandler extends Handler.Wrapper {
private static final String HEADER = "X-Handler-Lab";

public LabMarkerHandler(Handler next) {
super(next);
}

@Override
public boolean handle(
Request request,
Response response,
Callback callback) throws Exception {

response.getHeaders().put(HEADER, "jetty");
boolean handled = super.handle(request, response, callback);

if (!handled) {
response.getHeaders().remove(HEADER);
}
return handled;
}
}

构造一个可在启动后安全替换子节点的根 Wrapper:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package lab.jetty;

import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;

public final class JettyLabServer {
public static void main(String[] args) throws Exception {
Server server = new Server(8080);

Handler app = new Handler.Abstract() {
@Override
public boolean handle(
org.eclipse.jetty.server.Request request,
Response response,
org.eclipse.jetty.util.Callback callback) {

response.setStatus(200);
response.getHeaders().put(
"Content-Type", "text/plain;charset=UTF-8");
Content.Sink.write(
response, true, "JETTY_LAB_OK", callback);
return true;
}
};

Handler.Wrapper root = new Handler.Wrapper(true, app);
server.setHandler(root);
server.start();

Handler original = root.getHandler();
root.setHandler(new LabMarkerHandler(original));

server.dumpStdErr();
server.join();
}
}

验证:

1
curl -i http://127.0.0.1:8080/

预期响应包含:

1
2
3
X-Handler-Lab: jetty

JETTY_LAB_OK

Handler.Wrapper(true, app) 中的 true 表示动态容器,允许 Server 启动后调用 setHandler()。普通非动态 Wrapper 不能被当成通用热替换点;直接改私有字段还可能留下未启动、重复启动或未停止的 Handler。

恢复时使用最初保存的对象:

1
root.setHandler(original);

Jetty 的 server.dump() 或 dumpStdErr() 会输出包含 Handler 结构、生命周期状态和类加载信息的组件树。正常部署后保存一份 dump,事后差异对比比搜索某个恶意类名更可靠。

三、Undertow HttpHandler 链与根节点替换

Undertow 的核心接口只有一个方法:

1
2
3
public interface HttpHandler {
void handleRequest(HttpServerExchange exchange) throws Exception;
}

下面的节点增加固定标记,再把 Exchange 交给 next:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package lab.undertow;

import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.HttpString;

public final class LabMarkerHandler implements HttpHandler {
private static final HttpString HEADER =
new HttpString("X-Handler-Lab");

private final HttpHandler next;

public LabMarkerHandler(HttpHandler next) {
this.next = next;
}

@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
exchange.getResponseHeaders().put(HEADER, "undertow");
next.handleRequest(exchange);
}
}

Undertow Builder 在启动时接收 Root Handler。若确实需要受控热替换,应从一开始放入一个并发可见的代理节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package lab.undertow;

import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;

import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;

public final class SwitchableHandler implements HttpHandler {
private final AtomicReference<HttpHandler> delegate;

public SwitchableHandler(HttpHandler initial) {
delegate = new AtomicReference<>(Objects.requireNonNull(initial));
}

public HttpHandler replace(HttpHandler next) {
return delegate.getAndSet(Objects.requireNonNull(next));
}

public HttpHandler current() {
return delegate.get();
}

@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
HttpHandler current = delegate.get();
current.handleRequest(exchange);
}
}

完整装配与替换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
HttpHandler app = exchange -> {
exchange.getResponseHeaders().put(
io.undertow.util.Headers.CONTENT_TYPE,
"text/plain;charset=UTF-8");
exchange.getResponseSender().send("UNDERTOW_LAB_OK");
};

io.undertow.server.handlers.PathHandler routes =
io.undertow.Handlers.path().addExactPath("/demo", app);

SwitchableHandler root = new SwitchableHandler(routes);

io.undertow.Undertow server = io.undertow.Undertow.builder()
.addHttpListener(8081, "127.0.0.1")
.setHandler(root)
.build();

server.start();

HttpHandler original = root.current();
root.replace(new LabMarkerHandler(original));

验证:

1
curl -i http://127.0.0.1:8081/demo

恢复原链:

1
root.replace(original);

AtomicReference 只保证新请求能够看见新 delegate。已经读取旧引用的请求仍可能在旧链中执行,因此“replace 已返回”不等于异常对象已经从堆中消失,还要等待在途请求结束并检查其他强引用。

四、排查、WildFly 边界与恢复闭环

Jetty 排查从 Server 根节点开始:

1
2
3
4
5
保存 server.dump()
→ 展开 Handler 父子关系与顺序
→ 对比可信实例的组件树
→ 核对 Handler 生命周期状态
→ 检查 ClassLoader、CodeSource 和实际字节码

只枚举 ServletContext.getFilterRegistrations() 会漏掉位于 WebAppContext 外层的 Handler。异常节点如果提前处理请求,应用访问日志和 Filter 日志也可能完全没有记录。

Undertow 没有一个适用于所有 Handler 的统一 getNext() 接口。排查必须先找到 Root Handler,再按实际类型展开:

1
2
3
4
5
6
Root HttpHandler
→ Wrapper 的 next
→ PathHandler 的路径分支
→ PredicateHandler 的 true/false 分支
→ Security Handler
→ ServletInitialHandler 或业务 Handler

嵌入式应用应在装配时保留根引用并提供只读快照;缺少观测接口时,需要结合堆转储、调试器或应用自己的组件注册表恢复对象图。

WildFly 虽然使用 Undertow,但还存在管理模型、Deployment 和 JBoss Modules:

  • 导出 Undertow 子系统中的 Server、Host、Filter 与 Handler 配置;
  • 核对服务器级 custom-filter、expression-filter 及引用关系;
  • 对齐 DeploymentInfo 与堆中的实际 Handler;
  • 检查实现类来自哪个 JBoss Module 或部署 ClassLoader。

只看 WAR 会漏掉服务器级配置,只看管理配置又可能漏掉纯运行时对象。

最终判断仍然落到四个问题:

1
2
3
4
对象由谁持有
请求如何到达
配置能否解释
卸载后是否仍有强引用

Jetty 应通过动态容器恢复原子节点并遵守组件 start/stop 约束;Undertow 应恢复原 Root 或 next 引用,并等待在途请求结束;WildFly 还要同步处理管理配置和 Deployment 服务。完成后重新验证正常、错误、异步请求,并检查类加载器、线程、定时任务或 Agent 是否仍持有异常 Handler。

上一篇
下一篇