Spring Boot整合SSE实现消息推送:跨域问题解决与前后端联调实战
摘要
本文记录了一次完整的Spring Boot整合Server-Sent Events(SSE)实现实时消息推送的开发过程,重点分析前后端联调时遇到的跨域问题及解决方案。通过@CrossOrigin
注解的实际应用案例,帮助开发者快速定位和解决类似问题。
一、项目背景与需求
开发一个实时订单推送系统,需要实现:
- 司机端与服务端的持久化连接
- 订单信息实时推送
- 客户端主动关闭连接
二、技术方案设计
2.1 技术选型
技术组件 | 作用说明 |
---|---|
Spring Boot | 后端服务框架 |
SSE | 服务端推送协议 |
Vue/原生HTML | 前端消息展示 |
2.2 系统架构图
前端页面(localhost:63342)│▼Spring Boot服务端(localhost:9000)│▼订单推送系统(业务逻辑)
三、关键代码实现
3.1 前端实现(监听客户端)
<script>
// 建立SSE连接(关键代码)
const source = new EventSource(`http://localhost:9000/service-sse-push/connect?userId=1&identity=1`
);// 消息接收处理
source.addEventListener("message", e => {document.getElementById("message").innerHTML += e.data + '<br/>';
});// 关闭连接时通知服务端
function sourceClose() {fetch(`http://localhost:9000/close?userId=1&identity=1`);source.close();
}
</script>
3.2 后端核心控制器
@RestController
@CrossOrigin(origins = "http://localhost:63342") // 解决跨域的关键注解
public class SseController {// 存储所有连接的Mappublic static Map<String, SseEmitter> sseEmitterMap = new HashMap<>();/*** 建立SSE连接端点* @param userId 用户ID* @param identity 身份标识*/@GetMapping("/connect")public SseEmitter connect(@RequestParam Long userId, @RequestParam String identity) {SseEmitter emitter = new SseEmitter(60_000L);String key = SsePrefixUtils.generatorSseKey(userId, identity);sseEmitterMap.put(key, emitter);return emitter;}/*** 消息推送端点* @param pushRequest 推送请求体*/@PostMapping("/push")public String push(@RequestBody PushRequest pushRequest) {String key = SsePrefixUtils.generatorSseKey(pushRequest.getUserId(), pushRequest.getIdentity());if (sseEmitterMap.containsKey(key)) {sseEmitterMap.get(key).send(pushRequest.getContent());return "推送成功";}return "用户未连接";}
}
四、遇到的典型问题
4.1 跨域问题现象
控制台报错:
Access to XMLHttpRequest at 'http://localhost:9000/connect' from origin
'http://localhost:63342' has been blocked by CORS policy
4.2 问题分析
- 前端运行在
localhost:63342
(WebStorm默认端口) - 后端服务运行在
localhost:9000
- 浏览器安全策略阻止跨域请求
4.3 解决方案
// 在Controller类上添加注解
@CrossOrigin(origins = "http://localhost:63342") // 或在方法级别添加
@GetMapping("/connect")
@CrossOrigin(origins = "http://localhost:63342")
public SseEmitter connect(...) {...}
注解作用说明:
origins
:允许指定的源访问methods
:允许的HTTP方法(默认所有)allowedHeaders
:允许的请求头maxAge
:预检请求缓存时间
五、其他优化实践
5.1 连接保活机制
// 添加心跳检测
emitter.onTimeout(() -> {log.info("连接超时:{}", key);sseEmitterMap.remove(key);
});emitter.onCompletion(() -> {log.info("连接完成:{}", key);sseEmitterMap.remove(key);
});
5.2 线程安全改进
// 改用ConcurrentHashMap
public static Map<String, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>();
5.3 连接监控看板
@Scheduled(fixedRate = 5000)
public void printConnections() {log.info("当前活跃连接数:{}", sseEmitterMap.size());
}
六、完整联调流程
6.1 启动顺序
- 启动Spring Boot服务(端口9000)
- 用浏览器打开前端页面(端口63342)
6.2 测试步骤
- 点击页面按钮建立连接
- 通过Postman发送推送请求
- 观察页面消息展示
- 点击关闭按钮断开连接
测试结果示例:
用户[1_1] 成功建立连接
收到订单消息:新订单到达,请及时处理
用户[1_1] 主动断开连接
七、经验总结
- 跨域问题定位:当出现
CORS policy
错误时,首先检查前端请求地址与后端允许的源 - 连接管理要点:
- 及时清理失效连接
- 使用线程安全集合
- 添加心跳检测机制
- 生产环境建议:
- 使用Nginx反向代理统一端口
- 配置HTTPS加密通信
踩坑心得:
跨域问题就像程序员的"拦路虎",通过这次实践深刻理解了浏览器同源策略的重要性。
@CrossOrigin
虽好,但生产环境需要更严格的安全控制。
讨论话题:
你在使用SSE时还遇到过哪些奇葩问题?欢迎在评论区分享你的踩坑经历!