使用Spring Data的事件通知模式

48 阅读5分钟

使用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支持“简单”的事件驱动架构。