一、前言
@EventListener 是 Spring 框架提供的一种事件驱动编程的实现方式,在 Spring 4.2 版本之后出现。它是一种基于观察者设计模式的事件监听机制,用于解耦业务系统逻辑,提高可扩展性和可维护性。
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EventListener {
/**
* Alias for {@link #classes}.
*/
@AliasFor("classes")
Class<?>[] value() default {};
/**
* 可以处理的事件类型
*/
@AliasFor("value")
Class<?>[] classes() default {};
/**
SpEL表达式判断是否满足处理条件
*/
String condition() default "";
}
在 @EventListener 中,需要使用注解来建立事件对象,并在事件发布者中通过该注解寻找对应事件的监听者。具体来说,当一个事件发布后,Spring 框架会通过扫描 @EventListener 注解,找到监听该事件的 bean,并自动回调其对应的监听方法。
接下来,我们通过一个案例,来讲解具体怎么使用。
二、学习Demo
假设我们要记录系统内的接口请求是日志,以便后期出现问题进行溯源,我们来看看怎么做。
首先,我们需要定义一个日志事件类,用于表示请求被记录日志事件:
@Data
public class LogEventEntity implements Serializable {
private static final long serialVersionUID = 4344198495210544618L;
/**
* 时间
*/
private LocalDateTime time;
/**
* 参数
*/
private String params;
/**
* 说明
*/
private String message;
/**
* ip地址
*/
private String ipAddress;
}
然后,我们专门定义一个监听器类,用来处理请求日志记录事件。
@Slf4j
@Component
public class LogEventListener {
@EventListener()
public void handleNotifyEvent(LogEventEntity event) {
log.info("监听到请求日志事件:" +
"[{}]", event);
}
}
接着,通过 Http 接口来进行事件发布:
@RestController
@RequestMapping("/logEventPublish")
public class LogEventPublishController {
@Resource
private ApplicationContext applicationContext;
/**
* 发布事件
*/
@GetMapping("/publish")
public void publish() {
LogEventEntity logEventEntity = new LogEventEntity();
logEventEntity.setTime(LocalDateTime.now());
logEventEntity.setParams("A=A");
logEventEntity.setMessage("message");
logEventEntity.setIpAddress("127.0.0.1");
applicationContext.publishEvent(logEventEntity);
}
}
结果验证:


三、事件发布的其他一些玩法:
1、使用classes实现多事件监听器
如果一个监听器(被标注的方法)支持多种事件类型,那么需要使用注解的classes属性指定一个或多个支持的事件类型。
在上一个示例的基础上,再多加一个ResponseEvent事件。
/**
* 监听请求日志事件
* 注解:@EventListener(classes = {LogEventEntity.class, ResponseLogEvent.class})
* 参数:
* - event: 事件对象
*/
@EventListener(classes = {LogEventEntity.class, ResponseLogEvent.class})
public void handleNotifyEvent(Object event) {
log.info("监听到请求日志事件:" +
"[{}]", event);
}
/**
* 发布事件
*/
@GetMapping("/publish")
public void publish() {
LogEventEntity logEventEntity = new LogEventEntity();
logEventEntity.setTime(LocalDateTime.now());
logEventEntity.setParams("A=A");
logEventEntity.setMessage("message");
logEventEntity.setIpAddress("127.0.0.1");
applicationContext.publishEvent(logEventEntity);
}
/**
* 发布事件
*/
@GetMapping("/publishResponseEvent")
public void publishResponseEvent() {
ResponseLogEvent logEventEntity = new ResponseLogEvent();
logEventEntity.setTime(LocalDateTime.now());
logEventEntity.setParams("A=A");
logEventEntity.setMessage("message");
logEventEntity.setIpAddress("127.0.0.1");
applicationContext.publishEvent(logEventEntity);
}
测试结果如下:可以监听到多个事件

