解耦代码的另一种方式-事务监听

背景

系统中存在部分需要异步处理的业务没有与主业务分离,导致当这部分业务出现异常时直接影响主业务。
例如登录操作需要执行业务A、B等到其它业务或者其它模块,当这些操作出现异常而没有异步的时候直接影响到登录业务。
当前讨论的主要是特定业务需求有需要异步处理的具体业务时的处理方式,对于共性化的事件如记录公共业务日志等可能还是考虑切面的方式实现。

考虑方案

如需要进行异步处理,可能考虑到的解决方案主要有:

  • mq消费,增加消费mq的方法进行相应消息不同业务的处理
  • 异步线程进行异步操作,或者将任务扔到线程池里处理

以上方法都能解决当前问题实现非主业务的异步分离,但是可能考虑可能存在的问题:

  • mq的方式需要依赖mq,发送消息到mq和消费mq消息时需要了解mq消息结构进行相应处理;对于后续对同样的事件做其他处理的人如不能提前了解到已有相应消息发到了mq就得再发一次消息到mq等
  • 启异步线程的方法抽取的不好会在主业务代码中出现大段、多处创建异步任务的代码;容易出现线程数量控制不好、线程池不能重复利用等问题

利用spring实现事件监听和处理

可以使用spring框架自带的事件监听方式,通过注解的形式实现异步事件的监听处理,使需要异步的业务与主业务逻辑分离解耦,并对同事件不同异步业务的处理提供良好的后续扩展

定义事件对象

定义一个事件对象,对象包含监听事件用到的属性:

public class LoginEvent {  
  
    private String email;  
    private String loginIp;  
    private String loginTime;  
  
    public LoginEvent(String email, String loginIp, String loginTime) {  
        this.email = email;  
        this.loginIp = loginIp;  
        this.loginTime = loginTime;  
    }  
    // ...
}

监听对应事件进行处理

编写监听方法,入参为需要监听的事件对象,执行具体事件:

@Component  
public class LoginEventListener {  

	/**
	* 业务A
	**/
    @Async  
    @EventListener    
    public void A...(LoginEvent loginEvent) {  
        // 具体业务处理  
    }  
      
}
  • @Async 注解表示将通过异步的方式执行该方法,缺少该注解事件的具体执行将不是异步的方式
  • @EventListener 事件监听注解,有该注解才会执行具体事件的监听

事件发布

定义了事件对象,有了事件的具体执行方法,剩下只需要在需要发布事件的逻辑处增加对事件的发布即可

  • 通过注入的方式发布事件
@Autowired  
private ApplicationEventPublisher applicationEventPublisher;  
 
public void login() {  
    //登录业务的主业务逻辑  
    applicationEventPublisher.publishEvent(new LoginEvent("123@qq.com", "127.0.0.1", "date"));  
}
  • 利用ApplicationContextUtil直接发布事件
public void login() {  
    //登录业务的主业务逻辑  
    ApplicationContextUtil.getContext().publishEvent(new LoginEvent("email", "192.168.1.1", "time"));  
}

使用Idea的话会发现事件发布的代码处会显示小耳机,可查看所有监听执行了该事件的方法:

至此,对于登录业务下需要异步执行A业务即完成,A业务与主业务分离,即使出错、超时等也不影响登录本身逻辑。

同事件多业务处理

假如现在对于登录之后的业务除了业务A又新增了业务B需求,虽然可以在原来A方法中增加业务,
但本着一个方法做一件事、业务互相独立的原则,并且事件本来支持多处监听处理,我们再增加对登录事件的调用方法B即可:

/**  
 * 业务A  
 */@Async  
@EventListener  
public void A...(LoginEvent loginEvent) {  
    // 具体业务处理  
}  
  
/**  
 * 业务B  
 */  
@Async  
@EventListener  
public void B...(LoginEvent loginEvent) {  
    // 具体业务处理  
}
  • 后续指定异步逻辑不再需要的话只需去除或者注释 @EventListener 注解,事件发布处的代码无需改动,没有事件监听注解的方法将不再执行

主业务事务提交后执行监听事件

有些场景下异步处理的事件需要依赖主业务对于数据的操作结果,例如用户注册后可能需要查询用户信息发送新用户消息等,如果异步处理的事件执行时主业务的事务还未提交,会导致异步事件查询不到传入的用户信息,就需要让异步监听的事件在主业务事务提交后执行,只需将
@EventListener 注解改为 @TransactionalEventListener 注解:

