美文网首页
Spring 声明式事务控制

Spring 声明式事务控制

作者: SheHuan | 来源:发表于2020-01-15 14:57 被阅读0次

    Spring 的事务控制可以分为编程式和声明式两种,编程式需要开发者通过编写代码的方式来实现事务管理,并不好用。而声明式则不需要编码,只需开发者按照约定的规则配置即可,实际的事务管理由框架完成,更加方便灵活。接下来就是 Spring 声明式事务控制的使用介绍。

    一、要实现的业务功能

    业务代码的核心功能就是模拟银行转账功能,正常情况下转出、转入需要同时成功,如果转账过程中有异常导致转入或转出失败,需要进行事务回滚,防止账户余额异常。

    例子中,dao层使用JdbcTemplate进行数据库操作,模拟账户查询和更新账户金额:

    @Repository("accountDao")
    public class AccountDaoImpl implements IAccountDao {
        @Autowired
        private JdbcTemplate jdbcTemplate;
        // 根据姓名查询账户
        public Account getAccountByName(String name) {
            String sql = "select * from account where name = ?";
            return jdbcTemplate.query(sql, new BeanPropertyRowMapper<Account>(Account.class), name).get(0);
        }
        // 更新账户余额
        public void updateAccount(Account account) {
            String sql = "update account set name = ?, money = ? where id = ?";
            jdbcTemplate.update(sql, account.getName(), account.getMoney(), account.getId());
        }
    }
    

    service层则进行具体的转账业务,注意int s = 1/0;是用来模拟转账失败的场景,模拟正常转账时需要注释掉:

    @Service("accountService")
    public class AccountServiceImpl implements IAccountService {
        @Autowired
        private IAccountDao accountDao;
    
        public void transfer(String name1, String name2, float money) {
            // 1.查询转出账户
            Account account1 = accountDao.getAccountByName(name1);
            // 2.查询转入账户
            Account account2 = accountDao.getAccountByName(name2);
            // 3.转出账户减少钱
            account1.setMoney(account1.getMoney() - money);
            // 4.转入账户增加钱
            account2.setMoney(account2.getMoney() + money);
            // 5.更新转出账户
            accountDao.updateAccount(account1);
            // 制造异常,使转账失败
            int s = 1/0;
            // 6.更新转入账户
            accountDao.updateAccount(account2);
        }
    }
    

    account表中已创建了两个默认账户:

    Spring 声明式事务控制按照使用方式可以分为基于xml和注解两大类,这里只列举常用的使用方式,我们先看基于xml的:

    二、使用tx、aop标签的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: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">
        <!--扫描指定包下使用了@Component、@Service、@Repository注解的类-->
        <context:component-scan base-package="com.shh.jdbcTemplate"/>
    
        <!--加载属性文件-->
        <context:property-placeholder location="classpath:jdbc.properties" ignore-resource-not-found="true"/>
    
        <!--配置JdbcTemplate-->
        <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
            <property name="dataSource" ref="dataSource"/>
        </bean>
    
        <!--配置数据源-->
        <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
            <property name="driverClass" value="${database.driver}"/>
            <property name="jdbcUrl" value="${database.url}"/>
            <property name="user" value="${database.username}"/>
            <property name="password" value="${database.password}"/>
        </bean>
    
        <!--配置事务管理器-->
        <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            <property name="dataSource" ref="dataSource"/>
        </bean>
    
        <!--Spring中基于xml的声明式事务配置开始-->
        <!--配置事务的通知-->
        <tx:advice id="txAdvice" transaction-manager="transactionManager">
            <!--配置事务的属性, name表示要对哪些方法使用事务-->
            <!--isolation:指定事务的隔离级别,默认DEFAULT,使用数据库的默认隔离级别-->
            <!--propagation:指定事务的传播行为,默认REQUIRED,表示一定会有事务(增删改使用),SUPPORTS(查询可使用)-->
            <!--read-only:指定是否是只读事务,默认为false,查询方法可设置为true-->
            <!--timeout:指定事务的超时时间,默认-1,不会超时,单位为秒-->
            <!--rollback-for:指定一个异常,当发生该异常时回滚事务,有其它异常时不回滚,默认没有值,任何异常都会回滚-->
            <!--no-rollback-for:指定一个异常,当发生该异常时不回滚事务,有其它异常时回滚事务,默认没有值,任何异常都会回滚-->
            <tx:attributes>
                <tx:method name="*"/>
                <tx:method name="get*" propagation="SUPPORTS" read-only="true"/>
                <tx:method name="select*" propagation="SUPPORTS" read-only="true"/>
                <tx:method name="find*" propagation="SUPPORTS" read-only="true"/>
                <tx:method name="query*" propagation="SUPPORTS" read-only="true"/>
            </tx:attributes>
        </tx:advice>
        <!--事务的AOP相关配置-->
        <aop:config>
            <!--定义切入点表达式-->
            <aop:pointcut id="pt" expression="execution(* com.shh.jdbcTemplate.service.impl.*.*(..))"/>
            <!--建立切入点表达式和事务的通知的关系-->
            <aop:advisor advice-ref="txAdvice" pointcut-ref="pt"/>
        </aop:config>
        <!--Spring中基于xml的声明式事务配置结束-->
    </beans>
    

    比较重要的点都在注释中有写到,至于事务是如何开启、提交、回滚等操作都是由 Spring 完成的,目前需要我们关心的是配置事务控制的主要步骤:

    1. 配置事务管理器
    2. 配置事务的通知,这里需要用到事务管理器,然就是配置事务的属性,哪些方法会被匹配到,可以使用事务管理
    3. 使用Sping AOP 将切入点表达式和事务的同事关联起来,切入点的作用是指定哪些类中的方法会被进行事务控制增强

    测试代码如下,从张三账户给李四账户转100:

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(locations = {"classpath:jdbc-template.xml"})
    public class JdbcTemplateTest {
        @Autowired
        private IAccountService accountService;
    
        @Test
        public void transfer() {
            accountService.transfer("张三", "李四", 100);
        }
    }
    

    先将之前的int s = 1/0;放开,模拟转账失败,转账时会抛出异常,事务回滚,两个账户的余额不会变化:



    再将int s = 1/0;注释掉,执行测试方法,则转账成功,账户余额会有变化:

    经测试已经实现了我们的目的,接下来看第二种xml配置方式:

    三、使用拦截器类的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"
           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">
    
        <context:component-scan base-package="com.shh.jdbcTemplate"/>
    
        <!--加载属性文件-->
        <context:property-placeholder location="classpath:jdbc.properties" ignore-resource-not-found="true"/>
    
        <!--配置JdbcTemplate-->
        <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
            <property name="dataSource" ref="dataSource"/>
        </bean>
    
        <!--配置数据源-->
        <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
            <property name="driverClass" value="${database.driver}"/>
            <property name="jdbcUrl" value="${database.url}"/>
            <property name="user" value="${database.username}"/>
            <property name="password" value="${database.password}"/>
        </bean>
    
        <!--配置事务管理器-->
        <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            <property name="dataSource" ref="dataSource"/>
        </bean>
    
        <!--Spring中基于xml的声明式事务配置开始-->
        <!--配置事务拦截器,拦截哪些方法-->
        <bean id="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor">
            <property name="transactionManager" ref="transactionManager"/>
            <!--配置事务属性,key表示要对哪些方法使用事务,<prop>里边的是事务的属性 -->
            <property name="transactionAttributes">
                <props>
                    <prop key="*">PROPAGATION_REQUIRED</prop>
                    <prop key="get*">PROPAGATION_SUPPORTS,readOnly</prop>
                    <prop key="query*">PROPAGATION_SUPPORTS,readOnly</prop>
                    <prop key="find*">PROPAGATION_SUPPORTS,readOnly</prop>
                    <prop key="select*">PROPAGATION_SUPPORTS,readOnly</prop>
                </props>
            </property>
        </bean>
        <!--指定事务拦截器要拦截哪些类-->
        <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
            <property name="beanNames">
                <list>
                    <!--对象在IoC容器中的名称,*是通配符-->
                    <value>*Service</value>
                </list>
            </property>
            <property name="interceptorNames">
                <list>
                    <value>transactionInterceptor</value>
                </list>
            </property>
        </bean>
        <!--Spring中基于xml的声明式事务配置结束-->
    </beans>
    

    关键的地方就是使用TransactionInterceptor拦截器类,配置哪些方法要使用事务,以及事务的属性。使用BeanNameAutoProxyCreator类来指定拦截 Spring IoC 容器中的哪些对象,同时和TransactionInterceptor建立关联。

    具体的测试和之前类似就不重复了。

    使用注解的声明式事务配置相对简单些,可以xml+注解混合使用,也可以使用纯注解。

    四、使用xml+注解的配置

    这里 xml 的主要功能是开启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">
    
        <context:component-scan base-package="com.shh.jdbcTemplate"/>
    
        <!--加载属性文件-->
        <context:property-placeholder location="classpath:jdbc.properties" ignore-resource-not-found="true"/>
    
        <!--配置JdbcTemplate-->
        <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
            <property name="dataSource" ref="dataSource"/>
        </bean>
    
        <!--配置数据源-->
        <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
            <property name="driverClass" value="${database.driver}"/>
            <property name="jdbcUrl" value="${database.url}"/>
            <property name="user" value="${database.username}"/>
            <property name="password" value="${database.password}"/>
        </bean>
    
        <!--配置事务管理器-->
        <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            <property name="dataSource" ref="dataSource"/>
        </bean>
    
        <!--Spring中基于注解的声明式事务配置开始-->
        <!--开启Spring对基于注解事务的支持-->
        <tx:annotation-driven transaction-manager="transactionManager"/>
        <!--在需要事务支持的类上添加@Transactional注解-->
        <!--Spring中基于注解的声明式事务配置结束-->
    </beans>
    

    然后就是在service层需要使用事务控制的类上添加@Transactional注解,这样类里边的方法会受到事务控制:

    @Service("accountService")
    @Transactional
    public class AccountServiceImpl implements IAccountService {
        @Autowired
        private IAccountDao accountDao;
    
        public void transfer(String name1, String name2, float money) {
           ......
        }
    }
    

    @Transactional也可以配置事务的属性,例如:

    @Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
    

    测试代码和之前相同。

    五、使用全注解的配置

    在使用xml+注解的配置中,xml中的配置,可以完全用注解的方式替代,实现全注解配置。

    数据库配置文件jdbc.properties的加载、jdbcTemplatedataSourcebean对象的管理,可以用JdbcConfig类完成:

    @Configuration
    @PropertySource("classpath:jdbc.properties")
    public class JdbcConfig {
        @Value("${database.driver}")
        private String driverClass;
        @Value("${database.url}")
        private String jdbcUrl;
        @Value("${database.username}")
        private String user;
        @Value("${database.password}")
        private String password;
    
        @Bean("jdbcTemplate")
        public JdbcTemplate createJdbcTemplate(DataSource dataSource) {
            return new JdbcTemplate(dataSource);
        }
    
        @Bean("dataSource")
        public ComboPooledDataSource createDataSource() throws PropertyVetoException {
            ComboPooledDataSource dataSource = new ComboPooledDataSource();
            dataSource.setDriverClass(driverClass);
            dataSource.setJdbcUrl(jdbcUrl);
            dataSource.setUser(user);
            dataSource.setPassword(password);
            return new ComboPooledDataSource();
        }
    }
    

    事务管理器transactionManager对象,可以用TransactionConfig类维护:

    @Configuration
    public class TransactionConfig {
        @Bean("transactionManager")
        public PlatformTransactionManager createTransactionManager(DataSource dataSource) {
            return new DataSourceTransactionManager(dataSource);
        }
    }
    

    开启Spring对基于注解事务的支持<tx:annotation-driven transaction-manager="transactionManager"/>,可以使用@EnableTransactionManagement注解代替,
    bean的扫描<context:component-scan base-package="com.shh.jdbcTemplate"/>,可以用@ComponentScan代替,所以最终全注解配置类如下:

    @Configuration
    @ComponentScan("com.shh.jdbcTemplate")
    @Import({JdbcConfig.class, TransactionConfig.class})
    @EnableTransactionManagement
    public class SpringConfig {
    }
    

    修改我们的测试代码,加载SpringConfig

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes = {SpringConfig.class})
    public class JdbcTemplateTest {
        @Autowired
        private IAccountService accountService;
    
        @Test
        public void transfer() {
            accountService.transfer("张三", "李四", 100);
        }
    }
    

    至此,全注解的Spring 声明式事务控制改造完毕。

    这三种事务配置方式,前两种只需在xml中按照约定的步骤配置即可,而基于注解配置相对简单些,但需要在每个需要事务控制的service层业务类上添加@Transactional注解,如何选择就要看实际的需求了,我个人更倾向于前两种xml配置方式。

    相关文章

      网友评论

          本文标题:Spring 声明式事务控制

          本文链接:https://www.haomeiwen.com/subject/asebactx.html