Spring Event 业务解耦,贼好用!

在业务开发过程中,业务逻辑可能非常复杂,核心业务 + 多个子业务,比如,下单之后,发送通知、监控埋点、记录日志……,如果都放在一起,代码会很臃肿。而且有两个问题,一个是业务耦合,一个是串行耗时。

添加图片注释,不超过 140 字(可选)

还有一些业务场景不需要在一次请求中同步完成,比如邮件发送、短信发送等。对于这样的场景可以使用 MQ ,使用 MQ会增加系统设计的复杂性,还要考虑消息丢失、消息重复等问题。所以,一般在开发的时候,都会把这些操作抽象成观察者模式,也就是发布/订阅模式,下面给大家介绍一下 Spring Event。

简介

Spring Event(Application Event)其实就是一个观察者模式。观察者模式,含有主题(针对该主题的事件),发布者(发布主题或事件),订阅者(监听主题的人)。有三个部分组成,事件(ApplicationEvent)、监听器(ApplicationListener)和事件发布操作。

事件:描述发生了什么事情、比如说请求处理完成、Spring 容器刷新完毕 事件源:事件的产生者、任何一个事件都必须有一个事件源。比如请求处理完成的事件源就是 DispatcherServlet 、Spring 容器刷新完毕的事件源就是 ApplicationContext 事件广播器:事件和事件监听器的桥梁、负责把事件通知给事件监听器 事件监听器:监听事件的发生、可以在监听器中做一些处理

事件实现方式

ApplicationEvent:事件,每个实现类表示一类事件,可携带数据。 ApplicationListener:事件监听器,用于接收事件处理时间。 ApplicationEventMulticaster:事件管理者,用于事件监听器的注册和事件的广播。 ApplicationEventPublisher:事件发布者,委托ApplicationEventMulticaster完成事件发布。

事件

public abstract class ApplicationEvent extends EventObject { /** use serialVersionUID from Spring 1.2 for interoperability */ private static final long serialVersionUID = 7099057708183571937L; /** System time when the event happened */ private final long timestamp; /** * Create a new ApplicationEvent. * @param source the object on which the event initially occurred (never {@code null}) */ public ApplicationEvent(Object source) { super(source); this.timestamp = System.currentTimeMillis(); } /** * Return the system time in milliseconds when the event happened. */ public final long getTimestamp() { return this.timestamp; }}

ApplicationEvent:应用事件携带一个 Objecgt 对象,可以被发布,source表示事件源。

可以继承ApplicationEvent自定义事件。

public class TestEvent extends ApplicationEvent { private String message; public TestEvent(String message) { super(message); this.message = message; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; }}

事件监听器

事件监听器,有两种实现方式,一种是实现 ApplicationListener 接口,另一种是使用@EventListener 注解。

ApplicationListener:事件监听器,职责为处理事件广播器发布的事件。

@FunctionalInterfacepublic interface ApplicationListener<E extends ApplicationEvent> extends EventListener { void onApplicationEvent(E event);}

ApplicationListener 有两个实现接口:SmartApplicationListener和GenericApplicationListener

@Slf4j@Servicepublic class OrderLogListener implements ApplicationListener<OrderEvent> { @Override public void onApplicationEvent(PlaceOrderEvent event) { log.info("[afterPlaceOrder] log."); }}

@Log4j2@Componentpublic class AEventListener implements ApplicationListener<TestEvent> { @Async @EventListener public void listener(TestEvent event) throws InterruptedException { log.info("监听到数据:{}", event.getMessage()); }}

事件广播器

事件广播器,将EventPubsher(事件发布者)发布的event 广播给事件EventListener(事件监听器)。

Spring提供了默认的实现SimpleApplicationEventMulticaster,如果用户没有配置自定义事件广播器, 则会默认使用SimpleApplicationEventMulticaster作为事件广播器。在容器刷新的过程中会实例化、初始化事件广播器。

ApplicationContext 本来就实现了ApplicationEventPublisher接口,因此应用上下文本来就是一个事件发布者,在AbstractApplicationContext中实现了事件发布的业务。

  • 注入ApplicationContext 发布事件

