Spring的事件处理机制

1.前言

最近看公司代码,发现针对消息队列的消费和分发做了很多很精妙的设计,部分代码如下所示:

@Component
public class HMSStartupListener implements ApplicationListener<SoaStartedEvent> {
    
    

    @Override
    public void onApplicationEvent(SoaStartedEvent soaStartedEvent) {
    
    
        LogUtil.infoLog().log("HMS Service start Initialize");
        String hostAddress = "";
        try {
    
    
            hostAddress = Inet4Address.getLocalHost().getHostAddress();
        } catch (Exception e) {
    
    
            LogUtil.errorLog().log("获取IP地址异常", e);
        }

        if (Arrays.asList(ApolloConfigUtil.getArrayProperty(ApolloConstant.HMS_IP_BLACKLIST, ",", new String[]{
    
    })).contains(hostAddress)) {
    
    
            LogUtil.errorLog().log("{} in hms blacklist", hostAddress);
            return;
        }
        Stream.of(HMSConsumer.values())
                .filter(HMSConsumer::getEnabled)
                .forEach(hmsConsumer -> {
    
    
                    Properties properties = new Properties();
                    properties.setProperty("consumeThreadMin", hmsConsumer.getThreads().toString());
                    Set<String> tags = new HashSet<>();
                    if (hmsConsumer.getTags() != null) {
    
    
                        Collections.addAll(tags, hmsConsumer.getTags().split(","));
                    }
                    Hms.subscribe(hmsConsumer.getName(), tags, soaStartedEvent.getCtx().getBean(hmsConsumer.getMessageListener()), properties);
                });
        LogUtil.infoLog().log("HMS Service Initialize Complete");
    }
}

由于一些限制,没法针对代码做逐行讲解,只能借着一个点,把相关的核心技术点讲解一下,这个核心技术点就是Spring的事件监听机制ApplicationListener。

Spring中的事件处理机制是对设计模式中的观察者模式的一种扩展,它可以实现应用程序中的解耦,提高代码的可维护性和可扩展性。

在传统的观察者模式中,最主要的对象有观察者,被观察者,当被观察者的状态或行为发生改变时,所有的观察者将会收到通知,并作出相应的响应。比如当红绿灯(被观察者)由红灯变为绿灯(被观察者状态改变)状态时,驾驶员(观察者)会继续行驶汽车(观察者作出的响应)。

在Spring事件处理机制中,涉及到的对象主要有:事件监听器、事件源、事件、多播器;

  • 事件:ApplicationEvent
    事件是一个抽象的概念,它代表着应用程序中的某个动作或状态的发生。
  • 事件源:事件源就是产生事件的对象,对应于传统观察者模式中的被观察者,用于生成事件对象。
  • 事件监听器:ApplicationListener
    事件监听器是事件的接收者,它负责处理事件并执行相应的操作。
  • 多播器:ApplicationEventMulticaster
    可以理解为Spring中的事件管理中心,类似于广播站,向事件监听器广播事件信息,当事件源产生事件后,并不由事件源去一个个的通知事件监听器,而是由多播器向Spring中的所有事件监听器发送事件,这样做的好处就是每一个事件源不必再分别维护一个容器中所有事件监听器的列表,减少开销,且代码更加简洁(通知事件监听器的代码不必在每个事件源内重复编写)。

在 Spring 的事件机制中,事件源和事件监听器之间通过事件进行通信,从而实现了模块之间的解耦。

2.事件机制三要素

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AJi6wUiA-1691934577584)(6.png)]

2.1 定义事件

通过继承ApplicationEvent,实现自定义事件,是对 Java EventObject 的扩展,表示 Spring 的事件,Spring 中的所有事件都要基于其进行扩展。其源码如下:

public abstract class ApplicationEvent extends EventObject {
    
    
    private static final long serialVersionUID = 7099057708183571937L;
    private final long timestamp = System.currentTimeMillis();

    public ApplicationEvent(Object source) {
    
    
        super(source);
    }

    public final long getTimestamp() {
    
    
        return this.timestamp;
    }
}

我们自定义事件的话只需要继承ApplicationEvent即可

public class SoaStartedEvent extends ApplicationEvent {
    
    
    private String userId;

    public SoaStartedEvent(String userId) {
    
    
        super(new Object());
        this.userId = userId;
    }
}

继承后定义了一个 userId,有一个 SoaStartedEvent(PS:公司源码无法呈现,无法一一进行讲解,只能做一些修改后进行讲解)方法。这里就定义我们监听器需要的业务参数,监听器需要监听什么参数,我们这里就定义这些参数。

2.2 监听事件

实现监听器的方法有两种:

  • 实现 ApplicationListener 接口
  • 使用 EventListener 注解
  • 使用

下面我逐一讲解一下

1.实现 ApplicationListener 接口

ApplicationListener 是 Spring 事件的监听器,用来接受事件,所有的监听器都必须实现该接口。该接口源码如下。

@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
    
    
    void onApplicationEvent(E var1);
}

我们只需要新建一个类用于实现 ApplicationListener 接口,并且重写 onApplicationEvent 方法,并将该类注入到 Spring 容器中,交给 Spring 管理即可。如下代码:

@Component
public class MessageListener implements ApplicationListener<SoaStartedEvent> {
    
    

    @Override
    public void onApplicationEvent(SoaStartedEvent event) {
    
    
        System.out.println("收到事件:" + event);
        System.out.println("开始执行业务操作给用户发送短信。用户userId为:" + event.getUserId());
    }
}

新建了一个发送短信监听器,收到事件后执行业务操作。

2.使用 EventListener 注解

使用@EventListener注解标注处理事件的方法,此时Spring将创建一个ApplicationListenerbean对象,使用给定的方法处理事件源码如下参数可以给指定的事件这里巧妙的用到了@AliasFor的能力,放到了@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 "";

    String id() default "";
}

