Spring应用事件机制实践

0 阅读4分钟

1-简介

Spring Application Event是Spring框架自带的进程内事件发布、订阅机制,可用于:

  1. 监听Spring生命周期事件,以扩展Spring应用;
  2. 自定义组件解耦&依赖倒置;
  3. 实现Domain-Driven-Development(DDD),即领域(事件)驱动开发;

2-如何使用Application Event

2.1-订阅事件的两种方式

2.1.1-实现ApplicationListener<EventType>接口

代码示例:
public class MyClass implements ApplicationListener<CustomerApplicationEvent> {
    @Override
    public void onApplicationEvent(CustomerApplicationEvent event) {
        //...
    }
}
优点:
  1. 应用全生命周期有效;
缺点:
  1. 事件必须继承自ApplicationEvent抽象类;
  2. 监听器必须实现ApplicationEventListener<EventType>接口;
  3. 一个监听器只能监听一类事件或所有事件,不能有选择的监听部分事件;

2.1.2-使用@EventListener注解

代码示例:
public class AnnotationListenerService {

    @EventListener(ContextClosedEvent.class)
    public void handleContextClosedEvent() {
        log.info("ContextClosedEvent without method parameter");
    }

    @EventListener
    public void handleAllEvent(CustomerApplicationEvent event) {
        log.info("CustomerApplicationEvent: {}", event);
    }

    @EventListener(condition = "#event.eventType.name() == 'Add'")
    public void handleAddEvent(CustomerApplicationEvent event) {
        log.info("CustomerApplicationEvent Add: {}", event);
    }

    @EventListener(condition = "#event.eventType.name() == 'Delete'")
    public void handleDeleteEvent(CustomerApplicationEvent event) {
        log.info("CustomerApplicationEvent Delete: {}", event);
    }
}
优点:
  1. 事件和监听器无须继承/实现其它类或接口,侵入性更低;
  2. 支持监听多种事件;
  3. 支持按条件筛选事件;
  4. 只须增加一个注解,即可实现异步监听;
缺点:
  1. 无法监听启动阶段/Bean初始化阶段的事件;

2.2-发布事件

public class EventPublishService {
    // 1-注入`ApplicationEventPublisher`
    private final ApplicationEventPublisher applicationEventPublisher;

    public EventPublishService(ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
        // 2.1-初始化阶段即发布事件
        applicationEventPublisher.publishEvent(new CustomerApplicationEvent(this, EventType.Add, "Init"));
    }

    public void publish(String message) {
        // 2.2-发布继承自ApplicationEvent的事件
        applicationEventPublisher.publishEvent(new CustomerApplicationEvent(this, EventType.Update, message));
        // 2.3-发布任意事件
        applicationEventPublisher.publishEvent(new CustomerEvent(EventType.Update, message));
    }
}

3-应用案例

3.1-优雅停机

订阅ContextClosedEvent事件,并在订阅代码中关闭待资源并等待其得到释放:

public class NameListService {
    // ...
    // 延迟执行线程池
    ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

    @EventListener(ContextClosedEvent.class)
    public void onApplicationClosed() {
        // 服务停止时,关闭并等待线程池结束
        scheduler.awaitTermination(10, TimeUnit.SECONDS);
    }
    // ...
}

3.2-组件解耦/适配

以下示例展示了如何扩展Spring Caffeine以实现分布式缓存:

第二步:通过CaffeineCache包装器,发布缓存更新事件

public class CaffeineCacheWithEventDecorator<K, V> implements Cache<K, V>, CacheLocalOperations<K> {
    private final String name;
    private final Cache<@NonNull K, V> cache;
    private final ApplicationEventPublisher eventPublisher;

    public CaffeineCacheWithEventDecorator(String name, Cache<@NonNull K, V> cache, ApplicationEventPublisher eventPublisher) {
        this.name = name;
        this.cache = cache;
        this.eventPublisher = eventPublisher;
    }

