Spring Dao编程高级-事务监听器

从 0 开始深入学习 Spring 专栏目录总览

OK 咱马不停蹄的继续来学习 Dao 编程中的一些高级知识。

想必小伙伴们看标题会产生一种很复杂的心情吧:事务也有监听器?事务也需要监听器?来,咱先思考几个小问题:

  • 事务执行成功,提交之后,通知特定的组件发送成功的消息
  • 事务执行失败,回滚完事务之后,记录失败的日志
  • 无论事务执行成功还是失败,只要事务执行完成,就记录事务执行的日志

可以发现一个特点,这些事务执行之后的动作,大多都不是主干业务逻辑中的内容,而是不太重要的、可能还比较耗时(譬如发短信、发邮件等)。这种动作一般不会耦合进 Service 的业务逻辑中,而是使用 AOP 增强,或者使用监听器 + 事件更为合适。AOP 咱就不说了,这一章咱是讲事务监听器,那咱就来学习一下事务监听器的使用。

注意,由于事务监听器在实际开发中使用的并不多,所以本章小伙伴只需要了解、知道、会用即可。

1. 事务监听器的设计【了解】

事务的监听器,是基于 SpringFramework 的监听器扩展的,它来自 SpringFramework 4.2 (所以之前的版本是没有的哦)。

咱可以先来看一下事务事件监听器的核心注解:@TransactionalEventListener :

@EventListener
public @interface TransactionalEventListener {
    TransactionPhase phase() default TransactionPhase.AFTER_COMMIT;

    boolean fallbackExecution() default false;
    
    @AliasFor(annotation = EventListener.class, attribute = "classes")
    Class<?>[] value() default {};

    @AliasFor(annotation = EventListener.class, attribute = "classes")
    Class<?>[] classes() default {};

    String condition() default "";
}

可以发现,其实事务事件监听器的本质,还是一个普通的注解式事件监听器而已,只是它里面设计的字段与 @EventListener 不同罢了。

另外注意 @TransactionalEventListener 注解中的一个最最关键的属性:phase ,它代表的是在事务的哪个阶段触发监听,默认值是 AFTER_COMMIT ,代表提交之后触发。这个触发时机共有 4 个可选择的范围:

  • BEFORE_COMMIT :事务提交之前触发监听
  • AFTER_COMMIT :事务提交之后触发监听
  • AFTER_ROLLBACK :事务回滚之后触发监听
  • AFTER_COMPLETION :事务完成之后触发监听(无论提交或回滚均触发)

下面的 fallbackExecution 属性,代表没有事务时是否触发事件监听。这个属性一般我们不会用到,因为一个方法的执行既可能在事务中,也可能不在事务中,那该方法的事务传播行为一定为 SUPPORTS ,而传播行为是 SUPPORTS 的方法本来就用的少,所以这个 fallbackExecution 属性也几乎不需要设置。当然,如果属性设置为 true 的话,则方法无论有没有事务,都会触发事件监听。

再下面的 value 或者 classes ,那就是映射到 @EventListener 注解的属性咯,表示要监听哪些事件,或者监听哪些类型的 Payload 事件。(如果小伙伴们有点忘记 Payload 的话,可以回看第 31 章第 2 小节的内容哦)

2. 快速使用事务监听器【会用】

OK ,了解了事务监听器的核心注解,接下来我们就来实际搞一下吧。

本章源码均在 com.linkedbear.spring.transaction.f_listener 下。

2.1 Dao和Service

先把基本的代码都准备一下吧,咱快速编写一下,能复制粘贴的就复制粘贴。

先来准备 UserDao ,在 c_declarativexml.dao 包中已经有现成的了,直接复制粘贴过来就行。

UserService 就不复制了,咱写一个不带异常的吧:

@Service
public class UserService {
    
    @Autowired
    UserDao userDao;
    
    @Transactional
    public void saveUser() {
        User user = new User();
        user.setName("哈哈哈");
        user.setTel("123");
    
        userDao.save(user);
    }
}

2.2 编写监听器

接下来,要写监听器了,之前咱已经学过基于注解的事件监听器了,小伙伴们一定都还记得吧:

@Component
public class UserTransactionListener {
    
    @TransactionalEventListener
    public void onSaveUser() {
        System.out.println("监听到保存用户事务提交 ......");
    }
}

然后要写监听的事件了,该监听啥呢?事件提交之后会发布事件吗?有什么内部事件吗?

emmm 想多了,其实并没有,事件是需要自己发布的(是不是有点失望)。所以咱还需要在 UserService 的 saveUser 方法中添加一个事件的发布。

