Spring Dao编程基础-声明式事务

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

上一章咱已经见识了 SpringFramework 的编程式事务,看上去已经挺简单了,但这种写法还是麻烦的,每个 Service 中都要注入 TransactionTemplate 不说,编写的所有方法都要先写一句 transactionTemplate.execute 方法,每个方法都要写,那还是很烦的。

为了解决这个问题,SpringFramework 提供了声明式事务的配置方案。使用它,可以更简单的配置和实现事务控制。

1. 代码准备

声明式事务分 xml 配置和注解配置,所以咱需要分别搞两套 Service 和 Dao ,所以这次不仅需要造 tbl_user 对应的 Service 和 Dao 了,tbl_account 的也要造。

1.1 Dao

Dao 的编写还是简单的,快速过一遍吧:

@Repository
public class UserDao {
    
    @Autowired
    JdbcTemplate jdbcTemplate;
    
    public void save(User user) {
        jdbcTemplate.update("insert into tbl_user (name, tel) values (?, ?)", user.getName(), user.getTel());
    }
    
    public List<User> findAll() {
        return jdbcTemplate.query("select * from tbl_user", new BeanPropertyRowMapper<>(User.class));
    }
}
@Repository
public class AccountDao {
    
    @Autowired
    JdbcTemplate jdbcTemplate;
    
    public void addMoney(Integer userId, int money) {
        jdbcTemplate.update("update tbl_account set money = money + ? where user_id = ?", money, userId);
    }
    
    public void subtractMoney(Integer userId, int money) {
        jdbcTemplate.update("update tbl_account set money = money - ? where user_id = ?", money, userId);
    }
}

针对 tbl_account 表只做加钱减钱的动作吧,别的就不搞了。

1.2 Service

Service 层也要写俩,UserService 还是抄之前的就好:

@Service
public class UserService {
    
    @Autowired
    UserDao userDao;
    
    public void saveAndQuery() {
        User user = new User();
        user.setName("阿巴阿巴");
        user.setTel("123654789");
    
        userDao.save(user);

        int i = 1 / 0;

        List<User> userList = userDao.findAll();
        System.out.println(userList);
    }
}

AccountService 定义一个转账的方法:

@Service
public class AccountService {

    @Autowired
    AccountDao accountDao;
    
    public void transfer(Integer sourceId, Integer targetId, int money) {
        accountDao.subtractMoney(sourceId, money);
        
        int i = 1 / 0;
        
        accountDao.addMoney(targetId, money);
    }
}

这样 Service 也就写好了。

1.3 配置文件

咱先讲解使用 xml 配置文件的方式,先把基本的东西都准备好:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
                           http://www.springframework.org/schema/beans/spring-beans.xsd 
                           http://www.springframework.org/schema/tx 
                           http://www.springframework.org/schema/tx/spring-tx.xsd 
                           http://www.springframework.org/schema/aop 
                           https://www.springframework.org/schema/aop/spring-aop.xsd 
                           http://www.springframework.org/schema/context 
                           https://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/spring-dao?characterEncoding=utf8"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
    </bean>

    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <context:component-scan base-package="com.linkedbear.spring.transaction.c_declarative"/>
    <context:annotation-config/>

    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>

</beans>

1.4 配置类

与 xml 配置文件相似,咱也写一套注解驱动的配置:

@Configuration
@ComponentScan("com.linkedbear.spring.transaction.d_declarativeanno")
public class DeclarativeTransactionConfiguration {
    
    @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());
    }
}

可以发现,上面 xml 中定义的 bean ,在这里是完全一样的定义套路吧,很简单,小伙伴们跟着快速编写一下就可以了。

接下来,咱开始学习声明式事务的配置。

2. 基于xml配置文件的声明式事务【掌握】

对于 xml 配置文件的声明式事务,需要引入新的命名空间了:tx ,它就是关于事务的配置部分。

前面咱自己实战编写 AOP 事务控制的时候,是不是就已经运用到 AOP 的知识了吧,所以对应的 aop 也需要引入。

2.1 xml添加新的配置

按照之前的思路,如果要在 xml 中配置 SpringFramework 的事务控制,那想必一定是一个关于切面 / 通知的东西吧,哎,在配置文件中,还真就有一个事务通知的标签:

<tx:advice id="transactionAdvice" transaction-manager="transactionManager">
    
</tx:advice>

一个事务通知,只需要指定事务管理器即可,而事务管理器在前面已经定义过了,所以这里直接引用一下就可以。

有了事务通知,它对应的 AOP 配置还没有呢,接下来咱还得配置 AOP 的增强。有关 AOP 的配置,咱前面是没有讲解这部分的,这里咱一并说说:

<aop:config>
    <aop:advisor advice-ref="transactionAdvice"
                 pointcut="execution(* com.linkedbear.spring.transaction.c_declarativexml.service.*.*(..))"/>
</aop:config>

