Skip to content
欢迎扫码关注公众号

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
<?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 时,应用会自动停止。

xml
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-tomcat</artifactId>
  <scope>provided</scope>
</dependency>

2. 创建 SSE 控制器

创建一个控制器来处理 SSE 请求。

java
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 参数来配置。

properties
spring.mvc.async.request-timeout=15s

具体的过期时间可以根据项目需要进行调整,客户端也可以主动关闭连接或者超时后再次订阅。

上面的示例中在发生异常时,直接将 emitter 标记为已完成,除此之外也可以通过配置 reconnectTime 指定客户端在发生异常时尝试重新连接。此时在 onError 处理中就不可以调用 completecompleteWithError 方法了,因为一旦将 emitter 的状态标记为已完成,当前的连接就会被关闭,必须重新创建 SseEmitter

3. 前端示例页面

resources/static 目录下创建 index.html 文件。

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 应用

java
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 示例的效果:每一秒会打印一个服务器的时间戳,超时后会弹一个提示框。

在后台可以看到类似如下的日志:

java
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