Spring(四):声明式事务

Source

Spring(四):声明式事务

1. 事务概述

​ 事务是最小的执行单元,一个事务中的多个操作要么同时成功,要么同时失败。这体现了事务的原子性。

​ 之前已经写了一篇关于事务和事务的隔离级别的文章。这里就不赘述了。

2. Spring中的声明式事务

​ 在JavaEE三层架构中,事务处理一般处于业务层。Spring框架提供了事务控制的api。事务控制基于AOP实现,可以用配置方式实现,配置方式实现的事务是方法级别的粗粒度事务控制。也可以使用编程方式实现,这是一种细粒度的行级事务控制

2.1 事务控制相关api

  • PlatformTransactionManager

    • 继承结构
    |--PlatformTransactionManager  //事务管理器接口
    	//接口的实现类,真正管理事务的对象
    	|--DataSourceTransactionManager  // 使用SpringJDBC和mybatis进行持久化数据时使用
    
    	|--HibernateTransactionManager  // 使用Hibernate持久化数据时使用
    
    • 相关方法
    // 获取事务状态信息
    TransactionStatus getTransaction(TranactionDefinition)
    // 提交事务
    void commit(TransactionStatus status)
    // 回滚事务
    void rollback(TransactionStatus status)
    
TransactionDefinition 是事务的定义信息对象,里面有如下方法:
// 获取事务名称
String getName()
// 获取事务隔离级别
int getIsolationLevel()
// 获取事务传播级别
int getPropagationBehavior()
// 获取事务超时时间
int getTimeOut()
// 获取事务是否只读
boolean isReadOnly()
  • TransactionStatus

此接口提供事务具体的运行状态,拥有如下6个方法

// 刷新事务
void flush()
// 获取是否存在存储点
boolean hasSavepoint()
// 获取事务是否完成
boolean isCompleted()
// 获取事务是否为新的事务
Boolean isNewTransaction()
// 获取事务是否回滚
boolean isRollbackOnly()
// 设置事务回滚
void setRollbackOnly()

2.2 事务的传播级别

  • REQUIRED:如果当前没有事务,就新建一个事务。如果已经存在一个事务中,加入这个事务中(默认值)
  • SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行(没有事务)。
  • MANDATORY:使用当前的事务,如果当前没有事务,就抛出异常。
  • REQUERS_NEW:新建事务,如果当前存在事务,就把当前事务挂起。
  • NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
  • NEVER:以非事务方式运行,如果当前存在事务,抛出异常。
  • NESTED:如果当前存在事务,则在嵌套事务内运行。如果当前没有事务,则执行REQUIRED类似的操作。

###3. XML实现声明式事务

​ 在service层插入两条账户信息, 插入一条账户信息之后故意产生一个除零异常,如果事务控制生效会回滚事务,这个时候查看数据库,会发现没有插入数据。

3.0 导入依赖

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.1.3.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.0.9</version>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.8.7</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.26</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-tx</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>

</dependencies>

3.1 数据表和实体类

package com.zmysna.entity;

public class Account {
    private Integer accountId;
    private Integer uid;
    private Double money;
    
    ...set/get方法
}

3.2 dao层和service业务层

  • dao层
public interface IAccountDao {
    void save(Account account);
}

@Repository
public class AccountDaoImpl implements IAccountDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void save(Account account) {
        jdbcTemplate.update("insert into account values (null,?,?)", account.getUid(),account.getMoney());
    }
}
  • service层
public interface IAccountService {
    void save(Account account);
}

/**
  * 同时保存两个用户,进行事务控制
  */
@Service
public class AccountServiceImpl implements IAccountService {

    @Autowired
    private IAccountDao accountDao;
 
    @Override
    public void save(Account account) {      
        // 插入一条账户信息
	    accountDao.save(account);
        // 执行到这里会产生一个异常,如果事务控制生效会回滚事务,数据库不会插入数据
	    int i = 1/0;
	    accountDao.save(account);
	    return null;
    }
}

