
本文介绍一种轻量、标准、无需大型框架的 java web 解决方案:利用 http 状态码 202(accepted)配合 server-sent events(sse),在单次用户请求中实现“立即反馈 + 后续更新”,完全兼容 war 部署,不依赖 websocket 或第三方前端库。
本文介绍一种轻量、标准、无需大型框架的 java web 解决方案:利用 http 状态码 202(accepted)配合 server-sent events(sse),在单次用户请求中实现“立即反馈 + 后续更新”,完全兼容 war 部署,不依赖 websocket 或第三方前端库。
在传统 Servlet 架构中,一个 HTTP 请求只能对应一次响应(HttpServletResponse 输出流关闭即结束),因此无法直接“分两次向浏览器推送内容”。但真实业务场景中——例如提交报表生成、大文件处理或 AI 推理任务——用户需要即时感知请求已被接收(避免误点重试),再异步获取最终结果。此时,强行复用响应流、滥用长轮询或硬切 WebSocket,反而增加复杂度与部署风险。
✅ 正确解法是遵循 HTTP 语义,采用 “202 Accepted + SSE 回推” 的协作模式:
首次响应(即时):用户发起 POST 请求(如 /process),Servlet 不执行耗时逻辑,而是立即启动新线程处理,并返回 HTTP 202 Accepted 及唯一 jobId(如 JSON:{"status":"accepted","jobId":"j-abc123"})。浏览器收到后立刻显示“处理中…”对话框。
二次通知(异步):浏览器通过 EventSource 连接 /events?jobId=j-abc123,建立持久化 SSE 连接;服务端在后台任务完成时,向该 jobId 关联的 SseEmitter(或原生 PrintWriter)写入 data: {"status":"completed","result":"..."} 事件。
结果获取(按需):若结果数据较小(< 10KB),可直接随 SSE 事件一并下发;若较大(如导出 Excel),则仅推送轻量事件(如 {"status":"ready"}),由前端再发起 GET /result?jobId=j-abc123 下载二进制内容。
以下是关键代码示例(纯 Servlet,无 Spring):
// 1. 主处理 Servlet(/process)
@WebServlet("/process")
public class ProcessServlet extends HttpServlet {
private static final Map<String, PrintWriter> EVENT_WRITERS = new ConcurrentHashMap<>();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String jobId = "j-" + UUID.randomUUID().toString().substring(0, 8);
// 立即返回 202
resp.setStatus(HttpServletResponse.SC_ACCEPTED);
resp.setContentType("application/json");
resp.getWriter().write("{\"status\":\"accepted\",\"jobId\":\"" + jobId + "\"}");
// 启动后台任务(注意:需自行管理线程生命周期,生产建议用 ExecutorService)
new Thread(() -> {
try {
// 模拟耗时操作(如数据库聚合、文件转换)
Thread.sleep(5000);
// 任务完成:向对应 jobId 的 EventSource 推送事件
PrintWriter writer = EVENT_WRITERS.get(jobId);
if (writer != null && !writer.checkError()) {
writer.write("data: {\"status\":\"completed\",\"result\":\"Success!\"}\n\n");
writer.flush();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
}
}
// 2. SSE 事件 Servlet(/events)
@WebServlet("/events")
public class EventServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String jobId = req.getParameter("jobId");
if (jobId == null) return;
resp.setContentType("text/event-stream");
resp.setHeader("Cache-Control", "no-cache");
resp.setHeader("Connection", "keep-alive");
resp.setCharacterEncoding("UTF-8");
PrintWriter writer = resp.getWriter();
ProcessServlet.EVENT_WRITERS.put(jobId, writer); // 简化示例,实际需考虑连接超时清理
// 保持连接存活(发送心跳)
while (!writer.checkError() && !Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(15_000);
writer.write(": heartbeat\n\n");
writer.flush();
} catch (InterruptedException e) {
break;
}
}
ProcessServlet.EVENT_WRITERS.remove(jobId);
}
}前端调用示例(原生 JavaScript):
// 1. 提交任务
fetch('/process', { method: 'POST' })
.then(r => r.json())
.then(data => {
const jobId = data.jobId;
showLoadingDialog(); // 显示“处理中…”
// 2. 建立 SSE 连接
const eventSource = new EventSource(`/events?jobId=${jobId}`);
eventSource.onmessage = e => {
const result = JSON.parse(e.data);
if (result.status === 'completed') {
hideLoadingDialog();
updatePage(result.result);
eventSource.close();
}
};
});⚠️ 注意事项:
- 线程安全:EVENT_WRITERS 映射需使用 ConcurrentHashMap,且必须在连接断开或超时时及时清理 PrintWriter,否则内存泄漏;
- 连接保活:SSE 要求服务端定期发送注释行(: 开头)或空事件,防止代理/浏览器自动断连;
- WAR 兼容性:上述代码仅依赖 Servlet API(javax.servlet.*),编译为 WAR 后可直接部署到 Tomcat/Jetty,无需额外框架;
- 替代方案对比:WebSocket 虽支持双向通信,但需额外维护连接状态、消息路由,且 Servlet 容器对 WebSocket 的 WAR 支持需显式配置(如 @ServerEndpoint 注解),反而不如 SSE 简洁标准。
总结:用 202 + SSE 组合,既符合 REST 架构原则,又规避了 WebSocket 的复杂性与长轮询的低效性,是 Servlet 场景下实现“两次响应”的最优实践。

















