Spring Boot SSE 示例
在 Spring Boot 项目中实现 Server-Sent Events (SSE) 是一种向客户端推送实时数据的有效方式。SSE 允许服务器通过 HTTP 连接自动向客户端发送更新,而无需客户端进行轮询。以下是一个简单的示例,展示如何在 Spring Boot 项目中实现 SSE 请求。
1. 创建 Spring Boot 项目
使用 Spring Initializr 来生成一个基本的 Spring Boot 项目,选择 Spring Web 依赖。
点击查看完整 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>me.liujiajia.example</groupId>
<artifactId>sse</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>sse</name>
<description>Demo project for Spring Boot</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
注意
通过 Spring Initializr 生成项目时,如果打包类型设置为 War,生成的代码中会自动添加 spring-boot-starter-tomcat 依赖并将 scope 设置为 provided,表示该依赖仅在编译和测试阶段使用,而在运行时由容器提供。此时在 IDE 中启动 Application 时,应用会自动停止。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
2. 创建 SSE 控制器
创建一个控制器来处理 SSE 请求。
package me.liujiajia.example.sse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.*;
@RestController
public class SseController {
private static final Logger log = LoggerFactory.getLogger(SseController.class);
private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
private final Map<Integer, SseEmitter> emitterMap = new ConcurrentHashMap<>();
{
executorService.scheduleAtFixedRate(() -> {
String message = "Server time: " + System.currentTimeMillis();
emitterMap.values().forEach(emitter -> {
try {
emitter.send(SseEmitter.event()
//.reconnectTime(1000)
.data(message));
} catch (IOException ex) {
emitter.completeWithError(ex);
}
});
}, 0, 1, TimeUnit.SECONDS);
}
@GetMapping(value = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter streamEvents() {
var emitter = new SseEmitter(30_000L);
emitter.onCompletion(() -> {
emitterMap.remove(emitter.hashCode());
log.info("SseEmitter {} completed", emitter.hashCode());
});
emitter.onTimeout(() -> {
emitterMap.remove(emitter.hashCode());
log.info("SseEmitter {} timeout", emitter.hashCode());
emitter.complete();
});
emitter.onError(e -> {
emitterMap.remove(emitter.hashCode());
log.error("SseEmitter {} error", emitter.hashCode(), e);
emitter.completeWithError(e);
});
emitterMap.put(emitter.hashCode(), emitter);
log.info("SseEmitter {} created", emitter.hashCode());
return emitter;
}
}
创建 SseEmitter
时可以指定过期时间(单位是毫秒),如果未指定过期时间,默认是 30 秒。这个默认值可以通过 spring.mvc.async.request-timeout
参数来配置。
spring.mvc.async.request-timeout=15s
具体的过期时间可以根据项目需要进行调整,客户端也可以主动关闭连接或者超时后再次订阅。
上面的示例中在发生异常时,直接将 emitter
标记为已完成,除此之外也可以通过配置 reconnectTime
指定客户端在发生异常时尝试重新连接。此时在 onError
处理中就不可以调用 complete
或 completeWithError
方法了,因为一旦将 emitter
的状态标记为已完成,当前的连接就会被关闭,必须重新创建 SseEmitter
。
3. 前端示例页面
在 resources/static 目录下创建 index.html 文件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SSE Example</title>
</head>
<body>
<h1>Server-Sent Events Example</h1>
<div id="events"></div>
<script>
const eventSource = new EventSource('/sse');
eventSource.onmessage = function(event) {
const newElement = document.createElement("div");
newElement.innerHTML = "Message: " + event.data;
document.getElementById("events").appendChild(newElement);
};
eventSource.onerror = function(event) {
eventSource.close();
alert("EventSource failed: " + event);
};
</script>
</body>
</html>
4. 运行 Spring Boot 应用
package me.liujiajia.example.sse;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SseApplication {
public static void main(String[] args) {
SpringApplication.run(SseApplication.class, args);
}
}
应用默认使用 8080 端口,在浏览器中访问 http://localhost:8080/ 即可看到 SSE 示例的效果:每一秒会打印一个服务器的时间戳,超时后会弹一个提示框。
在后台可以看到类似如下的日志:
2025-03-12T17:52:14.391+08:00 INFO 35964 --- [sse] [nio-8080-exec-1] me.liujiajia.example.sse.SseController : SseEmitter 2019245575 created
2025-03-12T17:52:24.634+08:00 INFO 35964 --- [sse] [nio-8080-exec-2] me.liujiajia.example.sse.SseController : SseEmitter 2019245575 timeout
2025-03-12T17:52:24.641+08:00 WARN 35964 --- [sse] [nio-8080-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : Ignoring exception, response committed already: org.springframework.web.context.request.async.AsyncRequestTimeoutException
2025-03-12T17:52:24.641+08:00 WARN 35964 --- [sse] [nio-8080-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.context.request.async.AsyncRequestTimeoutException]
2025-03-12T17:52:24.642+08:00 INFO 35964 --- [sse] [nio-8080-exec-2] me.liujiajia.example.sse.SseController : SseEmitter 2019245575 completed