SSE: уведомления о новых постах
Не всегда нужен двусторонний WebSocket. Если данные идут только от сервера к клиенту — новые посты, уведомления, ход долгой задачи, — хватит Server-Sent Events (SSE). Это обычный HTTP-ответ, который не заканчивается: сервер держит соединение открытым и время от времени дописывает в него события. В браузере SSE принимает встроенный класс EventSource, который сам переподключается при обрыве.
Формат событий
Каждое событие — несколько строк текста и пустая строка в конце:
event: post
data: {"id":3,"title":"Живое обновление"}
event — имя события, data — данные. Javalin формирует эти строки сам.
Класс со списком клиентов
Сделаем так, чтобы при создании поста все подключённые клиенты получали событие post:
// src/main/java/com/example/blog/PostEvents.java
package com.example.blog;
import io.javalin.http.sse.SseClient;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
public class PostEvents {
private final Queue<SseClient> clients = new ConcurrentLinkedQueue<>();
public void connect(SseClient client) {
client.keepAlive();
client.onClose(() -> clients.remove(client));
clients.add(client);
}
public void published(Post post) {
for (SseClient client : clients) {
client.sendEvent("post", post);
}
}
}SseClient — подключённый клиент. Без keepAlive() Javalin закроет соединение, как только метод connect закончится. sendEvent отправляет событие с именем и данными; объект Post превращается в JSON.
Подключение
Передайте PostEvents в контроллер и вызовите его после сохранения поста:
// src/main/java/com/example/blog/PostController.java
public PostController(PostRepository repo, PostEvents events) {
this.repo = repo;
this.events = events;
}
public void create(Context ctx) {
NewPost data = ctx.bodyValidator(NewPost.class)
// ... правила check
.get();
Post post = repo.create(data);
events.published(post);
ctx.status(HttpStatus.CREATED).json(post);
}В Main создайте объект и маршрут:
// src/main/java/com/example/blog/Main.java
var events = new PostEvents();
var posts = new PostController(repo, events);
// внутри Javalin.create(config -> { ... })
config.routes.sse("/events", events::connect);Проверка
В первом терминале подпишитесь на события. Ключ -N выключает буферизацию, чтобы строки появлялись сразу:
curl -N -H "Accept: text/event-stream" http://localhost:8080/eventsВо втором терминале создайте пост:
curl -u anna:changeme -X POST http://localhost:8080/posts \
-d '{"title":"Живое обновление","body":"Пришло через SSE.","author":"Анна"}'В первом терминале тут же появится:
event: post
data: {"id":3,"title":"Живое обновление","body":"Пришло через SSE.","author":"Анна"}
В браузере то же делается в две строки: const source = new EventSource("/events"); и source.addEventListener("post", e => console.log(JSON.parse(e.data)));.
Частая ошибка
Javalin отвечает потоком событий, только если клиент прислал заголовок Accept: text/event-stream. EventSource в браузере делает это сам, а в curl его легко забыть. Без заголовка соединение сразу закрывается пустым ответом:
curl -i http://localhost:8080/eventsHTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 0