EventListener与EventBus

news2024/11/22 7:28:58

EventListener

JDK

JDK1.1开始就提供EventListener,一个标记接口,源码如下:

/**
 * A tagging interface that all event listener interfaces must extend.
 */
public interface EventListener {
}

JDK提供的java.util.EventObject

public class EventObject implements Serializable {
	protected transient Object source;
}

用户需要定义继承自EventObject的事件对象,然后定义具体的Listener接口继承EventListener。事件源通过添加监听器(addXXXListener方法)手动管理事件的订阅和发布,需要用户手动调用监听器的回调方法,事件管理逻辑往往由开发者编码完成。

典型场景:主要用于早期GUI应用开发(如AWT和Swing),用于组件间的事件通信。

优点:轻量级,易于理解;适合简单场景或单一模块。

缺点:

  • 需显式注册和管理事件监听器,手动处理事件流转,容易导致代码复杂度增加;
  • 缺乏高级特性,如事件异步处理、条件过滤等。

Spring

@EventListener

当在实现某些特定业务逻辑时,通常可通过发送事件的方式实现解耦,这也是观察者模式的一种体现。从Spring 4.2开始提供注解@EventListner,不再需要单独编写监听器类,只需在Spring Bean方法上标记@EventListener即可。

源码如下:

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EventListener {
	@AliasFor("classes")
	Class<?>[] value() default {};
	@AliasFor("value")
	Class<?>[] classes() default {};
	String condition() default "";
}

解读:

  • value和classes:作用相同,表示监听的一个或一组事件,用于支持方法中同一个父类的事件;
  • condition:支持Spring EL表达式,用来做Event中的变量或者方法判断。

Event的整个生命周期,从Publisher发出,经过applicationContext容器通知到EventListener,都是发生在单个Spring容器中。

事件可直接使用ApplicationEvent,或继承它。源码如下:

public abstract class ApplicationEvent extends java.util.EventObject {
	// 事件发生时间
	private final long timestamp;

	public ApplicationEvent(Object source) {
		super(source);
		this.timestamp = System.currentTimeMillis();
	}

	public ApplicationEvent(Object source, Clock clock) {
		super(source);
		this.timestamp = clock.millis();
	}
}
示例

多个监听器,监听Account创建,完成不同的业务逻辑。

创建Event事件监听

/**
 * 账号监听,处理账号创建成功的后续逻辑
 */
@Component
public class AccountListener {
	@EventListener
	@Async
	public void processAccountCreatedEvent1(AccountCreatedEvent event) {
		// 1. 发送邮件、短信
	}

	@EventListener
	@Order(100)
	public void processAccountCreatedEvent2(AccountCreatedEvent event) {
		// 2. 添加积分等,@Order(100)用来设定执行顺序
	}
}

使用ApplicationEventPublisher发送事件:

@Autowired
private ApplicationEventPublisher publisher;

public boolean save(Account account) {
	// 数据库保存成功
	if (true) {
		publisher.publishEvent(new AccountCreatedEvent(account));
	}
	return false;
}

一个发布者可对应多个监听者:

@EventListener(value = {AccountCreatedEvent.class, AccountUpdatedEvent.class}, condition = "#event.account.age > 10")
public void processAccountCreatedEvent2(AccountEvent event) {
	// 业务逻辑
}

监听执行顺序:可使用@Order(100)来标记事件的执行顺序,异步情况下只保证按顺序将监听器丢入进线程池,具体执行顺序得看线程。

监听异步执行:使用@Async标记即可,前提条件:使用@EnableAsync开启Spring异步。

优点:

  • 更加简洁,方法注解即声明监听;
  • 支持异步处理,条件过滤,功能更强大;
  • 与其他Spring特性(如AOP和事务)集成更好。

缺点:

  • 不如ApplicationListener明确(可能难以追踪监听器逻辑);
  • 依赖注解,可能不适合部分代码风格偏向接口设计的团队。