既然是 User 的保存,那就发一个 Payload 事件吧,还简单方便一些:(记得注入 ApplicationEventPublisher 或者 ApplicationContext )

@Autowired
ApplicationEventPublisher eventPublisher;

@Transactional
public void saveUser() {
    User user = new User();
    user.setName("哈哈哈");
    user.setTel("123");

    userDao.save(user);
    System.out.println("User save ......");
    eventPublisher.publishEvent(user);
}

接下来在监听器方法的参数上声明 PayloadApplicationEvent 和泛型:

@TransactionalEventListener
public void onSaveUser(PayloadApplicationEvent<User> event) {
    System.out.println("监听到保存用户事务提交 ......");
}

@TransactionalEventListener 注解在不声明其余属性时,默认监听的是事务成功提交后的事件触发。

2.3 编写配置类

配置类的编写那是相当简单常规了吧,咱就不多解释了,直接上代码:

@Configuration
@EnableTransactionManagement
@ComponentScan("com.linkedbear.spring.transaction.f_listener")
public class TransactionListenerConfiguration {
    
    @Bean
    public DataSource dataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/spring-dao?characterEncoding=utf8");
        dataSource.setUsername("root");
        dataSource.setPassword("123456");
        return dataSource;
    }
    
    @Bean
    public JdbcTemplate jdbcTemplate() {
        return new JdbcTemplate(dataSource());
    }
    
    @Bean
    public TransactionManager transactionManager() {
        return new DataSourceTransactionManager(dataSource());
    }
}

2.4 测试运行

接下来,用上面的配置类,驱动 IOC 容器,并取出 UserService 并调用 saveUser 方法:

public class TransactionListenerApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
                TransactionListenerConfiguration.class);
        UserService userService = ctx.getBean(UserService.class);
        userService.saveUser();
    }
}

运行 main 方法,控制台可以打印出事务监听器的执行,证明事务事件监听器已生效。

User save ......
监听到保存用户事务提交 ......

2.5 事件触发的四种时机

上面说了,事务监听的时机有 4 种,下面咱都写一遍,来实际测试一下。

@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void onSaveUser(PayloadApplicationEvent<User> event) {
    System.out.println("监听到保存用户事务准备提交 ......");
}

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onSaveUser2(PayloadApplicationEvent<User> event) {
    System.out.println("监听到保存用户事务提交成功 ......");
}

@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void onSaveUser3(PayloadApplicationEvent<User> event) {
    System.out.println("监听到保存用户事务回滚 ......");
}

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)
public void onSaveUser4(PayloadApplicationEvent<User> event) {
    System.out.println("监听到保存用户事务完成 ......");
}

之后重新运行 main 方法,可以发现控制台打印了 3 条输出:

User save ......
监听到保存用户事务准备提交 ......
监听到保存用户事务完成 ......
监听到保存用户事务提交成功 ......

注意看顺序,事件监听的触发顺序是 BEFORE_COMMIT → AFTER_COMPLETION → AFTER_COMMIT ,完成的时机早于成功(注意这里说的不对!!!)

再就是除零异常:

@Transactional
public void saveUser() {
    User user = new User();
    user.setName("哈哈哈");
    user.setTel("123");

    userDao.save(user);
    System.out.println("User save ......");
    eventPublisher.publishEvent(user);
    int i = 1 / 0;
}

重新运行 main 方法,可以发现控制台打印了两条输出:

User save ......
监听到保存用户事务完成 ......
监听到保存用户事务回滚 ......

也是注意一下事件监听触发的顺序,AFTER_ROLLBACK 的触发时机靠后,说明完成的时机也早于回滚(注意这里说的不对!!!)

2.6 【订正】事件触发的顺序

2021 年 5 月 17 日订正,上面 2.5 节的事件触发顺序其实是不准确的,有小伙伴反映运行结果与小册不一致,经过阿熊实测后发现有异常情况,所以这里小册作一个内容修正。

上面同样的代码,反复运行之后发现控制台打印的事务完成和事务回滚各有先后,这样会造成效果混乱,对于一个成熟的软件来讲是不能被接受的,所以我们需要找到原因,并给予对应的解决方案。

2.6.1 现象溯源

首先我们来说引起上述错误现象的根源,我们定义的所有事务事件监听器,最终还是会封装为一个一个的 ApplicationListener ,并保存在 IOC 容器中(准确的说应该是 ApplicationEventMulticaster 中)。当事件广播时,我们可以通过 Debug 的方式找到这 4 个监听器:

Spring Dao编程高级-事务监听器