3.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:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
    <!--导入外部配置文件-->
    <context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>

    <!--开启注解扫描,可以配置多个包,以逗号分隔-->
    <context:component-scan base-package="com.zmysna.service, com.zmysna.dao">
    </context:component-scan>

    <!--配置数据源-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="password" value="${jdbc.password}"></property>
        <property name="url" value="${jdbc.url}"></property>
        <property name="username" value="${jdbc.username}"></property>
        <property name="driverClassName" value="${jdbc.driverName}"></property>
    </bean>

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

    <!--配置事务管理器-->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!--事务通知
		name属性表示声明事务的方法
		propagation表示事务的传播行为
		read-only 为true时表示只读,用于查询;read-only为false用于增删改方法
	-->
    
    <tx:advice id="txAdvice" transaction-manager="txManager">
        <tx:attributes>
            <!--查询-->
            <tx:method name="select*" propagation="SUPPORTS" read-only="true"/>
            <tx:method name="query*" propagation="SUPPORTS" read-only="true"/>
            <tx:method name="search*" propagation="SUPPORTS" read-only="true"/>
            <tx:method name="find*" propagation="SUPPORTS" read-only="true"/>
            <!--增删改-->
            <tx:method name="*" read-only="false" propagation="REQUIRED" />
        </tx:attributes>
    </tx:advice>

    <!--配置aop-->
    <aop:config>
        <aop:pointcut id="pt" expression="execution(* com..*AccountServiceImpl.*(..))"/>
        <aop:advisor pointcut-ref="pt" advice-ref="txAdvice"></aop:advisor>
    </aop:config>

</beans>

外部配置文件jdbc.properties

jdbc.driverName=com.mysql.jdbc.Driver
jdbc.username=root
jdbc.password=awsed2zx
jdbc.url=jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8

3.4 测试

//spring整合Junit
@RunWith(SpringJUnit4ClassRunner.class)
//通过配置文件创建一个容器
@ContextConfiguration("classpath:bean.xml")
public class XmlTxTest {

    @Autowired
    private IAccountService accountService;

    @Test
    public void transactionTest() {
        Account account = new Account();
        account.setUid(51);
        account.setMoney(999.0D);
        //打印accountService的类型,查看是不是一个代理类
        System.out.println(accountService.getClass());       
        accountService.save(account);
    }
}

结果:

​ 先观察accountService是不是一个代理类,在观察数据库是否插入数据,如果是代理类,并且成功产生除零异常。则证明事务控制生效,数据库不会插入数据。

// 产生JDK动态代理对象
class com.sun.proxy.$Proxy31
// 除零错误
java.lang.ArithmeticException: / by zero
	......略

4. 注解实现事务控制

​ 注解实现事务控制只需在要实现注解控制的类和方法上添加@Transaction注解,然后开启spring声明式事务的注解支持。

4.1 @Transactional事务注解

  • 定义位置
    • 接口、父类上——表示实现当前接口的类,继承父类的子类都应用spring声明式事务
    • 接口方法上——表示接口的实现类重写的方法应用spring声明式事务
    • 实现类——该类的所有方法的应用声明式事务
    • 实现类的方法上——这个方法应用声明式事务
  • 属性
    • readOnly——为 true 时表示只读,用于查询;为 false 用于增删改方法。默认为false
    • propagation——表示事务的传播行为,默认为Propagation.REQUIRED

4.2 改造

  1. 在service层添加声明式事务注解
@Service
@Transactional
public class AccountServiceImpl implements IAccountService {

    @Autowired
    private IAccountDao accountDao;
    
    @Override
    public void save(Account account) {
     	accountDao.save(account);
     	int i = 1/0;
     	accountDao.save(account);
    }
}
  1. bean.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:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
    <!--导入外部配置文件-->
    <context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>

    <!--开启注解扫描,可以配置多个包,以逗号分隔-->
    <context:component-scan base-package="com.zmysna.service, com.zmysna.dao">
    </context:component-scan>

    <!--配置数据源-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="password" value="${jdbc.password}"></property>
        <property name="url" value="${jdbc.url}"></property>
        <property name="username" value="${jdbc.username}"></property>
        <property name="driverClassName" value="${jdbc.driverName}"></property>
    </bean>

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

    <!--配置事务管理器-->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

	<!--开启spring声明式事务的注解支持-->
   <tx:annotation-driven transaction-manager="txManager"></tx:annotation-driven>

</beans>
  1. 其他类、配置文件、测试和测试结果都相同

4.3 继续改造实现零配置

  • 相关注解

​ @Configuration——指定当前类为配置类

​ @Import——导入其他配置类

​ @ComponentScan——注解扫描

​ @Bean——应用在方法上,将方法返回的对象加入Spring容器

​ @PropertySource——加载外部配置文件

@EnableTransactionManagement——开启声明式事务的注解支持

  • 改造

    主配置类

