如何在Polarion ALM中避免HttpServlet服务结束后关闭输出流?
我正在为Polarion ALM开发基于javax.servlet.http.HttpServlet的自定义Servlet,希望在service方法执行完毕后,仍保持与客户端的连接/输出流处于打开状态,并定期向其发送ping消息。
相关代码如下:
public class CustomServlet extends HttpServlet { [...] @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { [...] response.setStatus(200); response.setContentType("application/x-ndjson"); ExperimentalPingThread thread = new ExperimentalPingThread(); thread.setOut(response.getOutputStream()); Thread t = new Thread(thread); t.start(); return; } [...] }
public class ExperimentalPingThread implements Runnable { ServletOutputStream out; @Override public void run() { JSONObject configJson = new JSONObject(); configJson.put("type", "ping"); configJson.put("payload", new JSONObject()); String outString = configJson.toString() + "\n"; Timer timer = new Timer(); TimerTask task = new TimerTask() { public void run() { System.out.println("sending Ping: " + outString); try { out.write(outString.getBytes()); out.flush(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }; timer.schedule( task, 0L ,1000L); } public void setOut(ServletOutputStream out) { this.out = out; } }
我知道ExperimentalPingThread类存在冗余,因为Timer本身也会创建新线程,但正如类名所示,目前处于实验阶段。实际测试发现,访问Polarion LiveReportPage的客户端无法接收到定期发送的ping消息,排查后发现service方法执行完毕后,输出流就被关闭了。请问是否是HttpServlet生命周期机制强制关闭了输出流?能否对此进行干预?
解答
是的,Servlet容器(Polarion底层通常基于Tomcat)会在service方法执行完成后自动关闭响应输出流,这是Servlet规范的强制要求——容器需要统一管理请求/响应的生命周期,确保资源被及时释放,避免内存泄漏。
要实现长连接定期发送消息的需求,不能在service方法返回后单独启动线程处理输出,必须通过合规的方式干预:
方案1:在service方法内阻塞处理(简单直接)
将发送逻辑放在service方法内部,保持当前线程阻塞,直到需要终止连接时再退出方法。这种方式适合简单场景,但会占用容器线程,不适合高并发场景。
修改后的代码示例:
public class CustomServlet extends HttpServlet { @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 配置响应头,确保连接保持且数据不被缓存 response.setStatus(200); response.setContentType("application/x-ndjson"); response.setHeader("Cache-Control", "no-cache"); response.setHeader("Connection", "keep-alive"); response.setBufferSize(0); // 禁用缓冲,实时发送数据 ServletOutputStream out = response.getOutputStream(); JSONObject configJson = new JSONObject(); configJson.put("type", "ping"); configJson.put("payload", new JSONObject()); String outString = configJson.toString() + "\n"; try { // 循环发送ping,直到线程被中断 while (!Thread.currentThread().isInterrupted()) { out.write(outString.getBytes()); out.flush(); Thread.sleep(1000); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 保留中断状态 } finally { out.close(); } } }
方案2:使用Servlet 3.0+异步API(推荐)
通过request.startAsync()获取异步上下文,将发送逻辑转移到异步线程中,容器不会在service方法结束后立即关闭输出流,同时能更好地利用容器线程池资源,适合高并发场景。
代码示例:
public class CustomServlet extends HttpServlet { @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 配置响应头 response.setStatus(200); response.setContentType("application/x-ndjson"); response.setHeader("Cache-Control", "no-cache"); response.setHeader("Connection", "keep-alive"); response.setBufferSize(0); // 启动异步上下文,脱离service方法的生命周期 AsyncContext asyncContext = request.startAsync(); asyncContext.setTimeout(0); // 设置不超时(或根据需求设置合理值) ServletOutputStream out = response.getOutputStream(); JSONObject configJson = new JSONObject(); configJson.put("type", "ping"); configJson.put("payload", new JSONObject()); String outString = configJson.toString() + "\n"; // 定时发送ping消息 Timer timer = new Timer(); TimerTask task = new TimerTask() { @Override public void run() { try { out.write(outString.getBytes()); out.flush(); } catch (IOException e) { // 客户端断开连接,清理资源 timer.cancel(); asyncContext.complete(); } } }; timer.schedule(task, 0L, 1000L); // 添加异步监听,处理连接完成、超时、异常等情况 asyncContext.addListener(new AsyncListener() { @Override public void onComplete(AsyncEvent event) { timer.cancel(); } @Override public void onTimeout(AsyncEvent event) { timer.cancel(); asyncContext.complete(); } @Override public void onError(AsyncEvent event) { timer.cancel(); asyncContext.complete(); } @Override public void onStartAsync(AsyncEvent event) {} }); } }
额外注意事项
- 必须处理客户端断开连接的场景:当客户端主动关闭连接时,
write或flush操作会抛出IOException,此时需要立即停止定时任务并调用asyncContext.complete()释放资源。 - 检查Polarion ALM及底层容器的配置:比如Tomcat的连接超时、反向代理的超时设置,确保这些组件不会主动断开长连接。
- 避免无限制创建长连接:根据业务需求设置合理的超时时间,防止资源耗尽。
内容的提问来源于stack exchange,提问作者cfhenk