    /// 重点1: 重载以下方法、委托给被包装缓存,并发布事件
    @Override
    public void invalidate(K key) {
        cache.invalidate(key);
        eventPublisher.publishEvent(CacheChangedEvent.builder().type(CacheChangedEvent.ChangeType.Evict).cacheName(name).keys(List.of(key)).build());
    }

    @Override
    public void invalidateAll(@NonNull Iterable<? extends K> keys) {
        cache.invalidateAll(keys);
        List<K> keyList = new ArrayList<>();
        keys.forEach(keyList::add);
        eventPublisher.publishEvent(CacheChangedEvent.builder().type(CacheChangedEvent.ChangeType.Evict).cacheName(name).keys(keyList).build());
    }

    @Override
    public void invalidateAll() {
        cache.invalidateAll();
        eventPublisher.publishEvent(CacheChangedEvent.builder().type(CacheChangedEvent.ChangeType.Evict).cacheName(name).build());
    }

    @Override
    public void cleanUp() {
        cache.cleanUp();
        eventPublisher.publishEvent(CacheChangedEvent.builder().type(CacheChangedEvent.ChangeType.Evict).cacheName(name).build());
    }

    /// 重点2: 以下是不发通知本地缓存操作方法,用于收到通知后刷新本地缓存、以避免事件无限循环
    public void localEvict(Collection<? extends K> keys) {
        cache.invalidateAll(keys);
    }

    public void localEvictAll() {
        cache.invalidateAll();
    }

    // 其它方法直接委托给cache
    // ...
}

异步缓存也需要写个包装器,只需重载返回同步缓存的方法即可:

public class CaffeineAsyncCacheWithEventDecorator<K, V> implements AsyncCache<@NotNull K, V> {
    private final String name;
    private final AsyncCache<@NotNull K, V> cache;
    private final ApplicationEventPublisher eventPublisher;

    public CaffeineAsyncCacheWithEventDecorator(String name, AsyncCache<@NotNull K, V> cache, ApplicationEventPublisher eventPublisher) {
        this.name = name;
        this.cache = cache;
        this.eventPublisher = eventPublisher;
    }

    @Override
    public @NotNull CaffeineCacheWithEventDecorator<@NotNull K, V> synchronous() {
        Cache<@NotNull K, V> c = cache.synchronous();
        if (c instanceof CaffeineCacheWithEventDecorator) {
            return (CaffeineCacheWithEventDecorator<K, V>) c;
        } else {
            return new CaffeineCacheWithEventDecorator<>(name, c, eventPublisher);
        }
    }
    // 其它方法直接委托给cache
    // ...
}

第二步:使用CacheManagerCustomizer自定义缓存的初始化过程,以便注册包装器


@Slf4j
public class CaffeineCacheManagerCustomizer implements CacheManagerCustomizer<CaffeineCacheManager> {
    public static final String PROPERTIES_PREFIX = "spring.cache.caffeine";
    private final CaffeineCacheProperties properties;
    private final ApplicationEventPublisher eventPublisher;

    public CaffeineCacheManagerCustomizer(CaffeineCacheProperties properties, ApplicationEventPublisher eventPublisher) {
        this.properties = properties;
        this.eventPublisher = eventPublisher;
    }

    @Override
    public void customize(CaffeineCacheManager cacheManager) {
        if (properties.caches == null) {
            return;
        }
        for (CaffeineCacheDefine define : properties.caches) {
            Caffeine<@NotNull Object, @NotNull Object> caffeine = Caffeine.from(define.spec);
            if (properties.isAsyncCacheMode()) {
                final AsyncCache<Object, Object> cache = caffeine.buildAsync();
                final CaffeineAsyncCacheWithEventDecorator<Object, Object> decorator = new CaffeineAsyncCacheWithEventDecorator<>(define.name, cache, eventPublisher);
                cacheManager.registerCustomCache(define.name, decorator);
            } else {
                final Cache<Object, Object> cache = caffeine.build();
                final CaffeineCacheWithEventDecorator<Object, Object> decorator = new CaffeineCacheWithEventDecorator<>(define.name, cache, eventPublisher);
                cacheManager.registerCustomCache(define.name, decorator);
            }
            log.info("已配置分布式缓存:{}", define.name);
        }
    }

