Spring Events

1. 概述

事件是框架中最容易被忽视的功能之一,但同时也是一个很有用的功能。像Spring其他特性一样,事件发布是ApplicationContext提供的功能之一。

事件通知是一个很有用的功能,使用事件机制可以将互相耦合的代码进行解耦,方便功能的新增或修改。

2. 自定义事件

Spring允许创建和发布自定义事件,默认情况下,事件都是同步执行的。这样有很多好处,比如事件的监听器和发布者在同一个事务内,能够很方便的处理一些业务。

2.1. 一个简单的Application Event

创建一个简单的事件类,使用一个String变量来存储事件数据。

public class CustomSpringEvent extends ApplicationEvent {

    @Getter
    private String message;

    public CustomSpringEvent(Object source, String message) {
        super(source);
        this.message = message;
    }

}

2.2. 事件发布者

现在来创建事件发布者。发布者创建事件对象,并把事件发送给所有的监听器。

要发布事件,发布者可以简单的注入ApplicationEventPublisher然后使用它的publishEvent()方法:

@Slf4j
@Component
public class CustomSpringEventPublisher {

    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    public void doStuffAndPublishAnEvent(final String message) {
        log.info("Publishing custom event. ");
        CustomSpringEvent customSpringEvent = new CustomSpringEvent(this, message);
        applicationEventPublisher.publishEvent(customSpringEvent);
    }

}

或者,发布者也可以实现ApplicationEventPublisherAware接口。通常情况下使用@Autowired注入会更简单。

2.3. 事件监听器

最后,来创建事件监听器。

监听器的唯一要求是一个bean并实现ApplicationListener接口:

@Slf4j
@Component
public class CustomSpringEventListener implements ApplicationListener<CustomSpringEvent> {

    @Override
    public void onApplicationEvent(CustomSpringEvent event) {
        log.info("Received spring custom event - {}", event.getMessage());
    }
    
}

上面已经说过,默认情况下事件都是同步执行的,在所有的监听器完成对事件的处理之前,doStuffAndPublishAnEvent() 方法会一直堵塞。

如果需要指定监听器的执行顺序,可以实现Ordered接口设置每个执行器的优先级。

3. 创建异步事件

在某些情况下,同步处理事件并不是我们想要的效果,我们可能需要异步处理事件。

AbstractApplicationContext中存在一个ApplicationEventMulticaster对事件进行广播,默认情况下框架初始化了一个SimpleApplicationEventMulticaster

/**
* Initialize the ApplicationEventMulticaster.
* Uses SimpleApplicationEventMulticaster if none defined in the context.
* @see org.springframework.context.event.SimpleApplicationEventMulticaster
*/
protected void initApplicationEventMulticaster() {
    ConfigurableListableBeanFactory beanFactory = getBeanFactory();
    if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) {
        this.applicationEventMulticaster =
            beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class);
    }
    else {
        this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
        beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster);

    }
}

SimpleApplicationEventMulticaster是如何进行事件广播的呢?

@Override
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
    ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
    Executor executor = getTaskExecutor();
    for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
        if (executor != null) {
            executor.execute(() -> invokeListener(listener, event));
        }
        else {
            invokeListener(listener, event);
        }
    }
}

如果存在Executor,那么事件就会进行异步处理,否则就是同步。

所以需要异步的处理事件,那么就需要手动创建一个name为applicationEventMulticaster的bean,然后为它设置一个TaskExecutor,例如:

@Configuration
public class AsynchronousSpringEventsConfig {

    @Bean(name = AbstractApplicationContext.APPLICATION_EVENT_MULTICASTER_BEAN_NAME)
    public ApplicationEventMulticaster simpleApplicationEventMulticaster() {
        SimpleApplicationEventMulticaster eventMulticaster = new SimpleApplicationEventMulticaster();
        eventMulticaster.setTaskExecutor(new SimpleAsyncTaskExecutor());
        return eventMulticaster;
    }

}

事件、发布者和监听器和前面定义相同,这样监听器就会在单独的线程中异步处理事件。

但是,这样配置的话,所有发布的事件都会以异步的方式进行处理,显然太简单粗暴了,下面介绍一种更加友好的方式。

首先,删除刚刚的AsynchronousSpringEventsConfig配置类,然后在Application主类上加上@EnableAsync注解,来让程序支持异步方法的调用,最后在监听器的onApplicationEvent方法上加上@Async注解。

@EnableAsync
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

@Slf4j
@Component
public class CustomSpringEventListener implements ApplicationListener<CustomSpringEvent> {

    @Async
    @Override
    public void onApplicationEvent(CustomSpringEvent event) {
        log.info("Received spring custom event - {}", event.getMessage());
    }

}

4. 基于注解的事件监听器

从Spring4.2开始,事件监听器不再需要实现ApplicationListener 接口,可以通过@EventListener注解在一个bean的任意public方法上注册:

@Slf4j
@Component
public class AnnotationDrivenContextStartedListener {

    @EventListener
    public void handleContextStartedEvent(ContextStartedEvent event) {
        log.info("Handling context started event.");
    }

}