ApplicationListener

Spring提供两种事件监听机制:

  • 基于@EventListener注解:
  • 基于ApplicationListener泛型接口:早期提供,实现onApplicationEvent方法

通过上下文来发布一个事件,监听器收到订阅的事件作相应的处理,Spring提供的方便高效的事件驱动模型。

ApplicationListener源码:

@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends java.util.EventListener {
	void onApplicationEvent(E event);

	default boolean supportsAsyncExecution() {
		return true;
	}

	static <T> ApplicationListener<PayloadApplicationEvent<T>> forPayload(Consumer<T> consumer) {
		return event -> consumer.accept(event.getPayload());
	}

}

简单使用:

@Component
public class MyEventListener implements ApplicationListener<MyEvent> {
	@Override
	public void onApplicationEvent(MyEvent event) {
		System.out.println("Received event: " + event.getMessage());
	}
}

优点:

  • 明确,适合固定、简单的监听逻辑;
  • 提供对所有事件的统一入口,可基于事件类型实现复杂的分发逻辑。

缺点:

  • 相对冗长,代码侵入性高(必须实现接口);
  • 不支持直接使用条件过滤和异步处理;
  • 扩展性和灵活性低。

@EventListener与ApplicationListener

特性ApplicationListener@EventListener
定义方式实现ApplicationListener接口使用@EventListener注解
写法复杂度必须实现接口注解方式更简洁
类型绑定泛型绑定到特定事件类型自动通过方法参数类型推断
条件过滤不支持支持(condition属性)
异步处理需手动实现多线程支持@Async开箱即用
事务集成支持,但需手动配置与Spring的事务注解自然集成
适用场景简单、固定事件监听逻辑更现代化、复杂事件管理逻辑
引入版本早期版本已有Spring 4.2+

JDK与Spring

特性JDK EventListenerSpring @EventListener
触发方式显式调用监听器基于事件发布自动触发
实现方式通过接口通过注解声明
事件管理手动注册、调用自动管理(基于Spring容器)
适用场景简单GUI或模块内部通信企业级应用、跨模块解耦
异步支持不支持支持
条件过滤不支持支持
事务集成不支持支持
复杂性简单,手动管理较高,但更自动化和灵活

EventBus

使用异步的方式来发送事件,或触发另外一个动作,即Publish/Subscribe Event。

EventBus有不同的实现框架,一般都指Guava EventBus。

框架

Guava EventBus包路径下:
在这里插入图片描述
类之间的关系如:
在这里插入图片描述
EventBus组成部分:

  1. EventBus、AsyncEventBus:事件发送器
  2. Event:事件承载单元
  3. SubscriberRegistry:订阅者注册器,将订阅者注册到Event上,即将有注解Subscribe的方法和Event绑定起来
  4. Dispatcher:事件分发器,将事件的订阅者调用来执行
  5. Subscriber、SynchronizedSubscriber:订阅者,并发订阅还是同步订阅

原理

EventBus是基于注册监听的方式来运行的,首先需将EventBus实例化,然后才会有事件及监听者:

// 同步
EventBus eventBus = new EventBus();

注册监听者,底层就是将类eventListener中所有注解有Subscribe的方法与其Event对放在一个map中(一个Event可以对应多个Subscribe的方法):

eventBus.register(eventListener);

在高并发的环境下使用AsyncEventBus时,发送事件可能会出现异常,因为它使用的线程池,当线程池的线程不够用时,会拒绝接收任务,就会执行线程池的拒绝策略,如果需要关注是否提交事件成功,就需要将线程池的拒绝策略设为抛出异常,并且try-catch来捕获异常:

try {
	eventBus.post(new LoginEvent("user", "pass"));
} catch (Exception e) {
	// log
}

内部的订阅通知模型,无需使用事件+事件listener模型,只有一个事件类。

示例

一个简单的实例,值得一提的是,注册和发布事件,与消费事件不在一个类里:

