使用Spring Data的事件通知模式
在本文中,我们将使用Spring Data实现一个简单的事件通知模式。
当实体被更新、删除或持久化时,将发布一个事件以通知其他系统该更改。
我们还将通过合并DTO对象来增强通知过程,从而消除获取更新数据的需要。这种增强解决了事件通知模式相对于事件来源的一个缺点。
完整的应用程序代码可以在GitHub上找到。
1-实体倾听者
首先,我们使用 @ Listeners为实体指定侦听器类。下面是一个示例,其中Book实体被注释为通过BookNotityList类侦听生命周期事件:
@Entity
@Table(name = "books")
@EntityListeners(BookEntityListener.class)
public class Book extends AbstractEntity {
// fields
}
listener类本身是一个Springbean,用 @Component注释。它可以处理多个实体的事件,并使用统一的对象结构发布相应的事件。
@Component
@RequiredArgsConstructor
public class BookEntityListener {
private final ApplicationEventQueue applicationEventQueue;
@PostUpdate
public void postUpdate(AbstractEntity entity) {
switch (entity) {
case Book book -> publishBookEvent(book, OperationType.UPDATE);
default ->
log.error(...)
}
}
@PostRemove
public void postRemove(AbstractEntity entity) {
// ...
}
@PostPersist
public void postPersist(AbstractEntity entity) {
// ...
}
private void publishBookEvent(Book book, OperationType operationType) {
DataChangeEvent entityUpdated =
DataChangeEvent.builder()
.eventName("book")
.id(book.getId())
.operationType(operationType)
.databaseVersion(book.getDatabaseVersion())
.build();
applicationEventQueue.enqueue(entityUpdated);
}
}
2-事件队列
我们不直接发布事件,而是将它们排队,这有两个关键原因:
1)过滤同一事务中的重复事件:过滤单个事务中作用于同一实体的事件,确保仅发布最后一个事件。实体中的@Version字段在这里很有用。
2)在事务提交后发布事件:仅在事务成功提交到数据库后才发布事件。这避免了为可能回滚的更改发送事件的风险。
ApplicationEventQueue类使用ThreadLocal将事件安全地存储在当前线程中。
@Component
@Getter
@Setter
@Slf4j
public class ApplicationEventQueue {
private static final ThreadLocal<Set<DataChangeEvent>> events =
ThreadLocal.withInitial(HashSet::new);
public void enqueue(DataChangeEvent event) {
events.get().add(event);
}
public void clear() {
events.remove();
}
public Set<DataChangeEvent> consumeEvents() {
Set<DataChangeEvent> allEvents = filterByLatestDatabaseVersion(events.get());
this.clear();
return allEvents;
}
private Set<DataChangeEvent> filterByLatestDatabaseVersion(
Set<DataChangeEvent> dataChangeEventSet) {
// return events filtered;
}
}
虽然@RequestScope bean可以工作,但当在请求范围之外创建事件时,例如在@Async方法中创建事件时,它就会出现福尔斯不足。ThreadLocal提供了一个线程安全的替代方案,但需要小心管理。具体来说,我们必须在处理完存储的事件后清除它们,以避免在请求之间泄漏事件。
此外,Oracle建议不要将虚拟线程池化,这使得ThreadLocal在使用轻量级虚拟线程时是一个安全的选择。有关详细信息,请参阅Oracle的DONT POOL VIRTUAL THREADS文档。
3-线程本地清除拦截器
为了防止池化线程时的事件泄漏,我们在请求完成后清除ThreadLocal。这可以使用拦截器来完成:
@RequiredArgsConstructor
@Slf4j
public class ThreadLocalClearingInterceptor implements HandlerInterceptor {
private final ApplicationEventQueue applicationEventQueue;
@Override
public void afterCompletion(
HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
log.trace("Clearing thread local request events");
applicationEventQueue.clear(); // Clear ThreadLocal events after request completion
}
}
在WebMvcConfigurer中注册拦截器:
@Configuration
@RequiredArgsConstructor
@Slf4j
public class WebConfig implements WebMvcConfigurer {
private final ApplicationEventQueue applicationEventQueue;
@Override
public void addInterceptors(InterceptorRegistry registry) {
log.debug("WebConfig::addInterceptors()");
registry.addInterceptor(new ThreadLocalClearingInterceptor(applicationEventQueue));
}
}
4-使用事务同步的事件通知
TransactionSynchronizationAspect确保仅在事务提交后发送事件。同步逻辑避免为失败的事务发送事件。
这个方面围绕着所有用@ transmitted注释的方法进行操作,包括那些用@ transmitted注释的类中的方法。方面在事务提交之后立即注册事件通知。为每个事务注册一个新的同步。
@Aspect
@Component
@ConditionalOnProperty(value = "events.notification-enabled", havingValue = "true")
public class TransactionSynchronizationAspect {
private final ApplicationEventQueue applicationEventQueue;
private final ApplicationEventPublisher springEventPublisher;
@Before(
"execution(* (@org.springframework.transaction.annotation.Transactional *).*(..)) || "
+ "@annotation(org.springframework.transaction.annotation.Transactional)")
public void beforeWriteEndpoint(JoinPoint joinPoint) throws Throwable {
if (isReadOnlyTransaction(joinPoint)) {
return;
}
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
return;
}
boolean alreadyRegistered =
TransactionSynchronizationManager.getSynchronizations().stream()
.anyMatch(DataChangeEventSynchronization.class::isInstance);
if (alreadyRegistered) {
return;
}
TransactionSynchronizationManager.registerSynchronization(
new DataChangeEventSynchronization());
}
private boolean isReadOnlyTransaction(JoinPoint joinPoint) throws NoSuchMethodException {
Method method = getTargetMethod(joinPoint);
Transactional transactional = method.getAnnotation(Transactional.class);
if (transactional == null) {
transactional = joinPoint.getTarget().getClass().getAnnotation(Transactional.class);
}
return transactional != null && transactional.readOnly();
}
private Method getTargetMethod(JoinPoint joinPoint) throws NoSuchMethodException {
Method signatureMethod = ((MethodSignature) joinPoint.getSignature()).getMethod();
return joinPoint
.getTarget()
.getClass()
.getMethod(signatureMethod.getName(), signatureMethod.getParameterTypes());
}
private class DataChangeEventSynchronization implements TransactionSynchronization {
private void publishEvents() {
Set<DataChangeEvent> eventsToPublish = applicationEventQueue.consumeEvents();
if (CollectionUtils.isEmpty(eventsToPublish)) {
return;
}
for (DataChangeEvent event : eventsToPublish) {
springEventPublisher.publishEvent(event);
}
}
@Override
public void afterCommit() {
publishEvents();
}
}
}
由于方面在事务提交之后执行,因此它确保只有在事务成功时才发送事件。如果失败,则不会发送事件,并且回滚更改。
5-带有DTO响应的事件通知
这种增强是通过ResponseBodyAdvice实现的,它在响应体发送到客户端之前拦截它。由于事件是在事务提交之后发送的,因此可以保证数据在数据库中持久化。
@ControllerAdvice
@RequiredArgsConstructor
@ConditionalOnProperty(value = "events.notification-response-enabled", havingValue = "true")
public class EventNotificationResponseBodyAdvice implements ResponseBodyAdvice<Object> {
private final ApplicationEventQueue applicationEventQueue;
private final ApplicationEventPublisher springEventPublisher;
@Override
public boolean supports(
MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@Override
public Object beforeBodyWrite(
Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
if (isWriteMethod(request.getMethod())) {
try {
this.publishEvents(body);
} catch (Exception e) {
log.error("Error while sending spring events", e);
}
}
return body;
}
private void publishEvents(Object body) {
Set<DataChangeEvent> eventsToPublish = applicationEventQueue.consumeEvents();
if (eventsToPublish.isEmpty()) {
return;
}
for (DataChangeEvent event : eventsToPublish) {
event.setBody(body);
springEventPublisher.publishEvent(event);
}
}
}
6-使用事件
DataChangeEvent事件可以使用@ EventEvent注释来使用。通过添加@Async,事件侦听器异步运行,避免阻塞主线程。
@Service
@RequiredArgsConstructor
@Slf4j
public class UpdateEventListener {
@Async
@EventListener
public void on(DataChangeEvent event) {
// do something with the event, e.g. update a cache, use a message broker, etc.
}
}j
一旦事件被使用,它就可以触发根据应用程序需求定制的各种操作。
此外,Spring Modulith提供了一种无缝的方式来外部化事件。要了解有关使用Spring Modulith外部化事件的更多信息,请查看使用Spring Modulith的简化事件外部化以获取更多详细信息。
7-备选方案和结论
Debezium是一个开源平台,它实现了变更数据捕获(CDC)模式,直接从数据库事务日志捕获实时变更。Debezium支持“简单”的事件驱动架构。