我们的使用方式也很简单,新建一个事件监听器,注入到 Spring 容器中,交给 Spring 管理。在指定方法上添加 @EventListener 参数为监听的事件。方法为业务代码。使用 @EventListener 注解的好处是一个类可以写很多监听器,定向监听不同的事件,或者同一个事件。

@Component
public class MessageEventListener {
    
    
    @EventListener({
    
    SoaStartedEvent.class})
    public void LogListener(SoaStartedEvent event) {
    
    
        System.out.println("收到事件:" + event);
        System.out.println("开始执行业务操作生成关键日志。用户userId为:" + event.getUserId());
    }

    @EventListener({
    
    SoaStartedEvent.class})
    public void messageListener(SoaStartedEvent event) {
    
    
        System.out.println("收到事件:" + event);
        System.out.println("开始执行业务操作给用户发送短信。用户userId为:" + event.getUserId());
    }
}

和@EventListener类似的还有一个是@TransactionalEventListener

3.@TransactionalEventListener注解

这两者有什么区别呢?

@EventListener是用来标识在方法上,使得该方法可以监听事件,在事件发布时调用方法处理事件(作用与实现ApplicationListener接口的事件监听器一样。

@TransactionalEventListener的注解上标识@EventListener注解,相当于继承了@EventListener注解的功能,并添加了新的特性。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kg1vIPHH-1691934577586)(1.png)]
在Spring中,使用@TransactionalEventListener注解可以将事件处理与事务管理结合起来。

@TransactionalEventListener注解用于标记一个方法,表示它是一个事件监听器,并且该方法应该在事务的不同阶段进行调用。

另外, 如果在@TransactionalEventListener注解的方法中传入一个DO类(也就是实体类),该方法也会在事务提交后执行。在方法中可以对这个DO进行任何操作,包括修改、删除等。当事务成功提交时,对DO的更改也会被持久化到数据库中。

在注解上,可以使用以下属性来指定要监听的事件类型、事务阶段以及是否使用异步方式处理事件:

  • value:指定要监听的事件类型,可以是单个事件类型或者一个事件类型的数组。
  • phase:指定要监听的事务阶段,可以是AFTER_COMMIT、AFTER_ROLLBACK或AFTER_COMPLETION。默认是AFTER_COMMIT。
  • fallbackExecution:指定在事务未提交或回滚时是否执行事件监听器方法,默认为false。
  • condition:指定一个SpEL表达式,用于决定是否要触发监听器方法。
  • executor:指定一个TaskExecutor,用于在异步模式下处理事件。

使用case:

首先,需要加入tx依赖

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
        </dependency>

确保在应用程序的配置类上启用事务管理器,例如使用 @EnableTransactionManagement 注解:

@Configuration
@EnableTransactionManagement
public class AppConfig {
    
    
    // 配置数据源、实体管理器等
}

然后,在需要触发事件的方法上添加 @TransactionalEventListener 注解,并指定要处理的事件类型:

@Service
public class UserService {
    
    
    @Autowired
    private ApplicationEventPublisher eventPublisher;

    @Transactional
    public void createUser(User user) {
    
    
        // 创建用户的逻辑
        eventPublisher.publishEvent(new UserCreatedEvent(user));
    }

    @TransactionalEventListener
    public void handleUserCreatedEvent(UserCreatedEvent event) {
    
    
        // 处理用户创建事件的逻辑
    }
}

上述代码中,createUser 方法使用了 @Transactional 注解,表示该方法运行在一个事务中。在方法执行完毕后,会触发一个 UserCreatedEvent 事件,并由 handleUserCreatedEvent 方法进行处理。由于 handleUserCreatedEvent 方法在事务提交之后被调用,它可以安全地使用已提交的数据。

2.3 发布事件

我们可以使用ApplicationContext进行发布,由于ApplicationContext已经继承了ApplicationEventPublisher,因此可以直接使用发布事件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EeCyIZUu-1691934577586)(2.png)]

        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("application.properties");
        applicationContext.publishEvent(new SoaStartedEvent("1"));//发布事件

也可以注入ApplicationEventPublisher进行发布
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RFyfMyRX-1691934577587)(3.png)]

3.异步事件

监听器默认是同步执行的,如果我们想实现异步执行,可以搭配 @Async 注解使用。

监听器默认是同步执行的,如果我们想实现异步执行,可以搭配@Async注解使用,但是前提条件是你真的懂@Async注解,使用不当会出现问题的

使用@Async时,需要配置线程池,否则用的还是默认的线程池也就是主线程池,线程池使用不当会浪费资源,严重的会出现OOM事故。

下图是阿里巴巴开发手册的强制要求:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rsqj1PWg-1691934577587)(4.png)]
如果你能够承担@Async的使用风险,可以继续使用它

1.在启动类添加 @EnableAsync 开启异步执行配置

@SpringBootApplication
@EnableAsync
public class Demo1Application {
    
    

    public static void main(String[] args) {
    
    
        SpringApplication.run(Demo1Application.class, args);
    }

}

2.在我们想要异步执行的监听器上添加 @Async 注解。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yA4bicRG-1691934577588)(5.png)]

有了上面的理论,我们下面具体来实操一下,感受一下

4.实战

在进行具体的代码设计之前,把上面的几个概念在总结一下:

  • 事件源:事件的触发者,比如上面的注册器就是事件源。
  • 事件:描述发生了什么事情的对象,比如上面的:xxx注册成功的事件
  • 事件监听器:监听到事件发生的时候,做一些处理

剩余内容暂未更新…

猜你喜欢

转载自blog.csdn.net/zhiyikeji/article/details/132266049