@Autowiredprivate ApplicationContext applicationContext;applicationContext.publishEvent();

  • 注入ApplicationEventPublisher发布事件

@Autowiredprivate ApplicationEventPublisher applicationEventPublisher;applicationEventPublisher.publishEvent(myEvent)

  • 实现ApplicationEventPublisherAware 发布事件

@Servicepublic class PublishEvent implements ApplicationEventPublisherAware { public static ApplicationEventPublisher eventPublisher = null; @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { eventPublisher.publishEvent("123"); }}

Spring Event 同步模式

定义事件

public class TestEvent extends ApplicationEvent { private String message; public TestEvent(String message) { super(message); this.message = message; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; }}

监听器

@Component@Slf4jpublic class ListenerService { @EventListener public void listener(TestEvent event) throws InterruptedException { Thread.sleep(5000); log.info("监听到数据:{}", event.getMessage()); }}

发布事件

@Servicepublic class PublishService { @Autowired private ApplicationEventPublisher applicationEventPublisher; public void publish(String message) { applicationEventPublisher.publishEvent(new TestEvent(message)); }}

@AutowiredPublishService publishService;@RequestMapping("publishMsg")public void publishMsg() { for (int i = 0; i < 5; i++) { publishService.publish("消息" + (i + 1)); }}

2022-12-07 15:45:12.825 INFO 16304 --- [nio-8080-exec-1] com.test.service.ListenerService : 监听到数据:消息1 2022-12-07 15:45:17.838 INFO 16304 --- [nio-8080-exec-1] com.test.service.ListenerService : 监听到数据:消息2 2022-12-07 15:45:22.842 INFO 16304 --- [nio-8080-exec-1] com.test.service.ListenerService : 监听到数据:消息3 2022-12-07 15:45:27.857 INFO 16304 --- [nio-8080-exec-1] com.test.service.ListenerService : 监听到数据:消息4 2022-12-07 15:45:32.870 INFO 16304 --- [nio-8080-exec-1] com.test.service.ListenerService : 监听到数据:消息5

从结果可以看出,只有处理完一个事件后才会处理下一个事件,这就是同步模式

Spring Event 异步模式

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

Listener 类需要开启异步的方法增加 @Async 注解:

@EventListener@Asyncpublic void listener(TestEvent event) throws InterruptedException { Thread.sleep(5000); log.info("监听到数据:{}", event.getMessage());}

2022-12-07 15:50:00.986 INFO 3964 --- [cTaskExecutor-5] com.test.service.ListenerService : 监听到数据:消息5 2022-12-07 15:50:00.986 INFO 3964 --- [cTaskExecutor-4] com.test.service.ListenerService : 监听到数据:消息4 2022-12-07 15:50:00.986 INFO 3964 --- [cTaskExecutor-1] com.test.service.ListenerService : 监听到数据:消息1 2022-12-07 15:50:00.986 INFO 3964 --- [cTaskExecutor-2] com.test.service.ListenerService : 监听到数据:消息2 2022-12-07 15:50:00.986 INFO 3964 --- [cTaskExecutor-3] com.test.service.ListenerService : 监听到数据:消息3

可以看到,事件监听器不会阻塞,多个事件可以同时进行。

自定义线程池

@Async默认线程池为 SimpleAsyncTaskExecutor,不是真的线程池,这个类不重用线程,默认每次调用都会创建一个新的线程。

  • 实现接口 AsyncConfigurer

  • 继承 AsyncConfigurerSupport

  • 配置由自定义的 TaskExecutor 替代内置的任务执行器

@Configurationpublic class TaskPoolConfig { @Bean(name = "asyncExecutor") public Executor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(20); executor.setQueueCapacity(200); executor.setKeepAliveSeconds(60); executor.setThreadNamePrefix("asyncExecutor-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); return executor; }}

@EventListener @Async("asyncExecutor") public void listener(TestEvent event) throws InterruptedException { Thread.sleep(5000); log.info("监听到数据:{}", event.getMessage()); }

总结

使用 Spring Event 实现发布/订阅模式,可以对一些业务进行解耦。

本文使用 文章同步助手 同步

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

推荐阅读更多精彩内容