// 事件类中方法以@Subscribe注解
class EventBusChangeRecorder {
	@Subscribe
	public void recordCustomerChange(ChangeEvent e) {
		// 业务逻辑
		System.out.println("事件触发");
	}
}
// 创建事件总线
EventBus eventBus = new EventBus();
// 注册事件
eventBus.register(new EventBusChangeRecorder());
ChangeEvent event = new ChangeEvent(new EventBusChangeRecorder());
// 发布事件
eventBus.post(event);
// 需要异步执行可使用EventBus的子类AsyncEventBus

另外,EventBus也可作为Spring Bean使用,可被注入:

@Service
public class TestService implements InitializingBean {
	@Resource
	private MyListener myListener;
	
	@Resource
	private EventBus eventBus;
	
	public void postEvent() {
		eventBus.post(new LoginEvent("johnny", "success"));
	}
	
	@Override
	public void afterPropertiesSet() throws Exception {
		eventBus.register(myListener);
	}
}

源码

EventBus的register方法:

void register(Object listener) {
	Multimap<Class<?>, Subscriber> listenerMethods = findAllSubscribers(listener);
	for (Entry<Class<?>, Collection<Subscriber>> entry : listenerMethods.asMap().entrySet()) {
	Class<?> eventType = entry.getKey();
	Collection<Subscriber> eventMethodsInListener = entry.getValue();
	CopyOnWriteArraySet<Subscriber> eventSubscribers = subscribers.get(eventType);
	if (eventSubscribers == null) {
		CopyOnWriteArraySet<Subscriber> newSet = new CopyOnWriteArraySet<>();
		eventSubscribers = MoreObjects.firstNonNull(subscribers.putIfAbsent(eventType, newSet), newSet);
	}
	eventSubscribers.addAll(eventMethodsInListener);
	}
}

事件发送:执行指定事件类型的订阅者(包含method),从订阅者中获取指定事件的订阅者,然后按照规则(同步、异步)执行指定的方法,如果事件没有监听者,就当作死亡事件来对待。EventBus的post方法:

public void post(Object event) {
	Iterator<Subscriber> eventSubscribers = subscribers.getSubscribers(event);
	if (eventSubscribers.hasNext()) {
		dispatcher.dispatch(event, eventSubscribers);
	} else if (!(event instanceof DeadEvent)) {
		// the event had no subscribers and was not itself a DeadEvent
		post(new DeadEvent(this, event));
	}
}

EventBus的dispatcher为PerThreadQueuedDispatcher,其dispatch方法如下:

@Override
void dispatch(Object event, Iterator<Subscriber> subscribers) {
	checkNotNull(event);
	checkNotNull(subscribers);
	Queue<Event> queueForThread = queue.get();
	queueForThread.offer(new Event(event, subscribers));
	if (!dispatching.get()) {
		dispatching.set(true);
		try {
			Event nextEvent;
			while ((nextEvent = queueForThread.poll()) != null) {
				while (nextEvent.subscribers.hasNext()) {
					nextEvent.subscribers.next().dispatchEvent(nextEvent.event);
				}
			}
		} finally {
			dispatching.remove();
			queue.remove();
		}
	}
}

dispatchEvent方法:

// Dispatches event to this subscriber using the proper executor.
final void dispatchEvent(final Object event) {
	executor.execute(
		(Runnable) () -> {
			try {
				invokeSubscriberMethod(event);
			} catch (InvocationTargetException e) {
				bus.handleSubscriberException(e.getCause(), context(event));
			}
		});
}

execute方法由Executor来执行。EventBus的executor为MoreExecutors.directExecutor()

public static Executor directExecutor() {
	return DirectExecutor.INSTANCE;
}

enum DirectExecutor implements Executor {
	INSTANCE;
	
	@Override
	public void execute(Runnable command) {
		command.run();
	}
}

