【Spring】Spring的事务管理 - 2、声明式事务管理(实现基于XML、Annotation的方式。)
声明式事务管理
简单记录 - 简单记录-Java EE企业级应用开发教程(Spring+Spring MVC+MyBatis) 和 Spring 3.0就这么简单 -Spring的事务管理
声明式事务管理
数据访问的技术很多,如JDBC、JPA、Hibernate、分布式事务等。面对众多的数据访问技术,Spring在不同的事务管理API上定义了一个抽象层PlatformTransaction-Manager,应用程序开发人员不必了解底层的事务管理API就可以使用Spring的事务管理机制。Spring并不直接管理事务,而是提供了许多内置事务管理器实现,常用的有DataSourceTransactionManager、JdoTransactionManager、JpaTransactionManager以及HibernateTransactionManager等。(Spring作为企业级应用程序框架,在不同的事务管理API上定义了一个抽象层,使应用程序开发人员不必了解底层的事务管理API,就可以使用Spring的事务管理机制。)
Spring事务管理分两种方式,一种是传统的编程式事务管理(也称编码式事务),另一种是声明式事务管理。
编程式事务管理
通过编写代码实现的事务管理,包括定义事务的开始、正常执行后的事务提交和异常时的事务回滚。编程式事务管理是将事务管理代码嵌入业务方法中来控制事务的提交和回滚。在编程式事务中,必须在每个业务操作中包含额外的事务管理代码。
声明式事务管理
声明式事务管理是指将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。在大多数情况下,声明式事务管理比编程式事务管理更好用。Spring是通过AOP框架技术支持实现的事务管理,主要思想是将事务作为一个“切面”代码单独编写,然后通过AOP技术将事务管理的“切面”植入到业务目标类中。
Spring的声明式事务管理底层是建立在AOP基础上的,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或加入一个事务,在执行完毕之后根据执行情况提交或回滚事务。
Spring事务管理的亮点在于声明式事务管理。Spring允许通过声明方式,在IoC配置中指定事务的边界和事务属性,Spring自动在指定的事务边界上应用事务属性。
声明式事务管理最大的优点在于开发者无需通过编程的方式来管理事务,**只需在配置文件中进行相关的事务规则声明,就可以将事务应用到业务逻辑中**。这使得开发人员可以更加专注于核心业务逻辑代码的编写,在一定程度上减少了工作量,提高了开发效率,所以在实际开发中,通常都推荐使用声明式事务管理。
如何实现Spring的声明式事务管理?
Spring的声明式事务管理可以通过两种方式来实现,一种是基于XML的方式,另一种是基于Annotation的方式。
基于XML方式的声明式事务
基于XML方式的声明式事务是在配置文件中通过<tx:advice>
元素配置事务规则来实现的。当配置了事务的增强处理后,就可以通过编写的AOP配置,让Spring自动对目标生成代理。
配置<tx:advice>
元素时,通常需要指定id和transaction-manager属性,其中id属性是配置文件中的唯一标识,transaction-manager属性用于指定事务管理器。除此之外,还需要配置一个tx:attributes子元素,该子元素可通过配置多个tx:method子元素来配置执行事务的细节
tx:advice元素及其子元素如下图所示:
配置tx:advice元素的重点是配置tx:method子元素,上图中使用灰色标注的几个属性是tx:method元素中的常用属性。其属性描述具体如下:
name 指定对哪些方法起作用
propagation指定事务的传播行为
isolation指定事务的隔离级别
read-only指定事务是否只读
了解了如何在XML文件中配置事务后,接下来通过一个案例来演示如何通过XML方式来实现Spring的声明式事务管理。这里是按照Spring的数据库开发,chapter04的项目代码和数据表为基础(代码在Spring的数据库开发的博客里),编写的一个模拟银行转账的程序,要求在转账时通过Spring对事务进行控制,其具体实现步骤如下。案例代码
(1)在Idea中,在JavaEE-enterprise-application-development-tutorial项目里,创建一个名为chapter05的Maven项目module,导入AOP的依赖
(2)将Spring的数据库开发chapter04项目中的代码和配置文件复制到chapter05项目的java和resources目录下,并在AccountDao接口中,创建一个转账方法transfer(),其代码如下所示。
// 转账
public void transfer(String outUser, String inUser, Double money);
(3)在其实现类AccountDaoImpl中实现transfer()方法,编辑后的代码如下所示。
/**
* 转账
* inUser:收款人
* outUser:汇款人
* money:收款金额
*/
public void transfer(String outUser, String inUser, Double money) {
// 收款时,收款用户的余额=现有余额+所汇金额
this.jdbcTemplate.update("update account set balance = balance +? "
+ "where username = ?",money, inUser);
// 模拟系统运行时的突发性问题
int i = 1/0;
// 汇款时,汇款用户的余额=现有余额-所汇金额
this.jdbcTemplate.update("update account set balance = balance-? "
+ "where username = ?",money, outUser);
}
在上述代码中,使用了两个update()方法对account表中的数据执行收款和汇款的更新操作。在两个操作之间,添加了一行代码“int i = 1/0; ”来模拟系统运行时的突发性问题。如果没有事务控制,那么在转账操作执行后,收款用户的余额会增加,而汇款用户的余额会因为系统出现问题而不变,这显然是有问题的;如果增加了事务控制,那么在转账操作执行后,收款用户的余额和汇款用户的余额在问题出现前后都应该保持不变。
(4)修改配置文件applicationContext.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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
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-4.3.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<!-- 1.配置数据源 -->
<bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<!--数据库驱动 -->
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<!--连接数据库的url -->
<property name="url" value="jdbc:mysql://localhost/spring?useSSL=false" />
<!--连接数据库的用户名 -->
<property name="username" value="root" />
<!--连接数据库的密码 -->
<property name="password" value="123456" />
</bean>
<!-- 2.配置JDBC模板 -->
<bean id="jdbcTemplate"
class="org.springframework.jdbc.core.JdbcTemplate">
<!-- 默认必须使用数据源 -->
<property name="dataSource" ref="dataSource" />
</bean>
<!--3.定义id为accountDao的Bean -->
<bean id="accountDao" class="com.awen.jdbc.AccountDaoImpl">
<!-- 将jdbcTemplate注入到AccountDao实例中 -->
<property name="jdbcTemplate" ref="jdbcTemplate" />
</bean>
<!-- 4.事务管理器,依赖于数据源 -->
<bean id="transactionManager" class=
"org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 5.编写通知:对事务进行增强(通知),需要编写对切入点和具体执行事务细节 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- name:*表示任意方法名称 -->
<tx:method name="*" propagation="REQUIRED"
isolation="DEFAULT" read-only="false" />
</tx:attributes>
</tx:advice>
<!-- 6.编写aop,让spring自动对目标生成代理,需要使用AspectJ的表达式 -->
<aop:config>
<!-- 切入点 -->
<aop:pointcut expression="execution(* com.itheima.jdbc.*.*(..))"
id="txPointCut" />
<!-- 切面:将切入点与通知整合 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut" />
</aop:config>
</beans>
在文件applicationContext.xml中,首先启用了Spring配置文件的aop、tx和context 3个命名空间(从配置数据源到声明事务管理器的部分都没有变化),然后定义了id为transactionManager的事务管理器,接下来通过编写的通知来声明事务,最后通过声明AOP的方式让Spring自动生成代理。
(5)在com.awen.jdbc包中,创建测试类TransactionTest,并在类中编写测试方法xmlTest(),如文件所示。文件TransactionTest.java
package com.awen.jdbc;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import
org.springframework.context.support.ClassPathXmlApplicationContext;
//测试类
public class TransactionTest {
@Test
public void xmlTest(){
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("applicationContext.xml");
// 获取AccountDao实例
AccountDao accountDao =
(AccountDao)applicationContext.getBean("accountDao");
// 调用实例中的转账方法
accountDao.transfer("柳小子", "柳狗柱", 5000.0);
// 输出提示信息
System.out.println("转账成功!");
}
}
在文件TransactionTest.java中,获取了AccountDao实例后,调用了实例中的转账方法,由柳小子向柳狗柱的账户中转入100元。如果在配置文件中所声明的事务代码能够起作用,那么在整个转账方法执行完毕后,柳小子和柳狗柱的账户余额应该都是原来的数值。在执行转账操作前,先查看account表中的数据,如图所示。
account表从图可以看出,此时柳小子的账户余额是10000,而柳狗柱的账户余额是500。执行完文件TransactionTest.java中的测试方法后,Junit的控制台的显示结果如图所示
警告: Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dataSource' defined in class path resource [applicationContext.xml]: BeanPostProcessor before instantiation of bean failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor#0': Cannot resolve reference to bean 'txPointCut' while setting bean property 'pointcut'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'txPointCut': Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate
java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
... 60 more
Process finished with exit code -1
应该是没导入AOP的依赖包
导入
<!-- AOP begin -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.7.4</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.7.4</version>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.1</version>
</dependency>
<!-- AOP end -->
运行结果
java.lang.ArithmeticException: / by zero
Process finished with exit code -1
从所得结果可以看到,java.lang.ArithmeticException: / by zero
Junit控制台中报出了“/by zero”的算术异常信息。此时如果再次查询数据表account,
会发现表中柳小子和柳狗柱的账户余额并没有发生任何变化(与刚开始的中的显示结果一样),这说明Spring中的事务管理配置已经生效。
我这变化了 不是汉字的问题 我实现注解的方法输入了 成功了
钱没有变化
柳狗柱一直增加
柳小子不变的
基于Annotation方式的声明式事务
1、在Spring容器中注册事务注解驱动;
<tx:annotation-driven transaction-manager="transactionManager"/>
<!-- 5.注册事务管理器驱动 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
2、在需要事务管理的类或方法上使用@Transactional注解。
如果将注解添加在Bean类上,则表示事务的设置对整个Bean类的所有方法都起作用;如果将注解添加在Bean类中的某个方法上,则表示事务的设置只对该方法有效。
使用@Transactional注解时,可以通过参数配置事务详情:
从表可以看出,@Transactional注解与tx:method元素中的事务属性基本是对应的,并且其含义也基本相似。
学习案例
接下来,就对前面模拟银行转账的案例进行改进,来演示基于Annotation方式的声明式事务管理的使用。
案例代码
(1)在在src/main/resources目录下,创建Spring的配置文件applicationContext-annotation.xml,在该文件中声明事务管理器等配置信息,如文件所示。文件 applicationContext-annotation.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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.3.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<!-- 1.配置数据源 -->
<bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<!--数据库驱动 -->
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<!--连接数据库的url -->
<property name="url" value="jdbc:mysql://localhost/spring?useSSL=false" />
<!--连接数据库的用户名 -->
<property name="username" value="root" />
<!--连接数据库的密码 -->
<property name="password" value="123456" />
</bean>
<!-- 2.配置JDBC模板 -->
<bean id="jdbcTemplate"
class="org.springframework.jdbc.core.JdbcTemplate">
<!-- 默认必须使用数据源 -->
<property name="dataSource" ref="dataSource" />
</bean>
<!--3.定义id为accountDao的Bean -->
<bean id="accountDao" class="com.awen.jdbc.AccountDaoImpl">
<!-- 将jdbcTemplate注入到AccountDao实例中 -->
<property name="jdbcTemplate" ref="jdbcTemplate" />
</bean>
<!-- 4.事务管理器,依赖于数据源 -->
<bean id="transactionManager" class=
"org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 5.注册事务管理器驱动 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
与基于XML方式的配置文件相比,文件applicationContext-annotation.xml通过注册事务管理器驱动,替换了文件applicationContext.xml中的第5步编写通知和第6步编写aop,这样大大减少了配置文件中的代码量。需要注意的是,如果案例中使用了注解式开发,则需要在配置文件中开启注解处理器,指定扫描哪些包下的注解。这里没有开启注解处理器是因为在配置文件中已经配置了AccountDaoImpl类的Bean,而@Transactional注解就配置在该Bean类中,所以可以直接生效。
(2)在AccountDaoImpl类的transfer()方法上添加事务注解,添加后的代码如下所示。
@Transactional(propagation = Propagation.REQUIRED,
isolation = Isolation.DEFAULT, readOnly = false)
public void transfer(String outUser, String inUser, Double money) {
// 收款时,收款用户的余额=现有余额+所汇金额
this.jdbcTemplate.update("update account set balance = balance +? "
+ "where username = ?",money, inUser);
// 模拟系统运行时的突发性问题
int i = 1/0;
// 汇款时,汇款用户的余额=现有余额-所汇金额
this.jdbcTemplate.update("update account set balance = balance-? "
+ "where username = ?",money, outUser);
}
上述方法已经添加了@Transactional注解,并且使用注解的参数配置了事务详情,各个参数之间要用英文逗号“, ”进行分隔。
提示在实际开发中,事务的配置信息通常是在Spring的配置文件中完成的,而在业务层类上只需使用@Transactional注解即可,不需要配置@Transactional注解的属性。
(3)在TransactionTest类中,创建测试方法annotationTest(),编辑后的代码如下所示。
@Test
public void annotationTest(){
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("applicationContext-annotation.xml");
// 获取AccountDao实例
AccountDao accountDao =
(AccountDao)applicationContext.getBean("accountDao");
// 调用实例中的转账方法
accountDao.transfer("Jack", "Rose", 100.0);
// 输出提示信息
System.out.println("转账成功!");
}
从上述代码可以看出,与XML方式的测试方法相比,该方法只是对配置文件的名称进行了修改。
java.lang.ArithmeticException: / by zero
注解的是有用的 😂
满足了事务的一致性