package com.zmysna.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScans;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@ComponentScans({
        @ComponentScan("com.zmysna.service"),
        @ComponentScan("com.zmysna.dao"),
})
@Import(JdbcConfig.class)
@EnableTransactionManagement	//开启声明式事务的注解支持
public class SpringConfiguration {
}

​ 数据源配置类

package com.zmysna.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import javax.sql.DataSource;
import javax.xml.crypto.Data;

@PropertySource("classpath:jdbc.properties")
public class JdbcConfig {
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.username}")
    private String username;
    @Value("${jdbc.password}")
    private String password;
    @Value("${jdbc.driverName}")
    private String driverClassName;

    @Bean("dataSource")
    public DataSource createDataSource(){
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setDriverClassName(driverClassName);
        dataSource.setPassword(password);
        return dataSource;
    }

    @Bean("jdbcTemplate")
    public JdbcTemplate createJDBCTemplate(@Qualifier("dataSource") DataSource dataSource){
        return new JdbcTemplate(dataSource);
    }

    @Bean("txManager")
    public DataSourceTransactionManager createTransactionManager(@Qualifier("dataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}
  • 测试

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfiguration.class)//加载配置类
public class AnnoTxTest {

    @Autowired
    private IAccountService accountService;

    @Test
    public void transactionTest() {
        Account account = new Account();
        account.setUid(51);
        account.setMoney(999.0D);
        System.out.println(accountService.getClass());
        accountService.save(account);
    }

}
  • 测试结果不变,xml配置文件可删去。

5. 编程实现事务管理

​ 声明式事务是方法级别的事务,粒度比较粗。如果想实现行级事务,就要需要编程实现,实现方法也很简单。将想要实现事务管理的代码定义在TransactionCallback事务回调函数中,然后使用transactionTemplate调用回调函数。

步骤:

  1. 使用spring创建并管理对象
        <?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:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 加载 jdbc.properties 文件 -->
<context:property-placeholder
        location="classpath:jdbc.properties"></context:property-placeholder>
<!--1. 创建 dataSource-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="driverClassName"
              value="${jdbc.driverClassName}"></property>
    <property name="url" value="${jdbc.url}"></property>
    <property name="username" value="${jdbc.username}"></property>
    <property name="password" value="${jdbc.password}"></property>
</bean>
<!--2. 创建 jdbcTemplate ,注入 dataSource-->
<bean id="jdbcTemplate"
      class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSource"></property>
</bean>
<!--3. 创建 dao ,注入 jdbcTemplate-->
<bean id="accountDao" class="com.zmysna.dao.impl.AccountDaoImpl">
    <property name="jdbcTemplate" ref="jdbcTemplate"></property>
</bean>
<!--4. 创建 service ,注入 dao-->
<bean id="accountService"
      class="com.zmysna.service.impl.AccountServiceImpl">
    <property name="accountDao" ref="accountDao"></property>
    <property name="transactionTemplate"
              ref="transactionTemplate"></property>
</bean>
<!--5.Spring 事务管理器 ( 切面类 )-->
<bean id="transactionManager"
      class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 创建 Spring 编程式事务控制的事务模板类 -->
<bean id="transactionTemplate"
      class="org.springframework.transaction.support.TransactionTemplate">
    <property name="transactionManager"
              ref="transactionManager"></property>
</bean>
</beans>
  1. 在service层中编程实现事务
import com.zmysna.dao.IAccountDao;
import com.zmysna.domain.Account;
import com.zmysna.service.IAccountService;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;

public class AccountServiceImpl implements IAccountService {
    // 注入 dao
    private IAccountDao accountDao;
    // 注入事务模板
    private TransactionTemplate transactionTemplate;

    public void setAccountDao(IAccountDao accountDao) {
        this.accountDao = accountDao;
    }

    public void setTransactionTemplate(TransactionTemplate
                                               transactionTemplate) {
        this.transactionTemplate = transactionTemplate;
    }

    @Override
    public void save(Account account) {
		// 定义回调函数
        TransactionCallback<Object> callback = new
                TransactionCallback<Object>() {
                    @Override
                    public Object doInTransaction(TransactionStatus status) {
				// 需要事务控制的代码,写到这里
                        accountDao.save(account);
                        int i = 1 / 0;
                        accountDao.save(account);
                        return null;
                    }
                };
	// 事务控制执行 , 传入回调函数
        transactionTemplate.execute(callback);
    }
}