@Async  
@TransactionalEventListener  
public void A...(LoginEvent loginEvent) {  
    // 具体A业务处理  
}
  • @TransactionalEventListener 注解下的方法将在发布事件处的主业务事务提交后处理,若发布事件处没有开启事务将不会执行到该监听方法,
    注解默认在事务提交后执行,可通过注解下的 phase 属性修改事务前后执行条件

使用指定线程池执行异步事件

@Async 注解默认使用SimpleAsyncTaskExecutor执行异步,SimpleAsyncTaskExecutor 不重用线程,每次调用都会创建一个新的线程,
若需要指定线程池,可通过定义线程池,在使用 @Async 注解时指定线程池名称来使用指定线程池:

  • 线程池定义举例:
@Configuration  
@EnableAsync  
public class PoolConfig {  
  
    //最大的缓存任务数  
    private static final int MAXIMUM_CACHED_AMOUNT = 20000;  
  
    public static final String BASE_THREAD_POOL_NAME = "BASE_THREAD_POOL_NAME";  
  
  
    @Bean(BASE_THREAD_POOL_NAME)  
    public Executor accountExecutor() {  
        int processorAmount = Runtime.getRuntime().availableProcessors();  
        int coreThreadSize = processorAmount * 2 + 1;  
        Executor threadPoolExecutor = new ThreadPoolExecutor(coreThreadSize, coreThreadSize + 1, 60, TimeUnit.SECONDS,  
                new LinkedBlockingQueue<Runnable>(MAXIMUM_CACHED_AMOUNT), new AsyncAccountThreadFactory("base-%s-thread-"), new RejectedPolicy());  
        return threadPoolExecutor;  
    }
}
  • 注解使用指定线程池异步:
@Async(PoolConfig.BASE_THREAD_POOL_NAME)  
@EventListener  
public void A...(LoginEvent loginEvent) {  
    // 具体A业务处理  
}

拓展:

一. ApplicationContextUtil

ApplicationContextUtil 是一个自定义的工具类,用于获取 Spring ApplicationContext 对象。它可以帮助在非 Spring 管理的类中获取到 ApplicationContext 实例。

下面是一个示例 ApplicationContextUtil 类的实现:

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class ApplicationContextUtil implements ApplicationContextAware {
    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext context) throws BeansException {
        applicationContext = context;
    }

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    public static <T> T getBean(Class<T> beanClass) {
        return applicationContext.getBean(beanClass);
    }

    public static Object getBean(String beanName) {
        return applicationContext.getBean(beanName);
    }
}

这个工具类通过实现 ApplicationContextAware 接口,并使用 @Component 注解将其作为一个 Spring Bean 进行管理。在 setApplicationContext 方法中,保存传入的 ApplicationContext 对象。

ApplicationContextUtil 提供了两个方法来获取 ApplicationContext 对象:

  • getApplicationContext():返回保存的 ApplicationContext 对象。
  • getBean() 方法重载:可以通过传入 Class 类型或者 Bean 的名称来获取对应的 Bean 实例。

使用 ApplicationContextUtil 类,你可以在任何非 Spring 管理的类中获取到 ApplicationContext 对象,并进一步使用 ApplicationContext 提供的各种功能,如获取其他 Bean、访问配置属性等。

需要注意的是,在使用 ApplicationContextUtil 类之前,确保 Spring 容器已经初始化完成,即 ApplicationContext 对象已经被正确地设置到 ApplicationContextUtil 中。

 

二.applicationEventPublisher.publishEvent

applicationEventPublisher.publishEvent 是 Spring Framework 中用于发布应用程序事件的方法。它是在实现了 ApplicationEventPublisher 接口的对象上调用的。

在 Spring 应用程序中,你可以使用 applicationEventPublisher.publishEvent 来发布自定义的应用程序事件。这些事件可以在应用程序内部的不同组件间进行通信和交互。