2、使用condition筛选监听的事件
可以通过 condition
属性指定一个SpEL表达式,如果返回 "true", "on", "yes", or "1"
中的任意一个,则事件会被处理,否则不会。
/**
* 处理 "message1" 消息的 LogEventEntity 事件监听方法
*
* @param event LogEventEntity类型的事件对象
*/
@EventListener(condition = "#event.getMessage().equals("message1")")
public void handleNotifyEvent(LogEventEntity event) {
log.info("监听到请求日志事件:" +
"[{}]", event);
}
/**
* 发布事件
*/
@GetMapping("/publish")
public void publish() {
LogEventEntity logEventEntity = new LogEventEntity();
logEventEntity.setTime(LocalDateTime.now());
logEventEntity.setParams("A=A");
logEventEntity.setMessage("message");
logEventEntity.setIpAddress("127.0.0.1");
applicationContext.publishEvent(logEventEntity);
LogEventEntity logEventEntity2 = new LogEventEntity();
logEventEntity2.setTime(LocalDateTime.now());
logEventEntity2.setParams("A=A");
logEventEntity2.setMessage("message1");
logEventEntity2.setIpAddress("127.0.0.1");
applicationContext.publishEvent(logEventEntity2);
}
结果验证:只打印了message==”message1″的数据

3、有返回值的监听器
被标注的方法可以没有返回值,也可以有返回值。当有返回值是,其返回值会被当作为一个新的事件发送。如果返回类型是数组或集合,那么数组或集合中的每个元素都作为一个新的单独事件被发送。
3.1返回一个单一对象
/**
* 发布事件
*/
@GetMapping("/publish")
public void publish() {
LogEventEntity logEventEntity = new LogEventEntity();
logEventEntity.setTime(LocalDateTime.now());
logEventEntity.setParams("A=A");
logEventEntity.setMessage("message");
logEventEntity.setIpAddress("127.0.0.1");
applicationContext.publishEvent(logEventEntity);
}
// 监听请求日志事件
@EventListener()
public ResponseLogEvent handleNotifyEvent(LogEventEntity event) {
log.info("监听到请求日志事件:" +
"[{}]", event);
ResponseLogEvent responseLogEvent = new ResponseLogEvent();
responseLogEvent.setTime(LocalDateTime.now());
responseLogEvent.setParams("A=A");
responseLogEvent.setMessage("response");
responseLogEvent.setIpAddress("127.0.0.1");
return responseLogEvent;
}
// 监听响应日志事件
@EventListener
public void handleHaveReturn(ResponseLogEvent responseLogEvent) {
log.info("监听到响应日志事件:" +
"[{}]", responseLogEvent);
}
验证结果:
可以看到我们监听到了2个事件,LogEventEntity
是我们主动发布的事件,ResponseLogEvent
是 handleNotifyEvent
方法的返回值,会被 Spring 自动当作一个事件被发送。

3.2返回一个集合
将监听器稍作修改,使其返回一个集合。
// 监听请求日志事件
@EventListener()
public List<ResponseLogEvent> handleNotifyEvent(LogEventEntity event) {
log.info("监听到请求日志事件:" +
"[{}]", event);
List<ResponseLogEvent> returnList = Lists.newArrayList();
ResponseLogEvent responseLogEvent1 = new ResponseLogEvent();
responseLogEvent1.setTime(LocalDateTime.now());
responseLogEvent1.setParams("A=A");
responseLogEvent1.setMessage("response1");
responseLogEvent1.setIpAddress("127.0.0.1");
returnList.add(responseLogEvent1);
ResponseLogEvent responseLogEvent2 = new ResponseLogEvent();
responseLogEvent2.setTime(LocalDateTime.now());
responseLogEvent2.setParams("B=B");
responseLogEvent2.setMessage("response2");
responseLogEvent2.setIpAddress("127.0.0.1");
returnList.add(responseLogEvent2);
return returnList;
}
// 监听响应日志事件
@EventListener
public void handleHaveReturn(ResponseLogEvent responseLogEvent) {
log.info("监听到响应日志事件:" +
"[{}]", responseLogEvent);
}
查看结果可以发现,集合中的每个对象都被当作一个单独的事件进行发送。