这个 <aop:advisor> 标签咱之前没有用过,它是 SpringFramework 原生 AOP 的东西,它相当于配置一个增强器 Advisor ,这个增强器的通知就是上面的事务通知,切入点表达式指定的就是要被控制事务的方法。很明显,小册这样配置,相当于让 c_declarativexml.service 包下面的所有类的所有方法,都织入一个事务通知。

这里小册多讲几句,<aop:aspect> 针对的是一个 Aspect 切面类的多个通知方法配置,而 <aop:advisor> 针对的是一个通知方法配置。这种配置是为了兼顾 SpringFramework 原生的 AOP 通知写法,由于我们日常开发中已经几乎不用该方法,所以小册前面也就没再讲,这里跟着写一遍熟悉一下就可以了。

如果小伙伴已经看过 AOP 原理部分的话,这里应该知道,一个通知 + 一个切入点表达式,就是一个增强器,被 AOP 增强过的代理对象,在执行方法时会先执行增强器的通知逻辑。

接下来,事务通知有了,AOP 配置有了,事务到底怎么控制还没配置呢(不然上面的 <tx:advice> 标签还空着一块干嘛呢)。接下来咱要配置事务的控制策略。

回到上面的 <tx:advice> 标签,继续编写,不过输入一个尖括号,发现它只有一个标签:

Spring Dao编程基础-声明式事务

嚯,这也太省心了吧,那直接回车输入。。。

哎,等一下,按下回车之后怎么出来这么多代码:

Spring Dao编程基础-声明式事务

这什么东东?直接就弹出 <tx:method> 标签?这是几个意思?这 attribute 标签中没别的了吗?另起一行,再输一个尖括号,发现它确实只有一个 method 标签:

Spring Dao编程基础-声明式事务

哇塞,那这也太简单了吧,只需要配置事务拦截的方法就得了吧!回头看看 UserService 和 AccountService 中都有什么方法吧:

<tx:advice id="transactionAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="saveAndQuery"/>
        <tx:method name="addMoney"/>
        <tx:method name="subtractMoney"/>
    </tx:attributes>
</tx:advice>

emmm这样倒是配好了,但貌似有个毛病,咱过会再说。

2.2 测试运行

接下来咱编写启动类,用 xml 配置文件驱动 IOC 容器,运行一把:

public class DeclarativeTransactionXmlApplication {
    
    public static void main(String[] args) throws Exception {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("tx/declarative-transaction.xml");
        UserService userService = ctx.getBean(UserService.class);
        userService.saveAndQuery();
    }
}

运行 main 方法,控制台会抛出一个 ClassNotFoundException :

Caused by: java.lang.ClassNotFoundException: org.aspectj.weaver.reflect.ReflectionWorld$ReflectionWorldException
	at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:338)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	... 37 more

??????是不是一脸黑人问号?咋又 aspectj 了?原因很简单,AOP 要整合 aspectj 呀,咱 pom 里头又没有,那得了,加上呗,前面学习 AOP 的时候都有了:

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.5</version>
</dependency>

导入 aspectj 的依赖后,重新运行 main 方法,这次控制台打印除零异常了:

Exception in thread "main" java.lang.ArithmeticException: / by zero

然后去数据库看,发现并没有继续添加新的 阿巴阿巴 :

Spring Dao编程基础-声明式事务

把除零的动作去掉,程序便可成功运行,数据库也会多一条 阿巴阿巴 ,下面的 findAll 方法也就可以正常执行了:

public void saveAndQuery() {
    User user = new User();
    user.setName("阿巴阿巴");
    user.setTel("123654789");

    userDao.save(user);
    // int i = 1 / 0;
    List<User> userList = userDao.findAll();
    System.out.println(userList);
}
[User{id=1, name='zhangsan', tel='110'}, User{id=2, name='lisi', tel='120'}, User{id=3, name='wangwu', tel='119'}, User{id=4, name='hahaha', tel='12345'}, User{id=5, name='阿巴阿巴', tel='123654789'}, User{id=8, name='阿巴阿巴', tel='123654789'}]

这样基于 xml 配置文件的声明式事务也就搞好了。

2.3 tx:method的麻烦?

回头看一眼上面配置的事务控制方法:

<tx:advice id="transactionAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="saveAndQuery"/>
        <tx:method name="addMoney"/>
        <tx:method name="subtractMoney"/>
    </tx:attributes>
</tx:advice>

现在的方法还少,回头 Service 一多,方法多起来,那这就一个一个写呗?那不得累死咯。SpringFramework 当然帮我们考虑到了,所以这里可以写通配符:

<tx:advice id="transactionAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="save*"/> <!-- 所有save开头的方法全部开启事务控制 -->
        <tx:method name="select*"/> <!-- 所有select开头的方法全部开启事务控制 -->
        <tx:method name="*"/> <!-- 任意方法均开启 -->
    </tx:attributes>
</tx:advice>

这样写起来是不是就简单多了呀,而且这样写完之后,程序照样能运行(这里就不再重复贴出测试结果了)。

2.4 tx:method的其它属性

