开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 19 天,点击查看活动详情
有时可能需要向用户发送实时数据,例如前端应用程序中的通知。为此,您可以使用以下简单方法,使用来自 Project Reactor 的 Flux 和来自 Spring Boot 的服务器发送事件。
首先,您需要创建一个类来存储通知。为简单起见,它只有两个字段:id 和 message。
public class Notification {
private final Integer id;
private final String message;
public Notification(Integer id, String message) {
this.id = id;
this.message = message;
}
public Integer getId() {
return id;
}
}
接下来,我们将创建一个管理订阅和通知的服务。我们将通过用户 id 来确定谁拥有通知。我们将为每个通知生成一个随机 ID。让我们将随机数限制在区间 [0, 3] 内。
private Integer generateId() {
return RANDOM.nextInt(4);
}
我们将每 2 秒生成一次简单的通知,而不是真实数据。
private ServerSentEvent<Notification> generateNotification() {
return ServerSentEvent.<Notification>builder()
.data(new Notification(generateId(), "Notification"))
.build();
}
private void generateNotifications(FluxSink<ServerSentEvent<Notification>> sink) {
Flux.interval(Duration.ofSeconds(2)) // Generate simple notifications every 2 seconds.
.map(i -> generateNotification())
.doOnNext(serverSentEvent -> {
sink.next(serverSentEvent); // Sending notifications to the global Flux via its FluxSink
log.info("Sent for {}", serverSentEvent.data().getId());
})
.doFinally(signalType -> log.info("Notification flux closed")) // Logging the closure of our generator
.takeWhile(notification -> !sink.isCancelled()) // We generate messages until the global Flux is closed
.subscribe();
}
如果我们立即从生成器开始发送通知,我们可能会遇到超时问题和断开连接。目前,通知每 2 秒创建一次,但在真实系统中,通知可以在几分钟甚至几小时内创建。如果在一定时间内(通常是几分钟)没有数据发送到打开的连接,它将自动关闭。
为了避免这种情况,让我们创建一个心跳——一个额外的空评论流,它将被发送到打开的连接以保持它打开。Spring 本身会负责关闭它并自动关闭我们的心跳。
private <T> Flux keepAlive(Duration duration, Flux<T> data, Integer id) {
Flux<ServerSentEvent<T>> heartBeat = Flux.interval(duration) // Создаем Flux с определенным интервалом
.map(
e -> ServerSentEvent.<T>builder() /Create a new SSE object with a comment and an empty body
.comment(“keep alive for: “ + id)
.build())
.doFinally(signalType -> log.info(“Heartbeat closed for id: {}”, id));
return Flux.merge(heartBeat, data);
}
现在让我们编写一个简单的方法来通过 id 订阅通知。
public Flux<ServerSentEvent<Notification>> subscribe(int id) {
return keepAlive(Duration.ofSeconds(3),
notificationFlux.filter(notification -> notification.data() == null ||
notification.data().getId() == id), id);
}
让我们在服务构造函数中创建一个全局 Flux。
private final Flux<ServerSentEvent<Notification>> notificationFlux;
public NotificationService () {
notificationFlux = Flux.push(这::generateNotifications);
}
现在让我们创建一个 RestController 和一个用于订阅通知的简单端点。
@RestController
public class NotificationController {
private final NotificationService notificationService;
@Autowired
public NotificationController(NotificationService notificationService) {
this.notificationService = notificationService;
}
@GetMapping("/subscribe/{id}")
public Flux<ServerSentEvent<Notification>> subscribe(@PathVariable Integer id) {
return notificationService.subscribe(id);
}
}
如果您在浏览器中打开http://localhost:8080/subscribe/1,您将获得以下输出。现在您可以停止加载页面或关闭它。
:keep alive for: 1
:keep alive for: 1
data:{"id":1,"message":"Notification"}
:keep alive for: 1
:keep alive for: 1
data:{"id":1,"message":"Notification"}
您可以使用内置的 JavaScript 工具或使用第三方库在前端应用程序上接收通知。我使用sse.js和 JQuery。
<!DOCTYPE HTML>
<html>
<head>
<script src="jquery.min.js"></script>
</head>
<body>
<h1>Watcher</h1>
<div class="container"></div>
<script src="sse.js"></script>
<script>
window.onload = function () {
const source = new SSE("http://localhost:8080/subscribe/1");
source.addEventListener('message', function (e) {
if (e.data) {
const payload = JSON.parse(e.data);
$(".container").append('<p>' + payload.action + '</p>')
}});
source.stream();
};
</script>
</body>
</html>