在Spring中使用事件机制

在Spring中使用事件机制

1. 事件机制

事件机制的底层设计模式是观察者模式,观察者设计模式定义了对象间的一种一对多的组合关系,以便一个对象的状态发生变化时,所有依赖于它的对象都得到通知并自动刷新,它能够将观察者和被观察者之间进行解耦。本文不具体介绍观察者模式,读者可以从其他文章进行查阅。

使用事件机制能够应对很多场景,它能够对系统业务逻辑之间的解耦。例如Swing中抽象出很多事件例如鼠标点击、键盘键入等,它通过事件派发线程不断从事件队列中获取事件并调用事件监听器的事件处理方法来处理事件。

再举一个实际业务中会存在的场景,我们有一个方法TradeService进行用户交易操作,此时有一个用户支付订单成功的方法paySuccess。当用户支付成功时,会有很多操作,比如向库存发送通知,如下述代码(我们对其进行简化,仅传入orderId):

public class TradeServiceImpl implements TradeService {

    /**
     * 支付成功相关操作
     * @param orderId 订单号
     */
    @Override
    public void paySuccess(Long orderId) {
        Preconditions.checkNotNull(orderId);
        
        // 1.修改订单状态为支付成功

        // 2.告知仓库准备发货

    }

}


但是此时产品经理需要我们在支付成功之后能够通过短信告诉用户可以进行抽奖,那我们仍需要修改该类。每次添加新功能的时候都需要修改原有的类,难以维护,这违反了设计模式的单一职责原则开闭原则。我们可以使用事件机制,将支付操作和其他支付之后的相关操作进行分离,通过事件来解耦。下文将围绕该业务通过事件进行改造。
(注:实际上该应用场景因为通常是分布式场景所以我们需要通过消息中间件例如Kafka进行异步处理,本文由于介绍Spring内置事件机制所以利用本地事件进行简化)

2. Spring原生事件驱动模型

Spring提供了一些内置的事件供开发者使用:

事件名称 概述
ContextRefreshedEvent 该事件会在ApplicationContext被初始化或者更新时发布。也可以在调用ConfigurableApplicationContext 接口中的refresh()方法时被触发。
ContextStartedEvent 当容器调用ConfigurableApplicationContext的Start()方法开始/重新开始容器时触发该事件。
ContextStoppedEvent 当容器调用ConfigurableApplicationContext的Stop()方法停止容器时触发该事件。
ContextClosedEvent 当ApplicationContext被关闭时触发该事件。容器被关闭时,其管理的所有单例Bean都被销毁。
RequestHandledEvent 在Web应用中,当一个http请求(request)结束触发该事件。

2.1 同步方式

  • 创建事件

首先我们先创建相关的事件,本场景下我们需要围绕支付成功进行相关操作,所以我们需要创建支付成功事件PaySuccessEvent,相关代码如下所示:

@Data
public class PaySuccessEvent extends ApplicationEvent {

    /**
     * 订单ID
     */
    Long orderId;

    public PaySuccessEvent(Object source, Long orderId) {
        super(source);
        this.orderId = orderId;
    }
}

  • 发布事件

发布事件我们可以通过ApplicationContext或者ApplicationEventPublisher进行发布,但是如果我们只需要进行发布事件,我们只需要创建一个类实现ApplicationEventPublisherAware接口,通过回调方法,使其能够获得ApplicationEventPublish,该接口的publishEvent方法能够对事件进行发布:

public interface ApplicationEventPublisherAware extends Aware {

    /**
     * Set the ApplicationEventPublisher that this object runs in.
     * <p>Invoked after population of normal bean properties but before an init
     * callback like InitializingBean's afterPropertiesSet or a custom init-method.
     * Invoked before ApplicationContextAware's setApplicationContext.
     * @param applicationEventPublisher event publisher to be used by this object
     */
    void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher);

}

我们常用的ApplicationContext也是继承自该接口:

public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,
        MessageSource, ApplicationEventPublisher, ResourcePatternResolver

若需要获取ApplicationContext我们只需要实现ApplicationContextAware接口通过其回调方法即可设置上下文环境。

我们创建一个事件发布的类:

public class EventPublisher implements ApplicationEventPublisherAware {

    public static ApplicationEventPublisher applicationEventPublisher;

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        EventPublisher.applicationEventPublisher = applicationEventPublisher;
    }

    public static void publishEvent(ApplicationEvent applicationEvent) {
        applicationEventPublisher.publishEvent(applicationEvent);
    }
}

然后我们对支付成功事件进行发布,如下所示:

