События CDI
Когда в блоге появляется запись, нужно сделать несколько дел: уведомить подписчиков, обновить поисковый индекс, отправить письма. Если вызывать всё это из сервиса публикации, он будет знать про каждую службу и разрастётся. События CDI решают задачу иначе: сервис только объявляет «запись создана», а все, кому это интересно, подписываются сами.
Событие
Событие — обычный объект с данными. Удобно описать его записью:
// src/main/java/com/example/blog/PostCreated.java
package com.example.blog;
public record PostCreated(long id, String title) {
}Отправка
Сервис получает объект Event<PostCreated> и вызывает fire. Обновите src/main/java/com/example/blog/PostService.java:
// src/main/java/com/example/blog/PostService.java
package com.example.blog;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Event;
import jakarta.inject.Inject;
@ApplicationScoped
public class PostService {
@Inject
PostStore store;
@Inject
Event<PostCreated> postCreated;
public Post publish(NewPost data) {
Post post = store.add(data);
postCreated.fire(new PostCreated(post.id(), post.title()));
return post;
}
}Здесь зависимости внедрены в поля — так короче, и конструктор без параметров не нужен.
Наблюдатели
Наблюдатель — метод бина, у которого параметр помечен @Observes:
// src/main/java/com/example/blog/NotificationService.java
package com.example.blog;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import java.util.logging.Logger;
@ApplicationScoped
public class NotificationService {
private static final Logger LOG = Logger.getLogger(NotificationService.class.getName());
void onPostCreated(@Observes PostCreated event) {
LOG.info("Уведомляем подписчиков о записи: " + event.title());
}
}Второй наблюдатель того же события ничего не знает о первом:
// src/main/java/com/example/blog/SearchIndex.java
package com.example.blog;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import java.util.logging.Logger;
@ApplicationScoped
public class SearchIndex {
private static final Logger LOG = Logger.getLogger(SearchIndex.class.getName());
void onPostCreated(@Observes PostCreated event) {
LOG.info("Добавляем запись " + event.id() + " в поисковый индекс");
}
}Создадим запись:
curl -H "Content-Type: application/json" -d '{"title":"События CDI","body":"Отправить и поймать."}' http://localhost:8080/blog/api/postsВ target/liberty/wlp/usr/servers/defaultServer/logs/messages.log появятся строки (дата сокращена):
com.example.blog.NotificationService I Уведомляем подписчиков о записи: События CDI
com.example.blog.SearchIndex I Добавляем запись 3 в поисковый индекс
fire вызывает наблюдателей сразу, в том же потоке: ответ клиенту уйдёт, когда все они закончат.
Асинхронные события
Долгую работу, например рассылку писем, лучше не делать во время запроса. Метод fireAsync отправляет событие в другой поток, а ловят его методы с @ObservesAsync:
// src/main/java/com/example/blog/EmailSender.java
package com.example.blog;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.ObservesAsync;
import java.util.logging.Logger;
@ApplicationScoped
public class EmailSender {
private static final Logger LOG = Logger.getLogger(EmailSender.class.getName());
void onPostCreated(@ObservesAsync PostCreated event) {
LOG.info("Рассылаем письма о записи " + event.id() + " в потоке "
+ Thread.currentThread().getName());
}
}В PostService после fire добавьте postCreated.fireAsync(event), сохранив событие в переменную event. В логе появится строка из другого потока:
com.example.blog.EmailSender I Рассылаем письма о записи 3 в потоке Default Executor-thread-101
| Отправка | Наблюдатель | Поток |
|---|---|---|
fire(event) | @Observes | Тот же, ответ ждёт |
fireAsync(event) | @ObservesAsync | Другой, ответ не ждёт |
Частая ошибка
Отправить событие через fireAsync, а наблюдателя пометить @Observes, или наоборот. Ошибки не будет — метод просто ни разу не вызовется. Синхронные и асинхронные наблюдатели не смешиваются: если событие должно дойти до обоих, вызовите и fire, и fireAsync, как в примере выше.