Spring 4.3实现Server-Sent Events(SSE)方案咨询(无法升级)
Spring 4.3 实现 Server-Sent Events (SSE) 完整方案
我太懂你的处境了——Spring 4.3的SSE资料确实稀缺,不能升级版本的限制又把很多Spring 5+的方案堵死了。别慌,不用@Schedule定时任务也能实现真正的持续推送,咱们从你提到的Emitter核心逻辑说起,一步步解决问题。
先搞懂Spring 4.3里的两个关键Emitter
Spring 4.2开始就引入了SSE相关的发射器,你之前没弄明白的差异其实很简单:
SseEmitter:这是专门为SSE场景封装的工具,自动帮你处理Content-Type: text/event-stream响应头、SSE消息格式(事件ID、类型、数据分隔符)、连接超时这些细节,是实现SSE的首选。ResponseBodyEmitter:这是更通用的异步响应发射器,你可以用它自定义任何流式响应,但SSE场景下SseEmitter已经把重复工作做了,没必要用这个。
真正可行的持续推送实现(非定时任务版)
你的问题核心是之前的实现没有持续的异步推送逻辑,导致接口一次性返回后就断开连接。下面是两种常见的实现方式:
1. 定时推送(用线程池替代@Schedule)
如果你需要固定间隔推送数据,用ScheduledExecutorService来替代@Schedule,避免Spring定时任务的全局配置问题:
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.io.IOException; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @RestController public class SseController { // 单独的线程池处理SSE推送,避免阻塞主线程 private final ScheduledExecutorService sseScheduler = Executors.newScheduledThreadPool(2); @GetMapping("/sseTest") public SseEmitter startSse() { // 设置连接超时时间(比如30分钟),避免无响应连接占用资源 SseEmitter emitter = new SseEmitter(1800000L); // 启动异步推送任务:立即执行,每2秒推送一次 sseScheduler.scheduleAtFixedRate(() -> { try { // 构建标准SSE消息,可自定义ID、事件类型、数据 SseEmitter.SseEventBuilder event = SseEmitter.event() .id(String.valueOf(System.currentTimeMillis())) .name("time-update") .data("服务器当前时间:" + System.currentTimeMillis()); emitter.send(event); } catch (IOException e) { // 推送失败时,关闭发射器并清理任务 emitter.completeWithError(e); sseScheduler.shutdown(); } }, 0, 2, TimeUnit.SECONDS); // 连接关闭/超时的回调,清理资源 emitter.onCompletion(sseScheduler::shutdown); emitter.onTimeout(() -> { emitter.complete(); sseScheduler.shutdown(); }); return emitter; } }
2. 事件驱动推送(业务触发时主动推送)
如果不需要定时,而是要在某个业务事件发生时(比如订单状态更新)主动推送给客户端,就用这种方式:
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.io.IOException; import java.util.Iterator; import java.util.concurrent.CopyOnWriteArrayList; @RestController public class EventDrivenSseController { // 用线程安全的集合保存活跃的SSE连接 private final CopyOnWriteArrayList<SseEmitter> activeEmitters = new CopyOnWriteArrayList<>(); @GetMapping("/sseEvent") public SseEmitter connectSse() { SseEmitter emitter = new SseEmitter(1800000L); activeEmitters.add(emitter); // 连接关闭/超时后,从集合中移除 emitter.onCompletion(() -> activeEmitters.remove(emitter)); emitter.onTimeout(() -> { emitter.complete(); activeEmitters.remove(emitter); }); return emitter; } // 业务代码中调用这个方法,触发推送 public void pushBusinessEvent(String eventContent) { Iterator<SseEmitter> iterator = activeEmitters.iterator(); while (iterator.hasNext()) { SseEmitter emitter = iterator.next(); try { emitter.send("业务通知:" + eventContent); } catch (IOException e) { // 推送失败,关闭连接并清理 emitter.completeWithError(e); iterator.remove(); } } } }
关键注意点(解决你之前一次性返回的问题)
- 必须用异步线程推送:如果在Controller主线程里直接发送一次就返回,客户端只会收到一次数据然后断开连接。一定要用线程池或者异步任务来持续发送。
- 连接生命周期管理:务必处理
onCompletion、onTimeout和推送异常,不然会导致线程泄漏或者无效连接占用资源。 - 不要手动设置响应头:
SseEmitter会自动设置text/event-stream、no-cache这些SSE必需的响应头,手动设置反而可能出问题。
内容的提问来源于stack exchange,提问作者jpganz18
相关产品推荐
相关产品推荐