如果方法需要监听多个事件或者你不想在方法上定义参数,那么你可以在注解里设置事件类型,例如:

@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
    // ...
}

注解的condition属性可以定义一个SpEL表达式来对事件进行过滤,表达式匹配才能调用特定事件的监听方法。

@EventListener(condition = "event.message == 'message'")
public void handleCustomSpringEvent(CustomSpringEvent event) {
    log.info("Received spring custom event - {}", event.getMessage());
}

如果处理完成一个事件后需要发布一个事件,那么你可以在方法上返回相应的事件,例如:

@EventListener
public ReturnEvent handleCustomSpringEvent(CustomSpringEvent event) {
    // 首先处理CustomSpringEvent事件
    // 处理完成后,发布ReturnEvent事件
}

如果需要返回多个事件,那么可以返回事件的集合。

4.1 异步监听器

监听器的异步处理可以通过@Async注解来实现。

@Async
@EventListener
public void handleCustomSpringEvent(CustomSpringEvent event) {
    // CustomSpringEvent会在一个独立的线程中进行处理
}

当使用异步事件监听时,需要注意以下限制:

  • 如果异步事件监听器抛出Exception,不会将其传播到调用方。可以查看AsyncUncaughtExceptionHandler来获取更多详细信息。
  • 异步事件监听方法无法通过返回值来发布后续事件。如果你确实需要发布后续事件,可以注入ApplicationEventPublisher来手动发布。

4.2 排序监听器

如果需要指定监听器的执行顺序,可以在方法上使用@Order注解。

@Order(18)
@EventListener
public void handleCustomSpringEvent(CustomSpringEvent event) {
    // ...
}

5. 泛型事件

不是所有的事件都必须继承ApplicationEventApplicationEventPublisher中有2种方式进行事件的发布。

@FunctionalInterface
public interface ApplicationEventPublisher {

    default void publishEvent(ApplicationEvent event) {
        publishEvent((Object) event);
    }

    void publishEvent(Object event);

}

通过查看实现类代码可以发现,发送的Object类型的事件最后会被包装为一个PayloadApplicationEvent,而PayloadApplicationEvent继承了ApplicationEvent

// Decorate event as an ApplicationEvent if necessary
ApplicationEvent applicationEvent;
if (event instanceof ApplicationEvent) {
    applicationEvent = (ApplicationEvent) event;
}
else {
    applicationEvent = new PayloadApplicationEvent<>(this, event);
    if (eventType == null) {
        eventType = ((PayloadApplicationEvent<?>) applicationEvent).getResolvableType();
    }
}

所以我们可以发布任何Object事件。

@Data
public class ObjectEvent {

    private String message;

    public ObjectEvent(String message) {
        this.message = message;
    }

}

// 发送事件
applicationEventPublisher.publishEvent(new ObjectEvent(message));

// 监听事件
@EventListener
public void handleObjectEvent(ObjectEvent event){
    log.info(event.getMessage());
}

6. 事务绑定事件

很多时候,只有事务提交之后我们才会发布相应的事件处理其他逻辑,比如用户注册之后,发送邮件或者短信。从Spring 4.2开始,框架提供了一个很方便的注解来实现此功能(4.2之前也可以通过自己写代码实现),即@TransactionalEventListener

@TransactionalEventListener是对@EventListener的一个扩展,允许将事件的监听器绑定到事务的某个阶段。可以绑定到以下事务阶段:

  • AFTER_COMMIT (默认),事务提交后
  • AFTER_ROLLBACK ,事务回滚后
  • AFTER_COMPLETION ,事务完成,包括提交后和回滚后
  • BEFORE_COMMIT ,事务提交前
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void handleTransactionalEvent(TransactionalEvent event) {
    log.info("Handling event inside a transaction BEFORE COMMIT.");
}

只有当上下文存在事务,并且事务提交前,才会调用此监听器的方法。

默认情况下,如果上下文不存在事务,则根本不会发送事件,我们可以通过设置@TransactionalEventListenerfallbackExecution为true来实现。

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

推荐阅读更多精彩内容

  • 1. 什么是事件监听机制 在讲解事件监听机制前,我们先回顾下设计模式中的观察者模式,因为事件监听机制可以说是在典型...
    habit_learning阅读 4,121评论 1 13
  • 前面我们讲到了Spring在进行事务逻辑织入的时候,无论是事务开始,提交或者回滚,都会触发相应的事务事件。本文首先...
    AI乔治阅读 1,725评论 0 1
  • 事件驱动模型简介 事件驱动模型也就是我们常说的观察者,或者发布-订阅模型;理解它的几个关键点: 1.首先是一种对象...
    algernoon阅读 1,656评论 0 4
  • Spring 框架事件收发功能的使用 (一) 基本使用 1. 概述 Spring 框架时间收发功能本系列的文章总共...
    云逸Dean阅读 794评论 0 0
  • 上午先生带着孩子去超市买菜,我还有演讲作业要练习就没有去,他们刚走了没多久。我听了下今天的晨读,关于减肥了和低糖食...
    利萍阅读 162评论 0 1