public class TradeServiceImpl implements TradeService {

    /**
     * 支付成功相关操作
     * @param orderId 订单号
     */
    @Override
    public void paySuccess(Long orderId) {
        Preconditions.checkNotNull(orderId);
        EventPublisher.publishEvent(new PaySuccessEvent(this, orderId));
    }

}
  • 事件监听

我们可以使用EventListener注解或者实现ApplicationListener接口来对事件进行监听。

  • 1. 使用EventListener注解

@Component
public class PaySuccessEventListener {

    /**
     * 修改订单状态
     * @param paySuccessEvent 支付成功事件
     */
    @EventListener
    public void modifyOrderStatus(PaySuccessEvent paySuccessEvent)
    {
        System.out.println("修改订单状态成功!orderId:" + paySuccessEvent.getOrderId());
    }

    /**
     * 发送短信告知用户进行抽奖
     * @param paySuccessEvent 支付成功事件
     */
    @EventListener
    public void sendSMS(PaySuccessEvent paySuccessEvent)
    {
        System.out.println("发送短信成功!orderId:" + paySuccessEvent.getOrderId());
    }


}

  • 2. 实现ApplicationListener接口

public class PaySuccessListener implements ApplicationListener<PaySuccessEvent> {

    @Override
    public void onApplicationEvent(PaySuccessEvent event) {
        System.out.println("告知仓库准备发货成功!" + event.getOrderId());
    }

}

2.2 异步方式

Spring通过ApplicationEventMulticaster提供异步侦听事件的方式,但是注册 ApplicationEventMulticaster Bean 后所有的事件侦听处理都会变成的异步形式,如果需要针对特定的事件侦听采用异步方式的话:可以使用@EnableAsync和@Async组合来实现。
我们可以通过@EnableAsync注解开启异步方式,之后我们在需要异步监听的方法上加上@Async注解,这样能够使得发布事件时,发布方能异步调用监听器,主方法不会阻塞。以下是EnableAsync的部分注释:

 * <p>By default, Spring will be searching for an associated thread pool definition:
 * either a unique {@link org.springframework.core.task.TaskExecutor} bean in the context,
 * or an {@link java.util.concurrent.Executor} bean named "taskExecutor" otherwise. If
 * neither of the two is resolvable, a {@link org.springframework.core.task.SimpleAsyncTaskExecutor}
 * will be used to process async method invocations. Besides, annotated methods having a
 * {@code void} return type cannot transmit any exception back to the caller. By default,
 * such uncaught exceptions are only logged.
 
  * <p>To customize all this, implement {@link AsyncConfigurer} and provide:
 * <ul>
 * <li>your own {@link java.util.concurrent.Executor Executor} through the
 * {@link AsyncConfigurer#getAsyncExecutor getAsyncExecutor()} method, and</li>
 * <li>your own {@link org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler
 * AsyncUncaughtExceptionHandler} through the {@link AsyncConfigurer#getAsyncUncaughtExceptionHandler
 * getAsyncUncaughtExceptionHandler()}
 * method.</li>
 * </ul>

简而言之就是Spring默认情况下会先搜索TaskExecutor或者名称为taskExecutor的Executor类型的bean,若都不存在那么Spring会用SimpleAsyncTaskExecutor去执行异步方法。此外我们还可以通过实现AsyncConfigurer接口去自定义异步配置。

我们新建一个异步配置类AsyncConfig,使其具备异步能力:

@Configuration
@EnableAsync
@Slf4j
public class AsyncConfig implements AsyncConfigurer {

    @Override
    @Bean(name = "taskExecutor")
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(5);
        taskExecutor.setMaxPoolSize(10);
        taskExecutor.setQueueCapacity(25);
        taskExecutor.initialize();
        return taskExecutor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new MyAsyncExceptionHandler();
    }

    /**
     * 自定义异常处理类
     */
    class MyAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {

        //手动处理捕获的异常
        @Override
        public void handleUncaughtException(Throwable throwable, Method method, Object... obj) {
            System.out.println("------catched exception!------");
            log.info("Exception message - " + throwable.getMessage());
            log.info("Method name - " + method.getName());
            for (Object param : obj) {
                log.info("Parameter value - " + param);
            }
        }
    }
}

在开启异步化之后,我们只需要在监听方法上添加@Async注解就可以实现事件的异步调用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,110评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,443评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,474评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,881评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,902评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,698评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,418评论 3 419
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,332评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,796评论 1 316
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,968评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,110评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,792评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,455评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,003评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,130评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,348评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,047评论 2 355