深入理解Java中的@EventListener注解及其应用场景

一、前言

@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);
    }

}

结果验证:

深入理解Java中的@EventListener注解及其应用场景
深入理解Java中的@EventListener注解及其应用场景

三、事件发布的其他一些玩法:

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);
    }

测试结果如下:可以监听到多个事件

深入理解Java中的@EventListener注解及其应用场景

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″的数据

深入理解Java中的@EventListener注解及其应用场景

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是我们主动发布的事件,ResponseLogEventhandleNotifyEvent 方法的返回值,会被 Spring 自动当作一个事件被发送。

深入理解Java中的@EventListener注解及其应用场景

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);
    }

查看结果可以发现,集合中的每个对象都被当作一个单独的事件进行发送。

深入理解Java中的@EventListener注解及其应用场景

4、异步监听器

当需要异步处理监听器时,可以在监听器方法上再增加另外的一个Spring注解 @Async,但需要注意以下限制:

监听器报错不会传递给事件发起者,因为双方已经不在同一个线程了。异步监听器的非空返回值不会被当作新的事件发布。如果需要发布新事件,需要注入 ApplicationEventPublisher后手动发布。

异步监听器就是在方法上加一个 @Async 标签即可(你可以指定线程池)。

  // 监听请求日志事件
    @Async
    @EventListener()
    public void handleNotifyEvent(LogEventEntity event) {
        log.info("监听到请求日志事件:" +
                "[{}]", event);

    }

从执行结果可以看出,异步线程是 task-1,不是主线程 main,即异步是生效的。

深入理解Java中的@EventListener注解及其应用场景

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中的@EventListener注解及其应用场景

四、项目中实际开发实例

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));
 }

}

五、总结:

  1. @EventListener的默认回调方法名是handleEvent,可以不指定方法名。
  2. @EventListener支持对事件进行过滤,可以通过在监听器类上添加@Filter注解并指定过滤器实现类来实现。
  3. @EventListener支持对事件进行排序,可以通过在监听器类上添加@Order注解并指定排序顺序来实现。
  4. 在Spring中,可以使用@Bean注解将一个普通的Java类转换为事件监听器,并将其注册到事件发布者中。
  5. 在使用@EventListener时,需要注意避免循环调用和内存泄漏等问题,特别是在监听器之间存在依赖关系时更需要谨慎。
  6. @EventListener的实现原理是基于观察者设计模式,它通过实现ApplicationListener接口来实现事件监听和订阅。

总之,@EventListener是Spring框架中一个非常有用的注解,可以帮助我们实现事件驱动编程,提高系统的可扩展性和可维护性。在使用时需要注意配置事件监听器和发布者之间的关系,以及正确地处理事件对象和参数传递等问题。同时,还需要注意避免循环调用和内存泄漏等问题,以保证程序的正确性和稳定性。


原文始发于微信公众号(明月予我):深入理解Java中的@EventListener注解及其应用场景

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/272755.html

(0)
明月予我的头像明月予我bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!