SpringBoot使用SSE進行實時通知前端的實現(xiàn)代碼
說明
項目有個需求是要實時通知前端,告訴前端這個任務加載好了。然后想了2個方案,一種是用websocket進行長連接,一種是使用SSE(Sever Send Event),是HTTP協(xié)議中的一種,Content-Type為text/event-stream,能夠保持長連接。
websocket是前端既能向后端發(fā)送消息,后端也能向前端發(fā)送消息。
SSE是只能后端向前端發(fā)送消息。
因為只需要后端通知,所以我這里選擇了使用SSE實現(xiàn)。
這里先做個筆記,怕以后忘記怎么使用。
maven依賴
<?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>2.7.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.project</groupId>
<artifactId>test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>test</name>
<description>test</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--web依賴,內嵌入tomcat,SSE依賴于該jar包,只要有該依賴就能使用SSE-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--lombok依賴,用來對象省略寫set、get方法-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</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>SSE工具類代碼
package com.etone.project.utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
@Slf4j
public class SseEmitterServer {
/**
* 當前連接數(shù)
*/
private static AtomicInteger count = new AtomicInteger(0);
private static Map<String, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>();
public static SseEmitter connect(String userId){
//設置超時時間,0表示不過期,默認是30秒,超過時間未完成會拋出異常
SseEmitter sseemitter = new SseEmitter(0L);
//注冊回調
sseemitter.onCompletion(completionCallBack(userId));
//這個onError在springbooot低版本沒有這個方法,公司springboot1.4.2版本,沒有這個方法,可以進行注釋。
sseemitter.onError(errorCallBack(userId));
sseemitter.onTimeout(timeoutCallBack(userId));
sseEmitterMap.put(userId,sseemitter);
//數(shù)量+1
count.getAndIncrement();
log.info("create new sse connect ,current user:{}",userId);
return sseemitter;
}
/**
* 給指定用戶發(fā)消息
*/
public static void sendMessage(String userId, String message){
if(sseEmitterMap.containsKey(userId)){
try{
sseEmitterMap.get(userId).send(message);
}catch (IOException e){
log.error("user id:{}, send message error:{}",userId,e.getMessage());
e.printStackTrace();
}
}
}
/**
* 想多人發(fā)送消息,組播
*/
public static void groupSendMessage(String groupId, String message){
if(sseEmitterMap!=null&&!sseEmitterMap.isEmpty()){
sseEmitterMap.forEach((k,v) -> {
try{
if(k.startsWith(groupId)){
v.send(message, MediaType.APPLICATION_JSON);
}
}catch (IOException e){
log.error("user id:{}, send message error:{}",groupId,message);
removeUser(k);
}
});
}
}
public static void batchSendMessage(String message) {
sseEmitterMap.forEach((k,v)->{
try{
v.send(message,MediaType.APPLICATION_JSON);
}catch (IOException e){
log.error("user id:{}, send message error:{}",k,e.getMessage());
removeUser(k);
}
});
}
/**
* 群發(fā)消息
*/
public static void batchSendMessage(String message, Set<String> userIds){
userIds.forEach(userid->sendMessage(userid,message));
}
//移除用戶
public static void removeUser(String userid){
sseEmitterMap.remove(userid);
//數(shù)量-1
count.getAndDecrement();
log.info("remove user id:{}",userid);
}
public static List<String> getIds(){
return new ArrayList<>(sseEmitterMap.keySet());
}
public static int getUserCount(){
return count.intValue();
}
private static Runnable completionCallBack(String userId) {
return () -> {
log.info("結束連接,{}",userId);
removeUser(userId);
};
}
private static Runnable timeoutCallBack(String userId){
return ()->{
log.info("連接超時,{}",userId);
removeUser(userId);
};
}
private static Consumer<Throwable> errorCallBack(String userId){
return throwable -> {
log.error("連接異常,{}",userId);
removeUser(userId);
};
}
}Controller測試代碼
package com.project.test.controller;
import com.hjl.test.util.SseEmitterServer;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping(value = "/test")
public class TestController {
//sse連接接口
@GetMapping (value = "/sse/connect/{id}")
public SseEmitter connect(@PathVariable String id){
return SseEmitterServer.connect(id);
}
//sse向指定用戶發(fā)送消息接口
@GetMapping (value = "/sse/send/{id}")
public Map<String,Object> send(@PathVariable String id,@RequestParam(value = "message", required = false) String message){
Map<String,Object> returnMap = new HashMap<>();
//向指定用戶發(fā)送信息
SseEmitterServer.sendMessage(id,message);
returnMap.put("message","向id為"+id+"的用戶發(fā)送:"+message+"成功!");
returnMap.put("status","200");
returnMap.put("result",null);
return returnMap;
}
//sse向所有已連接用戶發(fā)送消息接口
@GetMapping (value = "/sse/batchSend")
public Map<String,Object> batchSend(@RequestParam(value = "message", required = false) String message){
Map<String,Object> returnMap = new HashMap<>();
//向指定用戶發(fā)送信息
SseEmitterServer.batchSendMessage(message);
returnMap.put("message",message+"消息發(fā)送成功!");
returnMap.put("status","200");
returnMap.put("result",null);
return returnMap;
}
//sse關閉接口
@GetMapping (value = "/sse/close/{id}")
public Map<String,Object> close(@PathVariable String id){
Map<String,Object> returnMap = new HashMap<>();
//移除id
SseEmitterServer.removeUser(id);
System.out.println("當前連接用戶id:"+SseEmitterServer.getIds());
returnMap.put("message","連接關閉成功!");
returnMap.put("status","200");
returnMap.put("result",null);
return returnMap;
}
}測試結果如下:
這里測試SSE連接,就像正常接口那樣請求就行。
本地調用接口/sse/connect/1如下:
這里我連接2個用戶,用來模擬向指定用戶id發(fā)送信息和批量向已連接的用戶發(fā)送消。


