从零开始 Spring Boot 41:事件

news/2024/11/7 5:39:19/

从零开始 Spring Boot 41:事件

spring boot

图源:简书 (jianshu.com)

Spring 实现了一个简单、实用的事件框架,利用它我们可以在多个组件之间进行松耦合式的通信。

简单示例

让我们从一个简单的示例开始:

public record Email(String address, String content) {
}public class EmailsReceivedEvent extends ApplicationEvent {@Setter@Getterprivate List<Email> emails = Collections.emptyList();public EmailsReceivedEvent(Object source) {super(source);}
}

这里的Email表示一个电子邮件,EmailsReceivedEvent表示我们的系统收到电子邮件后在内部会发布的事件,它可以包含多封邮件。

这里的事件EmailsReceivedEvent继承自ApplicationEvent,这在早期 Spring (4.2之前)是必须的。

Publisher

我们需要定义一个 bean 负责事件的发送:

@Component
public class EmailsReceivedEventPublisher {@Autowiredprivate ApplicationEventPublisher applicationEventPublisher;/*** 发布多个邮件收到的事件* @param emails*/public void publish(@NonNull List<Email> emails){this.doPublishEvent(emails);}/*** 发布邮件收到的事件(单个邮件)* @param email*/public void publish(@NonNull Email email){List<Email> emails = Collections.singletonList(email);this.doPublishEvent(emails);}private void doPublishEvent(List<Email> emails){EmailsReceivedEvent event = new EmailsReceivedEvent(this);event.setEmails(emails);applicationEventPublisher.publishEvent(event);}
}

具体的事件发送我们需要使用ApplicationEventPublisher.publishEvent,这里通过依赖注入获取一个ApplicationEventPublisher的引用。

如果你因为某些原因不方便那么做(比如因为 bean 在 Spring 启动的早期阶段实例化,无法使用依赖注入),可以使用ApplicationEventPublisherAware获取依赖:

@Component
public class EmailsReceivedEventPublisher implements ApplicationEventPublisherAware {private ApplicationEventPublisher applicationEventPublisher;// ...@Overridepublic void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {this.applicationEventPublisher = applicationEventPublisher;}
}

当然,像上边这样为某个时间封装一个事件发布用的 bean 并非必须,你完全可以用自己喜欢的方式调用ApplicationEventPublisher.publish来发送事件,但封装事件发布可以让代码更清晰易用。

Listener

下面需要添加我们自定义事件的监听器,具体方式是向 ApplicationContext 中添加一个 bean,并实现ApplicationListener接口:

@Component
public class EmailsReceivedEventListener implements ApplicationListener<EmailsReceivedEvent> {@Overridepublic void onApplicationEvent(EmailsReceivedEvent event) {List<String> addresses = event.getEmails().stream().map(Email::address).collect(Collectors.toList());System.out.printf("收到多个电子邮件:%s%n", addresses);}
}

接口ApplicationListener是泛型的,所以这里实现的onApplicationEvent方法的参数是类型安全的,换言之该方法只会处理EmailsReceivedEvent类型的已发布事件。

最后,我们使用ApplicationRunner进行测试:

@Configuration
public class WebConfig {@Autowiredprivate EmailsReceivedEventPublisher emailsReceivedEventPublisher;@BeanApplicationRunner applicationRunner(){return args -> {List<Email> emails = List.of(new Email("tom@qq.com","123"),new Email("lilei@qq.com","hello"),new Email("hanmeimei@qq.com","good day"));emailsReceivedEventPublisher.publish(emails);};}
}

关于ApplicationRunner,可以阅读我的这篇文章。

注解驱动

从 Spring 4.2 开始,Spring 提供以注解的方式来定义事件监听器:

@Component
public class EmailsReceivedEventListener {@EventListenerpublic void handleEvent(EmailsReceivedEvent event) {List<String> addresses = event.getEmails().stream().map(Email::address).collect(Collectors.toList());System.out.printf("收到多个电子邮件:%s%n", addresses);}
}

@EventListener注解标记的方法将作为事件处理方法,且 Spring 会根据参数(事件)的类型来确定该方法处理何种事件。此时无需再让监听器实现ApplicationListener,事件处理方法的命名也可以更灵活。