其execute方法直接执行线程的run方法,即同步调用run方法。另外,invokeSubscriberMethod方法如下:

void invokeSubscriberMethod(Object event) throws InvocationTargetException {
	try {
		method.invoke(target, checkNotNull(event));
	} catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) {
		// 省略catch代码
	}
}

因此,整个执行过程如下:
在这里插入图片描述
整个过程都是同步方式执行,EventBus是同步的。

AsyncEventBus

AsyncEventBus是异步EventBus,其dispatcher为LegacyAsyncDispatcher,executor为自己指定的线程池,如

@Configuration
public class ConfigBean {
	@Bean
	public EventBus executorService() {
		BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(20);
		ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 20, 30, TimeUnit.SECONDS, workQueue);
		return new AsyncEventBus(executor);
	}
}

运行流程如下:
在这里插入图片描述

AllowConcurrentEvents

Guava提供的注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@ElementTypesAreNonnullByDefault
public @interface AllowConcurrentEvents {
}

注解判断逻辑位于Subscriber.isDeclaredThreadSafe方法内:

private static boolean isDeclaredThreadSafe(Method method) {
	return method.getAnnotation(AllowConcurrentEvents.class) != null;
}

此方法被create()方法调用:

static Subscriber create(EventBus bus, Object listener, Method method) {
	return isDeclaredThreadSafe(method)
		? new Subscriber(bus, listener, method)
		: new SynchronizedSubscriber(bus, listener, method);
}

解读:如果订阅者方法上有注解@AllowConcurrentEvents,则返回Subscriber(异步),否则,返回SynchronizedSubscriber(同步)。即没有使用注解AllowConcurrentEvents的订阅者,在并发环境中是串行执行,会影响性能。

SynchronizedSubscriber是同步的,从其类名可知,从其实现的invokeSubscriberMethod方法里也可看出:

@Override
void invokeSubscriberMethod(Object event) throws InvocationTargetException {
	synchronized (this) {
		super.invokeSubscriberMethod(event);
	}
}

拓展

EventListener和EventBus

相同点:都是基于事件驱动模式,都可用于实现解耦的组件通信。

不同点:

  • 功能:
    • EventListener:订阅事件并执行操作的组件;粒度更细,聚焦于局部业务逻辑;一般是自定义类或方法,绑定到特定事件类型;
    • EventBus:管理事件的发布、订阅、流转;侧重于全局管理事件,通常是单例模式或共享对象。
  • 实现方式:
    • EventListener:Spring的@EventListener是一种更广义的事件监听机制,可配合ApplicationEvent或自定义事件;
    • EventBus:Guava EventBus通过注解定义事件处理器,自动绑定到特定事件类型。

联系:EventListener通常依赖EventBus进行注册,接收由EventBus发布的事件。

EventBus对比MQ

特性EventBusMQ
使用范围单JVM跨进程、分布式
通信模式内存中发布-订阅发布-订阅、点对点
可靠性无内置持久化或重试机制支持持久化、重试、消息确认
延迟低延迟相对稍高(取决于网络和队列机制)
复杂性简单,适合轻量场景较高,需要额外运维

Guava EventBus现状

  • 局限性:
    基于内存的轻量级工具,仅适用于单JVM内的事件管理;在分布式跨服务场景下,无法满足可靠性、持久性和扩展性要求。
  • 依然适用的场景:
    • 轻量化需求:在小型项目中,快速实现解耦的优秀选择;
    • 单JVM内部事件管理:用于模块间事件流转(如前端事件或服务内异步操作);
    • 开发测试:在测试环境中模拟事件驱动的逻辑。

Netflix EventBus

除了Guava EventBus外,Netflix也提供一套框架:

<dependency>
	<groupId>com.netflix.netflix-commons</groupId>
	<artifactId>netflix-eventbus</artifactId>
	<version>0.3.0</version>
</dependency>

参考

  • EventBus原理深度解析

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2245187.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

优先级队列PriorityQueue(堆)