以下是使用 applicationEventPublisher.publishEvent 的一般步骤:

  1. 创建一个继承自 ApplicationEvent 的自定义事件类。你可以根据应用程序的需求定义事件类,包含适当的属性和方法,用于传递相关信息。
  2. 在你想要发布事件的地方,获取到 applicationEventPublisher 对象。可以通过依赖注入或自动装配方式获得该对象实例。
  3. 调用 applicationEventPublisher.publishEvent 方法,将自定义事件对象作为参数传递给该方法。Spring 框架将负责将事件广播给所有注册的事件监听器。
  4. 在应用程序中定义和注册事件监听器。通过实现 ApplicationListener 或使用 @EventListener 注解,你可以创建事件监听器来处理接收到的事件。

applicationEventPublisher.publishEvent 方法被调用时,Spring 框架将会触发相应的事件,并将事件广播给所有已注册的事件监听器。监听器可以根据事件的类型和内容执行相应的操作,例如处理业务逻辑、更新数据等。

需要注意的是,applicationEventPublisher.publishEvent 方法通常在 Spring 容器中可用,因此你需要确保在正确的上下文环境中调用该方法,以便事件可以被正确地发布和接收。

 

三.@TransactionalEventListene

@TransactionalEventListener 是 Spring Framework 提供的一个注解,用于在事务完成后处理事件。它可以将事件处理与事务管理结合起来,以确保在事务成功提交后再处理事件。

使用 @TransactionalEventListener 注解可以实现在事务完成后触发相应的事件监听器。

  1. 在监听器的方法中,使用 @TransactionalEventListener 注解标记要处理的事件,可以通过参数指定事件的类型。
  2. 通过在配置文件中启用事件驱动机制,或者使用 Spring Boot 进行自动配置,让 Spring 自动扫描并注册事件监听器。

当使用 @TransactionalEventListener 注解标记的方法被调用时,如果当前存在事务,并且事务成功提交,那么该方法将会被执行。这样就确保了事件处理的顺序与事务的提交顺序一致。

另外,你还可以通过配置 phase 属性来指定事件监听器的执行顺序。默认情况下,@TransactionalEventListener 执行在事务提交后的默认阶段中,但你可以根据需要显式指定 phase 属性的值,以调整它的执行顺序。

需要注意的是,@TransactionalEventListener 注解只对通过 Spring 进行事务管理的方法起作用。如果你正在使用其他事务管理机制,例如 EJB 事务等,可能无法正常工作。

 

@TransactionalEventListener 注解的 phase 属性用于指定事件监听器的执行阶段。通过设置 phase 属性,可以控制事件监听器在事务提交后的不同阶段执行。

phase 属性可以设置为 TransactionPhase 枚举的常量之一,它定义了以下几个阶段:

  • AFTER_COMMIT(默认值):表示在事务成功提交后执行。
  • AFTER_ROLLBACK:表示在事务回滚后执行。
  • AFTER_COMPLETION:表示在事务完成(提交或回滚)后执行。
  • BEFORE_COMMIT:表示在事务准备提交时执行。
  • BEFORE_ROLLBACK:表示在事务准备回滚时执行。

以下是使用 @TransactionalEventListener 注解并设置 phase 属性的示例:

@Component
public class MyEventListener {

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void handleMyEventAfterCommit(MyEvent event) {
        // 处理事件的逻辑(在事务成功提交后执行)
        // ...
    }

    @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
    public void handleMyEventAfterRollback(MyEvent event) {
        // 处理事件的逻辑(在事务回滚后执行)
        // ...
    }

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)
    public void handleMyEventAfterCompletion(MyEvent event) {
        // 处理事件的逻辑(在事务完成后执行)
        // ...
    }

    @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
    public void handleMyEventBeforeCommit(MyEvent event) {
        // 处理事件的逻辑(在事务准备提交时执行)
        // ...
    }

    @TransactionalEventListener(phase = TransactionPhase.BEFORE_ROLLBACK)
    public void handleMyEventBeforeRollback(MyEvent event) {
        // 处理事件的逻辑(在事务准备回滚时执行)
        // ...
    }
}

通过设置不同的 phase 值,可以控制事件监听器在不同的事务阶段执行。例如,你可以将一些敏感操作放在 AFTER_COMMIT 阶段,确保事务成功提交后再执行。

需要注意的是,事件监听器的执行顺序受到事务提交和回滚的影响。如果事务回滚,那么在 AFTER_COMMIT 或者 BEFORE_COMMIT 阶段注册的事件监听器不会被触发。

posted @ 2023-12-14 11:44  风浪很小  阅读(94)  评论(0编辑  收藏  举报