也可以在@EventListener注解中直接指定要处理的事件类型,无论方法有没有具体事件作为参数,都可以处理该事件:

@Component
public class EmailsReceivedEventListener {// ...@EventListener(EmailsReceivedEvent.class)public void receivedTip(){System.out.println("some emails get.");}
}

可以在@EventListener中指定多个要处理的事件类型,可以通过这种方式让同一个方法处理多种类型的事件:

@Component
public class EmailsReceivedEventListener {// ...@EventListener({EmailsReceivedEvent.class, ContextRefreshedEvent.class})public void receivedTip(Object event){if (event instanceof EmailsReceivedEvent){System.out.println("some emails get.");}else if (event instanceof ContextRefreshedEvent){System.out.println("ApplicationContext is refreshed.");}else{;}}
}

过滤

如果在测试代码中添加上下面的语句:

emailsReceivedEventPublisher.publish(Collections.emptyList());

我们就会看到下面的输出:

收到多个电子邮件:[tom@qq.com, lilei@qq.com, hanmeimei@qq.com]
收到多个电子邮件:[]

显然,这是因为没有在事件处理器中判断邮件队列是否为空导致的。当然,加上相应的判断很容易,但相比之下,我们有个更简单的实现方式——使用@EventListener注解对事件进行过滤:

@Component
public class EmailsReceivedEventListener {@EventListener(condition = "#erEvent.getEmails().isEmpty() == false")public void handleEvent(EmailsReceivedEvent erEvent) {List<String> addresses = erEvent.getEmails().stream().map(Email::address).collect(Collectors.toList());System.out.printf("收到多个电子邮件:%s%n", addresses);}// ...
}

@EventListenercondition属性是一个SpEL,这个 SpEL 的评估结果将决定是否执行事件处理方法。在这个示例中,#erEvent表示事件处理方法的erEvent参数(事件对象),利用这个参数可以构建“邮件列表不为空”的SpEL表达式,即#erEvent.getEmails().isEmpty() == false

  • 注意,这里的事件处理方法形参命名改为erEvent,而不是之前的event,这是因为在这里的SpEL中,event是一个预定义的变量,指代当前事件。
  • 可以通过多种方式定义SpEL表达式,比如上边的示例,也可以是!#erEvent.getEmails().isEmpty()
  • 关于 SpEL 的更多介绍,可以阅读我的另一篇文章。

新的事件

使用注解声明事件处理方法的另一个额外好处是,事件处理方法可以返回一个值,作为“新的事件”。

看下面这个示例:

public class WasteEmailsReceivedEvent extends ApplicationEvent {@Getterprivate final List<Email> emails;public WasteEmailsReceivedEvent(Object source, @NonNull List<Email> emails) {super(source);this.emails = emails;}
}@Component
public class WasteEmailsReceivedEventListener {@EventListener(condition = "!#werEvent.getEmails().isEmpty()")public void handleEvent(WasteEmailsReceivedEvent werEvent){werEvent.getEmails().forEach(email -> System.out.printf("将邮件%s移入垃圾邮件%n", email));}
}

WasteEmailsReceivedEvent是一个表示收到了垃圾邮件的事件,WasteEmailsReceivedEventListener监听器负责处理该事件。

用一个 bean 来判断某个邮件是否为黑名单中的邮件:

@Component
public class EmailBlacklist {private final Set<String> addresses = Set.of("lilei@qq.com");public boolean inBlackList(String address){return addresses.contains(address);}
}

修改邮件接收事件的监听器,检查收到的邮件中是否有在黑名单中的,如果有,就返回相应的垃圾邮件事件:

@Component
public class EmailsReceivedEventListener {@Autowiredprivate EmailBlacklist emailBlacklist;@EventListener(condition = "!#erEvent.getEmails().isEmpty()")public WasteEmailsReceivedEvent handleEvent(EmailsReceivedEvent erEvent) {List<String> addresses = erEvent.getEmails().stream().map(Email::address).collect(Collectors.toList());System.out.printf("收到多个电子邮件:%s%n", addresses);List<Email> wasteEmails = erEvent.getEmails().stream().filter(email -> emailBlacklist.inBlackList(email.address())).toList();if (wasteEmails.isEmpty()) {return null;} else {return new WasteEmailsReceivedEvent(this, wasteEmails);}}// ...
}