1. 优先级队列 队列是一种先进先出的数据结构,而如果我们操作的数据带有优先级,我们出队的时候就会先出队优先级最高的元素.比如打游戏的时候有人给你打电话,操作系统看来有俩个进程,优先会处理打电话. 主要功能 1> 返回最高优先级对象 2> 添加新的对象 2. 堆的概念 2.1 …

【AI】人工智能报告解读——中国人工智能的发展

自 2016 年 AlphaGo 与世界顶级围棋选手对战后&#xff0c;AI 概念和技术从此走入大众视野。2017 年&#xff0c;国务院颁布《新一代人工智能发展规划》&#xff0c;这是中国在人工智能领域第一个部署文件&#xff0c;确定了人工智能产业发展的总体思路、战略目标和任务。技术和…

Flutter:photo_view图片预览功能

导入SDK photo_view: ^0.15.0单张图片预览&#xff0c;支持放大缩小 import package:flutter/material.dart; import package:photo_view/photo_view.dart;... ...class _MyHomePageState extends State<MyHomePage>{overrideWidget build(BuildContext context) {return…

uni-app 修改复选框checkbox选中后背景和字体颜色

编写css&#xff08;注意&#xff1a;这个样式必须写在App.vue里&#xff09; /* 复选框 */ /* 复选框-圆角 */ checkbox.checkbox-round .wx-checkbox-input, checkbox.checkbox-round .uni-checkbox-input {border-radius: 100rpx; } /* 复选框-背景颜色 */ checkbox.checkb…

c++中mystring运算符重载

#include <iostream> #include <cstring>using namespace std;class mystring {char* buf; public:mystring(); //构造函数mystring(const char * str); //构造函数mystring(const mystring& str); //深拷贝函数void show(); //输出函数void setmystr(const my…

oracle数据恢复—通过拼接数据库碎片的方式恢复Oracle数据的案例

Oracle数据库故障&#xff1a; 存储掉盘超过上限&#xff0c;lun无法识别。管理员重组存储的位图信息并导出lun&#xff0c;发现linux操作系统上部署的oracle数据库中有上百个数据文件的大小变为0kb。数据库的大小缩水了80%以上。 取出&并分析oracle数据库的控制文件。重组…

SpringBoot中小企业人事管理系统:设计模式

摘 要 随着科学技术的飞速发展&#xff0c;社会的方方面面、各行各业都在努力与现代的先进技术接轨&#xff0c;通过科技手段来提高自身的优势&#xff0c;中小企业人事管理系统当然也不能排除在外。中小企业人事管理系统是以实际运用为开发背景&#xff0c;运用软件工程原理和…

git分支合并某一次提交

1.A分支&#xff1a;使用git log --oneline查看需要合并的id 或者直接去Git仓库查看提交记录 2.B分支&#xff1a;git cherry-pick id 合并A分支的请求

NFS文件服务器

持久化存储&#xff1a;NFS 1 NFS 工作原理 NFS&#xff08;Network File System&#xff09;是一种分布式文件系统协议&#xff0c;它允许用户在网络上通过一个网络共享访问文件&#xff0c;就如同访问本地存储一样。NFS 工作时&#xff0c;服务端将文件系统中的一个或多个目…

Springboot 整合 Java DL4J 搭建智能问答系统

&#x1f9d1; 博主简介&#xff1a;CSDN博客专家&#xff0c;历代文学网&#xff08;PC端可以访问&#xff1a;https://literature.sinhy.com/#/literature?__c1000&#xff0c;移动端可微信小程序搜索“历代文学”&#xff09;总架构师&#xff0c;15年工作经验&#xff0c;…

Android kotlin之配置kapt编译器插件

配置项目目录下的gradle/libs.versions.toml文件&#xff0c;添加kapt配置项&#xff1a; 在模块目录下build.gradle.kt中增加 plugins {alias(libs.plugins.android.application)alias(libs.plugins.jetbrains.kotlin.android)// 增加该行alias(libs.plugins.jetbrains.kotl…

