Spring Boot SSE 示例

在 Spring Boot 项目中实现 Server-Sent Events (SSE) 是一种向客户端推送实时数据的有效方式。SSE 允许服务器通过 HTTP 连接自动向客户端发送更新,而无需客户端进行轮询。以下是一个简单的示例,展示如何在 Spring Boot 项目中实现 SSE 请求。

1. 创建 Spring Boot 项目

使用 Spring Initializr 来生成一个基本的 Spring Boot 项目,选择 Spring Web 依赖。

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
相关推荐
wb043072014 小时前
使用 Java 开发 MCP 服务并发布到 Maven 中央仓库完整指南
java·开发语言·spring boot·ai·maven
nbwenren5 小时前
Springboot中SLF4J详解
java·spring boot·后端
helx826 小时前
SpringBoot中自定义Starter
java·spring boot·后端
rleS IONS6 小时前
SpringBoot获取bean的几种方式
java·spring boot·后端
lifewange7 小时前
Go语言-开源编程语言
开发语言·后端·golang
白毛大侠7 小时前
深入理解 Go:用户态和内核态
开发语言·后端·golang
R***z1018 小时前
Spring Boot 整合 MyBatis 与 PostgreSQL 实战指南
spring boot·postgresql·mybatis
王码码20358 小时前
Go语言中的数据库操作:从sqlx到ORM
后端·golang·go·接口
星辰_mya8 小时前
雪花算法和时区的关系
数据库·后端·面试·架构师
赵丙双9 小时前
spring boot AutoConfiguration.replacements 文件的作用
java·spring boot