因为垃圾邮件事件监听器中加了空邮件过滤,所以这里其实可以不用判断wasteEmails是否为空,直接返回new WasteEmailsReceivedEvent(this, wasteEmails),示例中这样做是为了展示在不需要产生新消息的情况下可以返回一个null

现在运行示例,因为有一个垃圾邮件lilei@qq.com,所以处理EmailsReceivedEvent的监听器会产生一个新的WasteEmailsReceivedEvent事件,后者也会继续触发监听器WasteEmailsReceivedEventListener

如果需要在事件处理方法中生成多个后续事件,可以返回一个包含多个事件的容器(Collection)或者数组:

@Component
public class EmailsReceivedEventListener {// ...@EventListener(condition = "!#erEvent.getEmails().isEmpty()")public List<WasteEmailsReceivedEvent> handleEvent(EmailsReceivedEvent erEvent) {// ...return wasteEmails.stream().map(email -> new WasteEmailsReceivedEvent(EmailsReceivedEventListener.this, List.of(email))).collect(Collectors.toList());}// ...
}

这个示例中,为每个垃圾邮件单独生成一个WasteEmailsReceivedEvent事件,并返回一个WasteEmailsReceivedEvent事件组成的List

Event

从 Spring 4.2 开始,不再强制要求自定义事件必须继承自ApplicationEvent,因此我们的示例可以改写为:

public class EmailsReceivedEvent {@Setter@Getterprivate List<Email> emails = Collections.emptyList();private final Object source;public EmailsReceivedEvent(Object source) {this.source = source;}
}

相应的,ApplicationEventPublisher接口也添加了一个发布Object类型事件的重载方法:

@FunctionalInterface
public interface ApplicationEventPublisher {default void publishEvent(ApplicationEvent event) {this.publishEvent((Object)event);}void publishEvent(Object event);
}

事务绑定事件

从 Spring 4.2 开始,我们可以使用一个特殊的@TransactionalEventListener,利用它可以监听事务特定阶段产生的事件。

@TransactionalEventListener@EventListener的扩展。

看一个示例:

@Data
@TableName("email")
public class EmailEntity {@TableId(type = IdType.AUTO)private long id;@TableField@NonNullprivate String address;@TableField@NonNullprivate String content;public Email toEmail() {return new Email(this.getAddress(), this.getContent());}
}public interface EmailMapper extends BaseMapper<EmailEntity> {
}public class EmailAddEvent extends ApplicationEvent {@Getterprivate final EmailEntity email;public EmailAddEvent(Object source, EmailEntity email) {super(source);this.email = email;}
}@Service
public class EmailService {@Autowiredprivate EmailMapper emailMapper;@Autowiredprivate ApplicationEventPublisher eventPublisher;/*** 将电子邮件批量添加到持久层*/@Transactionalpublic void addEmails(List<Email> emails){this.addEmailEntities(emails.stream().map(Email::toEmailEntity).collect(Collectors.toList()));}@Transactionalpublic void addEmailEntities(List<EmailEntity> emailEntities){emailEntities.forEach(emailEntity -> {eventPublisher.publishEvent(new EmailAddEvent(EmailService.this, emailEntity));});emailEntities.forEach(emailEntity -> emailMapper.insert(emailEntity));}
}

注意,这里事件发布动作必须在数据库插入相关API调用之前,否则事务回滚后产生的异常会阻止事件发布相关代码的运行,也就无法监听和处理事务回滚后的相关事件。

示例中使用 MyBatisPlus 向持久层批量加入电子邮件记录,每条记录添加时都会发送一个EmailAddEvent事件,表示添加电邮记录这个行为的发生。

因为是批量插入,为了确保数据库的数据一致性,我们需要使用事务,在这里就是用@Transactional标记批量插入方法。

  • 因为 Spring 事务是通过 AOP 实现,所以关联的"自调用"方法(这里是addEmails)同样要用@Transactional标记。
  • 要运行这个示例,需要添加数据库相关依赖,并且连接上一个数据库。
  • 关于如何在 Spring Boot 中使用 MyBatisPlus 和数据库,可以看我的相关文章:
    • 从零开始 Spring Boot 3:数据库 - 红茶的个人站点 (icexmoon.cn)
    • 从零开始 Spring Boot 4:Mybatis Plus - 红茶的个人站点 (icexmoon.cn)

