在 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
|
恢复原链:
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。