除了 name 之外,<tx:method> 标签还有一些其它的属性,下面咱也一一来看:

  • isolation :事务隔离级别。默认是 DEFAULT ,即依据数据库默认的事务隔离级别来定
  • timeout :事务超时时间,当事务执行超过指定时间后,事务会自动中止并回滚,单位  。默认值 -1 ,代表永不超时
  • read-only :设置是否为只读事务。默认值 false ,代表读写型事务。
    • 当设置为 true 时,当前事务为只读事务,通常用于查询操作(此时不会有 setAutoCommit(false) 等操作,可以加快查询速度)
  • rollback-for :当方法触发指定异常时,事务回滚,需要传入异常类的全限定名。
    • 默认值为空,代表捕捉所有 RuntimeException 和 Error 的子类
    • 一般情况下,在日常开发中,我们都会显式声明其为 Exception ,目的是一起捕捉非运行时异常
  • no-rollback-for :当方法触发指定异常时,事务不回滚继续执行,需要传入异常类的全限定名。默认值为空,代表不忽略异常
  • propagation :事务传播行为,下一章讲解

这些属性的配置也都蛮简单的,小册在这里就不展开逐个讲解了,小伙伴们可以自行测试练习一下。

最后一个属性 propagation 属性,它涉及到事务传播行为了,这个概念相对比较难理解,咱放到下一章单独讲解。

3. 基于注解驱动的声明式事务【掌握】

接下来,咱继续学习注解驱动的声明式事务。

3.1 配置类添加新的配置

前面咱已经把基本的配置类都写完了,接下来咱继续向里面添加新的配置。

既然前面在 xml 配置文件中,咱要配置事务通知,还要 AOP ,那在注解驱动中,完全不需要那么麻烦,只需要一个注解即可:

@Configuration
@ComponentScan("com.linkedbear.spring.transaction.d_declarativeanno")
@EnableTransactionManagement
public class DeclarativeTransactionConfiguration {

对,就这么简单,打一个注解就开启注解声明式事务控制了。(但话说回来,模块装配不就应该这么简单嘛)

当然,只有这一个注解还不够,SpringFramework 不知道哪些方法需要进行事务控制呀。所以,还需要在那些需要控制事务的方法上标注 @Transactional 注解,这样才算是对方法完成了事务控制。(咱前面的第 45 章不就是这么设计的嘛)

比方说,咱在 UserService 的 saveAndQuery 方法标注 @Transactional 注解:

@Transactional
public void saveAndQuery() {
    User user = new User();
    user.setName("阿巴阿巴");
    user.setTel("123654789");

    userDao.save(user);

    int i = 1 / 0;

    List<User> userList = userDao.findAll();
    System.out.println(userList);
}

3.2 测试运行

好了,上面这就配完了,咱编写测试启动类来搞吧:

public class DeclarativeTransactionAnnoApplication {
    
    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(DeclarativeTransactionConfiguration.class);
        UserService userService = ctx.getBean(UserService.class);
        userService.saveAndQuery();
    }
}

运行 main 方法,控制台会抛出除零异常,而且数据库没有添加新的数据:(刚才在 2.2 节中有成功添加过一次 阿巴阿巴 哦)

Spring Dao编程基础-声明式事务

对,有木有发现注解声明式事务非常简单呀!比起 xml 的声明式事务,这可简单太多了。

但是要注意,事务管理器还是要配置的,不然会报 NoSuchBeanDefinitionException 的异常,事务管理器找不到。

3.3 如果xml与注解驱动混用

实际开发中难免会出现 xml 配置文件,与注解驱动同时存在的问题(前面 xml 的声明式事务,不就开启了 annotation-driven 嘛),那如果没有配置类,也就没机会标注 @EnableTransactionManagement 了,那怎么办呢?哎,在 tx 命名空间中,有一个跟 @EnableTransactionManagement 注解相同作用的标签:

<tx:annotation-driven transaction-manager="transactionManager"/>

很容易理解吧,这就是开启事务的注解驱动,作用与 @EnableTransactionManagement 完全一致。(而且可以看到,注解声明式事务也需要传入事务管理器的,说明事务管理器才是整个事务控制的核心,xml 配置还是注解驱动,都只是利用事务管理器的两种形式罢了)

3.4 @Transactional的细节

跟前面的 <tx:method> 一样,如果一个 Service 类中有太多需要标注 @Transactional 的方法,那一个一个标注也太麻烦了,SpringFramework 当然也帮我们考虑到了,于是它给 @Transactional 定义了可标注范围不止 METHOD ,还有 TYPE ,也即可以直接标到类 / 接口上:

@Service
@Transactional
public class AccountService {

然后我们在测试类上试试调用 AccountService 的方法看看:

    AccountService accountService = ctx.getBean(AccountService.class);
    accountService.transfer(1, 2, 100);

运行 main 方法,控制台依然会报除零异常,然而账户表里两个人的余额都不会变动。

Spring Dao编程基础-声明式事务

3.5 @Transactional的其它属性

借助 IDE ,可以发现 @Transactional 注解中的属性,与 <tx:method> 标签中的属性几乎完全一致,所以使用方法和套路也都是一样的,小伙伴们变通着练习一下就可以啦。

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