现在,如果所有数据成功插入,就会正常提交事务,否则就会触发事务回滚,数据库会恢复到插入前。

创建一个监听器用于监听在这个事务中所发布的事件:

@Component
public class EmailAddEventListener {@TransactionalEventListenerpublic void addSuccess(EmailAddEvent eaEvent){System.out.printf("Email %s is already add to db.%n", eaEvent.getEmail());}@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)public void addFail(EmailAddEvent eaEvent){System.out.printf("Email %s add to db failed.%n", eaEvent.getEmail());}
}

addSuccess用于监听事务成功提交后事务产生的EmailAddEvent事件,addFail用于监听事务回滚后事务产生的EmailAddEvent事件。

实际上是通过@TransactionalEventListenerphase属性决定在事务的哪个阶段触发事件监听:

  • AFTER_COMMIT,事务成功提交,默认值。
  • AFTER_ROLLBACK,事务失败,回滚。
  • AFTER_COMPLETION,事务完成(无论失败还是成功)。
  • BEFORE_COMMIT,事务提交之前。

可以用下边的表结构进行测试:

CREATE TABLE `email` (`id` bigint unsigned NOT NULL AUTO_INCREMENT,`address` varchar(255) NOT NULL,`content` text NOT NULL,PRIMARY KEY (`id`),UNIQUE KEY `address_idx` (`address`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

这个表的address字段设置了唯一索引,如果重复添加相同的数据,就会触发事务回滚以及相应事件监听。

要说明的是,TransactionalEventListener仅用于监听事务中发布的事件,如果没有事务也就不起作用。

排序

可以用@Order对监听器进行排序,以确保某个监听器在另一个之前被调用:

@Component
public class EmailsReceivedEventListener {@Autowiredprivate EmailBlacklist emailBlacklist;@Order(2)@EventListener(condition = "!#erEvent.getEmails().isEmpty()")public List<WasteEmailsReceivedEvent> handleEvent(EmailsReceivedEvent erEvent) {// ...}@Order(1)@EventListener({EmailsReceivedEvent.class, ContextRefreshedEvent.class})public void receivedTip(Object event) {// ...}
}

数字越小越先被执行。

异步

默认情况下,Spring 的事件模型是同步的(单线程),这样的好处是事件发布和监听都是顺序执行的,并且可以很容易地在事件中返回新的事件来触发新的后续处理。此外,前边介绍的事务绑定事件也只能在这种情况下生效。

我们看下边的示例:

@Component
public class WasteEmailsReceivedEventListener {@EventListener(condition = "!#werEvent.getEmails().isEmpty()")public void handleEvent(WasteEmailsReceivedEvent werEvent) throws InterruptedException {Thread.sleep(1000);werEvent.getEmails().forEach(email -> System.out.printf("将邮件%s移入垃圾邮件%n", email));}
}

现在,处理垃圾邮件的监听器每次执行都要等待1秒,运行测试很容易能看到这种“迟滞”。

@Async

如果每封垃圾邮件移入回收站这个动作都可以并行执行,那我们就可以用异步执行(@Async)来改善性能:

@EnableAsync
public class WebConfig {// ...
}@Component
public class WasteEmailsReceivedEventListener {@Async@EventListener(condition = "!#werEvent.getEmails().isEmpty()")public void handleEvent(WasteEmailsReceivedEvent werEvent) throws InterruptedException {// ...}
}

再次运行就能看到垃圾处理过程是有多么的迅速。

需要注意的是,因为是异步执行,所以如果事件处理方法中产生异常,调用方是无法捕获这个异常的。此外,异步执行的时候也不能通过返回事件的方式发布新的事件,而是需要手动发布事件:

public class WasteEmailRemovedEvent extends ApplicationEvent {@Getterprivate final Email email;public WasteEmailRemovedEvent(Object source, Email email) {super(source);this.email = email;}
}@Component
public class WasteEmailsReceivedEventListener {@Autowiredprivate ApplicationEventPublisher eventPublisher;@Async@EventListener(condition = "!#werEvent.getEmails().isEmpty()")public void handleEvent(WasteEmailsReceivedEvent werEvent) throws InterruptedException {// ...werEvent.getEmails().forEach(email -> {this.eventPublisher.publishEvent(new WasteEmailRemovedEvent(WasteEmailsReceivedEventListener.this, email));});}
}@Component
public class WasteEmailRemovedEventListener {@EventListenerpublic void eventHandler(WasteEmailRemovedEvent werEvent){System.out.printf("Email %s is already removed.%n", werEvent.getEmail());}
}

ApplicationEventMulticaster

除了使用@Async让事件处理方法异步执行外,我们还可以修改事件模型的默认策略,让所有的事件监听都异步进行,比如:

@Configuration
public class WebConfig {// ...@Bean(name = "applicationEventMulticaster")public ApplicationEventMulticaster simpleApplicationEventMulticaster() {SimpleApplicationEventMulticaster eventMulticaster =new SimpleApplicationEventMulticaster();eventMulticaster.setTaskExecutor(new SimpleAsyncTaskExecutor());return eventMulticaster;}
}

此时并没有用@EnableAsync开启异步相关注解,也没有用@Async标记相应方法,但运行示例就能发现所有事件监听都很快发生,几乎没有延迟。

这样做会导致事务绑定事件(@TransactionalEventListener)无法正常使用。

Application Event

Sprring 本身就定义了很多事件,用于内部的处理,比如 Spring 在启动阶段产生的ContextRefreshedEvent等,我们可以用同样的方式监听这些事件,以在特定阶段执行某些任务。

相关的内容我在这篇文章中有过介绍,这里不过多赘述。

泛型事件

可以在定义一个有泛型参数的事件,并利用泛型来区分事件并监听:

public class GenericMsgEvent<T> extends ApplicationEvent {@Getterprivate final T msg;public GenericMsgEvent(Object source, T msg) {super(source);this.msg = msg;}
}@Component
public class GenericMsgEventListener {@EventListenerpublic void strEventHandler(GenericMsgEvent<String> gmEvent){System.out.printf("String msg event is get, msg:%s.%n", gmEvent.getMsg());}@EventListenerpublic void intEventHandler(GenericMsgEvent<Integer> gmEvent){System.out.printf("Int msg event is get, msg:%s.%n", gmEvent.getMsg());}
}

这里两个方法strEventHandlerintEventHandler分别用于泛型参数是String和泛型参数是Integer时的事件监听。

看起来很不错,但实际上有一个“陷阱”,假如你像下边这样发布事件:

eventPublisher.publishEvent(new GenericMsgEvent<String>(this, "hello"));

实际上并不会触发任何事件监听,这是因为new GenericMsgEvent<String>(this, "hello")中的泛型String仅存在于编译阶段,运行时会对泛型进行类型擦除,实际上这里相当于GenericMsgEvent<Object>,所以不会触发针对泛型参数是StringInteger定义的监听器。

关于 Java 泛型中的类型擦除,可以阅读我的这篇文章。

所以,使用这类事件的正确方式是派生出一个特定类型后发布该类型的事件,比如:

public class IntMsgEvent extends GenericMsgEvent<Integer>{public IntMsgEvent(Object source, Integer msg) {super(source, msg);}
}public class StringMsgEvent extends GenericMsgEvent<String> {public StringMsgEvent(Object source, String msg) {super(source, msg);}
}

现在可以用这两个类型发布事件:

eventPublisher.publishEvent(new StringMsgEvent(this, "hello"));
eventPublisher.publishEvent(new IntMsgEvent(this, 11));

这两个事件都会被对应的监听器方法监听到。

当然,这种方式多少有些“无趣”,并且不得不定义大量派生类型,为此,Spring 给我们提供了一种额外方式:

public class EntityCreatedEvent<T> extends ApplicationEvent implements ResolvableTypeProvider {@Getterprivate final T entity;public EntityCreatedEvent(Object source, T entity) {super(source);this.entity = entity;}@Overridepublic ResolvableType getResolvableType() {ResolvableType resolvableType = ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(entity));return resolvableType;}
}

在这里,泛型事件EntityCreatedEvent<T>实现了ResolvableTypeProvider接口,并且在方法getResolvableType中返回一个“确切的”泛型类型。

在前文中我提到过ResolvableType这个类型,Spring 可以通过它来确认泛型的具体类型。

监听器的写法与之前的示例一致,这里不再赘述。

现在无需使用任何派生类,直接使用泛型事件进行发布:

eventPublisher.publishEvent(new EntityCreatedEvent<>(this, new Email("123@tomcom","sdfdsf")));

The End,谢谢阅读。

本文中的完整示例可以从这里获取。

参考资料

  • 从零开始 Spring Boot 3:数据库 - 红茶的个人站点 (icexmoon.cn)
  • 从零开始 Spring Boot 4:Mybatis Plus - 红茶的个人站点 (icexmoon.cn)
  • 标准和自定义事件
  • Spring Events | Baeldung

http://www.ppmy.cn/news/404123.html

相关文章

简要介绍 | 三维点云配准:理论、方法与挑战

三维点云配准&#xff1a;理论、方法与挑战 注&#xff1a;”简要介绍“系列仅从概念上对某一领域进行非常简要的介绍&#xff0c;不适合用于深入和详细的了解 三维点云配准 是计算机视觉和机器人领域的重要课题&#xff0c;涉及从不同视角或时间点采集的三维点云数据之间寻找最…

【无标题】面试常考算法(3):二叉树遍历(创建、遍历、销毁)

这部分不够熟悉的话&#xff0c;面试直接递归就行。不过实际中虽然递归在某些情况下可以提供简洁和优雅的解决方案&#xff0c;但可能占用大量的内存空间和导致额外时间开销&#xff0c;所以还是尽量使用非递归。因为每次递归调用时&#xff0c;函数的局部变量和参数都需要在栈…

VBA基础(宏编程)

VBA介绍&#xff1a; Visual Basic for Applications&#xff08;VBA&#xff09;是 VisualBasic 的一种宏语言&#xff0c;是微软开发出来在其桌面应用程序中执行通用的自动化(OLE)任务的编程语言。主要能用来扩展 Windows 的应用程序功能&#xff0c;特别是Microsoft Office…

360全景拍摄为什么要使用鱼眼镜头,与超广角镜头区别?

360全景摄影&#xff0c;通常使用8mm至15mm鱼眼镜头。360全景摄影之后以一定要选择鱼眼镜头进行360全景摄影&#xff0c;其主要原因为了单张照片拍摄到较大的视角范围&#xff0c;从而以较少的照片拼接成一个360全景图。 使用8mm鱼眼镜头&#xff0c;360全景摄影少则拍摄…

vue3+ts+vite+element plus中使用luckysheet(预览效果)

前言&#xff1a; 这两天一个项目&#xff0c;需要在页面中以excel的形式展示大量数据&#xff0c;喜欢偷懒的我果断扒拉了一堆适用于vue3的插件&#xff0c;下面简单说说我使用的luckysheet 使用&#xff1a; 一、准备一个vue3tsviteelement plus的项目 此处省略n个字。。。…

618父亲节,感恩的祝福送给父亲!

父亲节&#xff08;Fathers Day&#xff09;&#xff0c;是感恩父亲的节日。Fathers day, is a day of thanksgiving for fathers. 第一个提出父亲节理念的人是1906年的多德夫人。她想用一个特殊的日子来纪念她的父亲&#xff0c;她的妈妈多年前就去世了。起初&#xff0c;多德…

【闭包函数与装饰器大全】——python基础

目录索引 闭包&#xff1a;闭包三要素&#xff1a;闭包的作用&#xff1a;闭包演示&#xff1a;闭包的意义&#xff1a; 装饰器&#xff1a;特点&#xff1a;实例演示&#xff1a;实例演示2之参数&#xff1a; 装饰器常用的场景&#xff1a;编写一个计时的装饰器&#xff1a;*普…

实测Maven依赖包可通过域名抢注实现钓鱼攻击吗

先说结论&#xff1a;基本不可行 原理 Maven包中 groupId 字段是域名反写&#xff0c;比如你有一个 12345.com&#xff0c;就可以申请到 com.12345 的groupId。 很多开源项目都停止维护&#xff0c;但是很多人使用&#xff0c;这些团队可能忘记续费域名&#xff1b;同时目前…