    @Data
    @ConfigurationProperties(PROPERTIES_PREFIX)
    public static class CaffeineCacheProperties {
        private boolean asyncCacheMode = false;
        private boolean allowNullValues = true;
        private List<CaffeineCacheDefine> caches;
    }

    @Data
    public static class CaffeineCacheDefine {
        private String name;
        private String spec;

    }
}

第三步:使用Ignite之类网格框架,实现分布式缓存同步


@Slf4j
public class IgniteSyncCaffeineCacheManagerCustomizer implements CacheManagerCustomizer<CaffeineCacheManager> {
    public static final String IGNITE_MESSAGE_TOPIC = "event.spring.cache.changed";
    private final Ignite ignite;
    private final JsonMapper jsonMapper;
    private final ApplicationEventPublisher applicationEventPublisher;
    private CaffeineCacheManager cacheManager;

    public IgniteSyncCaffeineCacheManagerCustomizer(Ignite ignite, JsonMapper jsonMapper, ApplicationEventPublisher applicationEventPublisher) {
        this.ignite = ignite;
        this.jsonMapper = jsonMapper;
        this.applicationEventPublisher = applicationEventPublisher;
        //注册ignite的消息
        ignite.message().localListen(IGNITE_MESSAGE_TOPIC, this::onIgniteChangeMessage);
    }

    /// 优雅停机:停机时移除事件监听
    @EventListener(ContextClosedEvent.class)
    public void onApplicationClose(@SuppressWarnings("unused") ContextClosedEvent event) {
        ignite.message().stopLocalListen(IGNITE_MESSAGE_TOPIC, this::onIgniteChangeMessage);
    }

    /// 本地缓存变更事件转换为分布式消息发送
    @Async
    @EventListener
    public void onCacheChangedEvent(CacheChangedEvent event) {
        log.trace("Receive CacheChangedEvent:{}", event);
        try {
            // 转为Ignite消息发送
            String message = cacheMessageToString(event);
            if (message != null) {
                ignite.message().send(IGNITE_MESSAGE_TOPIC, message);
            }
        } catch (Exception e) {
            log.error("Send Ignite Message failed, topic={}", IGNITE_MESSAGE_TOPIC, e);
        }
    }

    /// 收到分布式消息时,更新本地缓存
    private boolean onIgniteChangeMessage(UUID nodeId, Object message) {
        //跳过非字符串消息
        if (!(message instanceof String strMsg)) {
            log.trace("Ignore non string message:{}", message);
            return true;
        }
        log.trace("Receive Ignite message from node=[{}],cache changed:[{}]", nodeId, strMsg);

        // 解析缓存消息
        CacheChangedEvent objMsg = parseCacheMessage(strMsg);
        //跳过无效消息
        if (objMsg == null || objMsg.getCacheName() == null || objMsg.getType() == null) {
            return true;
        }

        //跳过自己发送的通知
        if (!nodeId.equals(ignite.cluster().localNode().id())) {
            log.trace("Ignore self send Ignite message from node[{}]", nodeId);
            // 更新本地缓存
            updateLocalCache(objMsg);
        }

        // 发送分布式缓存更新系统事件
        sendDistributedCacheChangedEvent(objMsg);

        return true;
    }

    /// 通过CacheManagerCustomizer获取到缓存管理器实例
    @Override
    public void customize(CaffeineCacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }

    // 具体消息序列化/反序列化、发送代码,不是重点
    // ...
}

参考

  1. Spring官方文档-可观测性-ApplicationEvent
  2. Spring官方文档-应用上下文