后端服務打印如下:

本地調用接口/sse/send/1如下:

用戶1的結果如下,發(fā)現(xiàn)它收到了消息:

用戶2沒有收到結果,如下:

本地調用接口/sse/batchSend如下:
批量向所有已經(jīng)連接的用戶發(fā)送消息。

用戶1結果如下,發(fā)現(xiàn)接收到了消息:

用戶2結果如下,發(fā)現(xiàn)也接收到了消息:

測試結果都符合預期。
點擊postman的close按鈕,關閉連接:


發(fā)現(xiàn)前端連接雖然關閉了,但是后端實際還在連接中,根本沒有移除用戶的提示:

所以這里還需要自己手動寫關閉接口測試。
本地調用接口/sse/close/1如下:

可以看到把用戶id為1的給移除了,只剩用戶2還在連接中。

這里所有測試完成,結果符合預期。
注意
將超時時間由原來的0改為默認的30秒,會報錯。

測試結果如下:


這里直接出現(xiàn)了一個異常:org.springframework.web.context.request.async.AsyncRequestTimeoutException
甚至連接都斷開了。
將springboot降為低版本如1.4.2.RELEASE。
使用postman進行測試的時候,發(fā)現(xiàn)它不是一直在請求中:如下:
將Springboot降為1.4.2.RELEASE

springboot的1.4.2.RELEASE版本沒有onError方法,需要注釋掉。

postman測試如下:
低版本測試的時候發(fā)現(xiàn)它有一個這個連接可以直接看到,而使用springboot版本2.x版本就發(fā)現(xiàn)它一直處于發(fā)送請求的狀態(tài),什么時候后端向前端發(fā)送了消息,它就顯示這個。
springboot的1.4.2.RELEASE版本結果:

springboot的2.7.3版本結果:

這里先將這種情況先記錄下來先,等后面有時間再研究。怎么高版本就不能向低版本那樣返回這個連接信息呢?所以SpringBoot高版本使用SSE連接的時候一直處于Sending request這種情況,這種情況是正常的嗎?
到此這篇關于SpringBoot使用SSE進行實時通知前端的文章就介紹到這了,更多相關SpringBoot實時通知前端內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Maven項目如何在pom文件中引入lib下的第三方jar包并打包進去
在使用Maven進行項目開發(fā)時,引入第三方私有的Jar包可能會遇到問題,一種常見的解決方案是將Jar包添加到項目的lib目錄,并通過IDE進行配置,但這需要每個開發(fā)者單獨操作,效率低下,更好的方法是通過Maven的pom.xml文件管理這些Jar包2024-09-09
JAVA8獲取list集合中重復的元素與獲取去重數(shù)據(jù)實例
這篇文章主要給大家介紹了關于JAVA8獲取list集合中重復的元素與獲取去重數(shù)據(jù)的相關資料,在實際開發(fā)中經(jīng)常會遇到需要找出(刪除)一個list中某些元素的屬性相同的元素,需要的朋友可以參考下2023-07-07
@Transaction,@Async在同一個類中注解失效的原因分析及解決
這篇文章主要介紹了@Transaction,@Async在同一個類中注解失效的原因分析及解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12
Spring中的@ExceptionHandler注解統(tǒng)一異常處理詳解
這篇文章主要介紹了Spring中的@ExceptionHandler注解統(tǒng)一異常處理詳解,當我們使用這個@ExceptionHandler注解時,定義一個異常的處理方法,加上@ExceptionHandler注解,這個方法就會處理類中其他方法拋出的異常,需要的朋友可以參考下2024-01-01
9個小技巧讓你的Java if else看起來更優(yōu)雅
這篇文章主要給大家介紹了9個小技巧,通過這幾個小技巧可以讓你的Java if else看起來更優(yōu)雅,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-11-11
Spring擴展點之BeanFactoryPostProcessor詳解
這篇文章主要介紹了Spring擴展點之BeanFactoryPostProcessor詳解,Spring的設計非常優(yōu)雅,有很多的擴展點供我們對項目進行擴展,今天學習一下Spring其中擴展點之一的BeanFactoryPostProcessor,需要的朋友可以參考下2023-11-11