4、异步监听器
当需要异步处理监听器时,可以在监听器方法上再增加另外的一个Spring注解 @Async,但需要注意以下限制:
监听器报错不会传递给事件发起者,因为双方已经不在同一个线程了。异步监听器的非空返回值不会被当作新的事件发布。如果需要发布新事件,需要注入 ApplicationEventPublisher后手动发布。
异步监听器就是在方法上加一个 @Async
标签即可(你可以指定线程池)。
// 监听请求日志事件
@Async
@EventListener()
public void handleNotifyEvent(LogEventEntity event) {
log.info("监听到请求日志事件:" +
"[{}]", event);
}
从执行结果可以看出,异步线程是 task-1
,不是主线程 main
,即异步是生效的。

5、 监听器排序
如果同时有多个监听器监听同一个事件,默认情况下监听器的执行顺序是随机的,如果想要他们按照某种顺序执行,可以借助Spring的另外一个注解 @Order
实现。
创建三个监听器,并使用@Order
排好序。
// 监听请求日志事件
@Order(1)
@EventListener()
public void handleNotifyEvent1(LogEventEntity event) {
log.info("监听到请求日志事件1:" +
"[{}]", event);
}
// 监听请求日志事件
@Order(2)
@EventListener()
public void handleNotifyEvent2(LogEventEntity event) {
log.info("监听到请求日志事件2:" +
"[{}]", event);
}
// 监听请求日志事件
@Order(3)
@EventListener()
public void handleNotifyEvent3(LogEventEntity event) {
log.info("监听到请求日志事件3:" +
"[{}]", event);
}
从执行结果可以看到,确实是按照@Order
中指定的顺序执行的。

