Spring应用事件机制实践

1-简介

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

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

2-如何使用Application Event

2.1-订阅事件的两种方式

2.1.1-实现ApplicationListener<EventType>接口

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

2.1.2-使用@EventListener注解

代码示例:
java 复制代码
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-发布事件

java 复制代码
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事件,并在订阅代码中关闭待资源并等待其得到释放:

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

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

3.2-组件解耦/适配

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

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

java 复制代码
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
    // ...
}

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

java 复制代码
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自定义缓存的初始化过程,以便注册包装器

java 复制代码
@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之类网格框架,实现分布式缓存同步

java 复制代码
@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官方文档-应用上下文
相关推荐
回家路上绕了弯8 分钟前
为什么 AI 写代码时,总喜欢“防御性编程”?
后端
灯澜忆梦12 分钟前
【Redis中间件】#4 | 进阶数据类型语法
redis·后端
广州山泉婚姻15 分钟前
前后端分离的核心优势
前端·后端
IT_陈寒34 分钟前
Vue的响应式让我加班到凌晨,问题竟出在这个不起眼的地方
前端·人工智能·后端
乱码三千1 小时前
使用 Docker-compose 搭建SRS直播中转服务端
后端·架构·github
y1su1 小时前
【Leetcode】1477. 找两个和为目标值且不重叠的子数组
数据结构·后端·算法·leetcode·职场和发展
愛芳芳1 小时前
Swagger2 多环境动态配置实战指南
java·后端·maven·springboot·swagger
JaguarJack2 小时前
PHP 8.6 只是个小版本,三十项弃用却已在为 PHP 9.0 铺路
后端·php·服务端
BingoGo2 小时前
PHP 8.6 只是个小版本,三十项弃用却已在为 PHP 9.0 铺路
后端·php
Bs_MoneyMagnet5 小时前
基于springboot+vue的滑雪场票务与装备租赁系统的设计与实现 源码+文档
java·vue.js·spring boot·后端·spring·毕业设计·计算机毕业设计