这样看貌似不大方便,我们 toString 一下再观察:

Spring Dao编程高级-事务监听器

可以发现四个事件监听的方法顺序完全不对劲!乱七八糟的!这也就导致了后面在封装事务事件监听器时,顺序也跟着混乱了。

以下是两次 Debug 的事件监听器顺序观察:

这是第一次 Debug 的观察结果:

Spring Dao编程高级-事务监听器

但我们如果重新 Debug 一下,可以发现四个监听器的顺序不一样了:

Spring Dao编程高级-事务监听器

好吧,就是一开始封装的 4 个方法的顺序不对劲,所以导致后面的顺序也跟着错乱了,那这底层的原因是什么呢?

其实原因我们在 IOC 篇的第 33 章 2.6.6 节中讲过:**使用 JVM 的标准反射,在不同的 JVM 、或者同一个 JVM 上的不同应用中,返回的方法列表顺序可能是不同的。**简言之,JVM 的标准反射不保证方法列表返回的顺序一致

所以就是因为 SpringFramework 直接使用了反射,导致我们获取到的 4 个方法顺序得不到保障,从而出现顺序错乱的问题。

2.6.2 修正方案

既然反射没有办法保证顺序一致性,那我们就要找别的方案了。其实 SpringFramework 帮我们考虑到这个事了,我们可以直接在方法上标注 @Order 注解,显式指定监听器的排序,这样就不会因为反射问题导致的顺序不一致了。

    @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
    @Order(1)
    public void onSaveUser(PayloadApplicationEvent<User> event) {
        System.out.println("监听到保存用户事务准备提交 ......");
    }
    
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    @Order(2)
    public void onSaveUser2(PayloadApplicationEvent<User> event) {
        System.out.println("监听到保存用户事务提交成功 ......");
    }
    
    @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
    @Order(3)
    public void onSaveUser3(PayloadApplicationEvent<User> event) {
        System.out.println("监听到保存用户事务回滚 ......");
    }
    
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)
    @Order(4)
    public void onSaveUser4(PayloadApplicationEvent<User> event) {
        System.out.println("监听到保存用户事务完成 ......");
    }

以此法声明好顺序之后,无论怎样运行,控制台永远先打印回滚后打印完成。

3. 基于自定义事件的事务监听【会用】

接下来咱思考一个问题。。。

3.1 Payload事件的不足

注意 saveUser 中发布事件的方式:

eventPublisher.publishEvent(user);

发布 Payload 事件固然没问题,那如果回头 findById 或者类似的方法,它也发布 Payload 事件的话,那事务监听器也会一起监听到。我们希望的是,能在不同的功能、不同的场景中,有不同的事件发布,同样的也有对应的事务监听器来监听对应的事件。

为此,我们又需要自定义事件了。。。

3.2 自定义事务事件

事务事件,说白了也是一个普通的事件,它没有什么特别的东西,所以我们可以来定义一个 UserSaveEvent 吧:

public class UserSaveEvent extends ApplicationEvent {
    
    public UserSaveEvent(Object source) {
        super(source);
    }
}

3.3 事件与监听

定义好事件之后,在 UserService 的 saveUser 方法中,将 Payload 事件改为 UserSaveEvent :

@Transactional
public void saveUser() {
    User user = new User();
    user.setName("哈哈哈");
    user.setTel("123");

    userDao.save(user);
    System.out.println("User save ......");
    eventPublisher.publishEvent(new UserSaveEvent(user));
}

随后,编写事务事件监听:

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onSaveUserEvent(UserSaveEvent event) {
    System.out.println("监听到保存用户事务提交成功 ......");
}

编写完成之后,IDEA 又有提示了,而且点击图标就能跳转到上面的 saveUser 方法中,只能说,不愧是你,IDEA ,太强了:

Spring Dao编程高级-事务监听器

3.4 测试运行

直接重新运行 main 方法即可,控制台可以打印出事务提交成功的监听,一切编写正确。

User save ......
监听到保存用户事务提交成功 ......
免责声明:
1.本站所有内容由本站原创、网络转载、消息撰写、网友投稿等几部分组成。
2.本站原创文字内容若未经特别声明,则遵循协议CC3.0共享协议,转载请务必注明原文链接。
3.本站部分来源于网络转载的文章信息是出于传递更多信息之目的,不意味着赞同其观点。
4.本站所有源码与软件均为原作者提供,仅供学习和研究使用。
5.如您对本网站的相关版权有任何异议,或者认为侵犯了您的合法权益,请及时通知我们处理。
火焰兔 » Spring Dao编程高级-事务监听器