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

Page Layout Max Width

Adjust the exact value of the page width of VitePress layout to adapt to different reading needs and screens.

Adjust the maximum width of the page layout
A ranged slider for user to choose and customize their desired width of the maximum width of the page layout can go.

Content Layout Max Width

Adjust the exact value of the document content width of VitePress layout to adapt to different reading needs and screens.

Adjust the maximum width of the content layout
A ranged slider for user to choose and customize their desired width of the maximum width of the content layout can go.