四、项目中实际开发实例
在Java开发中,日志记录是一项极其重要的任务。准确、完整、高效的日志记录对于开发人员来说至关重要,可以帮助我们更好地了解系统运行过程中的各种情况和异常。
我们需要记录项目中的每个接口的请求日志,考虑到记录日志使用事件模式可以将代码解耦,使得代码更加灵活和可扩展,如果直接调用处理记录日志的方法,那么每次需要添加新的功能或者修改现有功能时,都需要修改这个方法,这样会导致代码的耦合度很高,难以维护和扩展。
而且记录日志和接口方法逻辑没有交集,异步日志记录将日志的写入操作从主线程中分离出来,避免了阻塞主线程,从而提高了系统的吞吐量和响应速度。这对于处理大量并发请求的场景尤其有利。
而使用事件模式,可以将接口请求和处理日志的方法分离开来,当有新的功能需要添加时,只需要添加一个新的事件处理器即可,不需要修改原有的代码,这样可以大大提高代码的可维护性和可扩展性。
下面来看下具体代码:
首先定义一个切面,Java切面是一种非常有用的编程范式,它可以提高系统的可扩展性和可维护性,降低代码的耦合度,减少重复代码,提高代码的可读性和可维护性。
/**
* 系统日志记录切面注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SystemLog {
/**
* 描述
*
* @return {String}
*/
String value();
}
接着定义一个事件发布切面:
/**
* 操作日志使用spring event异步入库
*/
@Slf4j
@Aspect
@AllArgsConstructor
public class SysLogAspect {
private final ApplicationEventPublisher publisher;
@SneakyThrows
@Around("@annotation(systemLog)")
public Object around(ProceedingJoinPoint point, SystemLog systemLog) {
String strClassName = point.getTarget().getClass().getName();
String strMethodName = point.getSignature().getName();
log.debug("类名:[{}],方法:[{}]", strClassName, strMethodName);
cloud.epx.raven.admin.api.entity.SysLog logVo = SysLogUtils.getSysLog();
logVo.setTitle(systemLog.value());
// 发送异步日志事件
Long startTime = System.currentTimeMillis();
Object obj = point.proceed();
Long endTime = System.currentTimeMillis();
logVo.setTime(endTime - startTime);
publisher.publishEvent(new SysLogEvent(logVo));
return obj;
}
}
事件实体类:
@Data
@ApiModel(value = "日志")
public class SysLog implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 编号
*/
@TableId(type = IdType.AUTO)
@ApiModelProperty(value = "日志编号")
private Long id;
/**
* 日志类型
*/
@NotBlank(message = "日志类型不能为空")
@ApiModelProperty(value = "日志类型")
private String type;
/**
* 日志标题
*/
@NotBlank(message = "日志标题不能为空")
@ApiModelProperty(value = "日志标题")
private String title;
/**
* 创建者
*/
@ApiModelProperty(value = "创建人")
private String createBy;
/**
* 创建时间
*/
@ApiModelProperty(value = "创建时间")
private LocalDateTime createTime;
/**
* 更新时间
*/
@ApiModelProperty(value = "更新时间")
private LocalDateTime updateTime;
/**
* 操作IP地址
*/
@ApiModelProperty(value = "操作ip地址")
private String remoteAddr;
/**
* 用户代理
*/
@ApiModelProperty(value = "用户代理")
private String userAgent;
/**
* 请求URI
*/
@ApiModelProperty(value = "请求uri")
private String requestUri;
/**
* 操作方式
*/
@ApiModelProperty(value = "操作方式")
private String method;
/**
* 操作提交的数据
*/
@ApiModelProperty(value = "提交数据")
private String params;
/**
* 执行时间
*/
@ApiModelProperty(value = "方法执行时间")
private Long time;
/**
* 异常信息
*/
@ApiModelProperty(value = "异常信息")
private String exception;
/**
* 服务ID
*/
@ApiModelProperty(value = "应用标识")
private String serviceId;
/**
* 删除标记
*/
@TableLogic
@ApiModelProperty(value = "删除标记,1:已删除,0:正常")
private String delFlag;
}
/**
* @author leo 系统日志事件
*/
@Getter
@AllArgsConstructor
public class SysLogEvent {
private final SysLog sysLog;
}
异步监听日志事件:
/**
* @author leo 异步监听日志事件
*/
@AllArgsConstructor
public class SysLogListener {
private final RemoteLogService remoteLogService;
@Async
@Order
@EventListener(SysLogEvent.class)
public void saveSysLog(SysLogEvent event) {
SysLog sysLog = event.getSysLog();
remoteLogService.saveLog(sysLog, SecurityConstants.FROM_IN);
}
}
项目启动初始化日志路径:
/**
* <p>
* 初始化日志路径
*/
public class ApplicationLoggerInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
ConfigurableEnvironment environment = applicationContext.getEnvironment();
String appName = environment.getProperty("spring.application.name");
String logBase = environment.getProperty("LOGGING_PATH", "logs");
// spring boot admin 直接加载日志
System.setProperty("logging.file.name", String.format("%s/%s/debug.log", logBase, appName));
}
}
五、总结:
-
@EventListener的默认回调方法名是handleEvent,可以不指定方法名。 -
@EventListener支持对事件进行过滤,可以通过在监听器类上添加@Filter注解并指定过滤器实现类来实现。 -
@EventListener支持对事件进行排序,可以通过在监听器类上添加@Order注解并指定排序顺序来实现。 -
在Spring中,可以使用@Bean注解将一个普通的Java类转换为事件监听器,并将其注册到事件发布者中。 -
在使用@EventListener时,需要注意避免循环调用和内存泄漏等问题,特别是在监听器之间存在依赖关系时更需要谨慎。 -
@EventListener的实现原理是基于观察者设计模式,它通过实现ApplicationListener接口来实现事件监听和订阅。
总之,@EventListener是Spring框架中一个非常有用的注解,可以帮助我们实现事件驱动编程,提高系统的可扩展性和可维护性。在使用时需要注意配置事件监听器和发布者之间的关系,以及正确地处理事件对象和参数传递等问题。同时,还需要注意避免循环调用和内存泄漏等问题,以保证程序的正确性和稳定性。
原文始发于微信公众号(明月予我):深入理解Java中的@EventListener注解及其应用场景
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/272755.html