设计模式:4、命令模式(双重委托)

目录 0、定义 1、命令模式包括四种角色 2、命令模式的UML类图 3、代码示例 0、定义 将一个请求封装为一个对象&#xff0c;从而使用户可用不同的请求对客户进行参数化&#xff1b;对请求排队或记录请求日志&#xff0c;以及支持可撤销的操作。 1、命令模式包括四种角色 接…

详细教程-Linux上安装单机版的Hadoop

1、上传Hadoop安装包至linux并解压 tar -zxvf hadoop-2.6.0-cdh5.15.2.tar.gz 安装包&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1u59OLTJctKmm9YVWr_F-Cg 提取码&#xff1a;0pfj 2、配置免密码登录 生成秘钥&#xff1a; ssh-keygen -t rsa -P 将秘钥写入认…

短剧系统小程序开发产品设计实例解析

短剧系统小程序开发架构深度解析引言 随着数字娱乐市场的蓬勃发展&#xff0c;短剧因其紧凑的情节、创新的表现形式和便捷的观看体验&#xff0c;迅速吸引了大量观众的关注。作为承载短剧内容的重要平台&#xff0c;短剧系统小程序不仅需要在用户体验、内容管理等方面做到极致&…

STM32--JLINK下载问题记录

1.下载提示&#xff1a; 现象&#xff1a;keil下载&#xff0c;会提示如上信息&#xff1b; 使用segger jflash可以连接成功&#xff0c;但是下载程序会失败&#xff1b; 解决过程&#xff1a;尝试一边复位一边下载程序&#xff0c;一直失败&#xff1b;STM32CubeProgrammer也…

推荐算法(基于用户/物品的协同过滤算法)

1.1 推荐算法概述 信息过载的时代。信息消费者面临的问题是如何收集到自己感兴趣的信息。对于信息生产者来说&#xff0c;高效地把信息推送给感兴趣的信息消费者&#xff0c;而不是淹没在信息互联网的海洋之中&#xff0c;也非常困难。 如何从大量的数据信息中获取有价值的信息…

【PCIE常见面试问题-1】

PCIE常见面试问题-1 1 PCIE概述1.1 PCI为何发展开PCIE&#xff1f;1.2 什么是Root Complex(RC)1.3 什么是EP&#xff1f;1.4 什么是Swith1.5 PCIE协议如何组织通信的&#xff1f;1.6 简要介绍一下PCIE的分层结构&#xff0c;为什么需要分层&#xff1f;1.7 PCIE的事务类型有哪些…

Easyexcel(5-自定义列宽)

相关文章链接 Easyexcel&#xff08;1-注解使用&#xff09;Easyexcel&#xff08;2-文件读取&#xff09;Easyexcel&#xff08;3-文件导出&#xff09;Easyexcel&#xff08;4-模板文件&#xff09;Easyexcel&#xff08;5-自定义列宽&#xff09; 注解 ColumnWidth Data…

C++进阶:哈希表实现

目录 一:哈希表的概念 1.1直接定址法 1.2哈希冲突 1.3负载因子 1.4实现哈希函数的方法 1.4.1除法散列法/除留余数法 1.4.2乘法散列法 1.4.3全域散列法 1.5处理哈希冲突 1.5.1开放地址法 线性探测 二次探测 ​编辑 双重散列 1.5.2链地址法 二.代码实现 2.1开放地址…

汽车资讯新趋势:Spring Boot技术解读

5系统详细实现 5.1 管理员模块的实现 5.1.1 用户信息管理 汽车资讯网站的系统管理员可以管理用户&#xff0c;可以对用户信息修改删除审核以及查询操作。具体界面的展示如图5.1所示。 图5.1 用户信息管理界面 5.1.2 汽车品牌管理 系统管理员可以汽车品牌信息进行添加&#xf…