JAVA && Spring && SpringBoot2.x — 学习目录
1. 编程式事务和声明式事务
spring支持编程式事务管理和声明式事务管理两种方式。
编程式事务Spring推荐使用
TransactionTemplate
。需要在业务代码中掺杂事务管理的代码,粒度可以作用到代码块级别。声明式事务管理建立在
AOP
之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务可以在配置文件中做相关的事务规则声明(或基于@Transactional
注解的方式),不需要侵入业务代码。但是声明式事务粒度只能做到方法级别。
Spring基于XML的声明式事务配置:
- 配置
事务管理器
,将数据源交由事务管理器。
<bean id="transactionManager" class="...DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
- 配置
事务通知
,根据方法名,指定事务的属性。
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="get*" read-only="true" propagation='"SUPPORTS">
<tx:method name="purchase" protagation="REQUIRED">
</tx:attributes>
</tx:advice>
- 配置
aop
,将事务和切点关联起来。
<aop:config>
<aop:pointcut expresssion="execution(* com.yxr.*(...))" id="txPointcut">
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
</aop:config>
Spring事务管理,本质上就是AOP编程的实现,将数据源配置到数据管理器之后。在配置事务通知,规定哪些方法需要事务。最后将事务通知和切点整合起来。
Spring基于注解的声明式事务配置:
- 配置
事务管理器
,将数据源交由事务管理器。
<bean id="transactionManager" class="...DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
- 启动事务注解
<tx:annotation-driven transaction-manager="transactionManager">
- 可以在对应方法上添加Transaction注解
@Transaction
public void purchase(){}
SpringBoot如何使用事务:
Springboot默认集成事物,只要在方法上加上@Transactional即可
2. Spring事务的使用和注意事项
说起来Spring事务,我们需要先读一下系统之间那点事-异常知多少,复习下Java异常的东西。
注意事项:
- @Transaction可以用于接口以及接口方法上,类以及类方法上。当作用于类上时,该类的所有
public
方法都具有该类型的事务属性。同时可以使用在方法级别使用注解来覆盖类级别的定义。 - Spring不建议
@Transaction
注解作用于接口以及接口方法上,因为这只有在使用基于接口的代理事务情况下才会生效。另外,@Transaction
注解只能被应用到public方法上,这是由于Spring AOP本质上决定的,在其他访问修饰符的方法使用@Transaction
注解,事务不会起作用。 - 只有【外部方法】调用【事务方法】时才会被AOP代理捕获,也就是说,类内部方法调用本类事务方法并不会引起事务行为。(内部方法使用this调用时:this为目标对象)。
- Spring默认情况下会对运行期异常(非检查异常
RuntimeException
)或者ERROR进行回滚。可以使用rollbackFor=Exception.class
对检查时异常进行回滚。 - MySQL事务库表引擎应为InnoDB,否则不支持事务。
3. Spring事务的传播行为
正如行为的英文含义,很好的解释各种行为的含义:
事务的传播(propagation [ˌprɒpə'ɡeɪʃn])
行为是指:如果在开始当前事务之前,一个事务上下文已经存在,此时我们可以有多个选项指定事务性方法的执行行为。
-
PROPAGATION_REQUIRED:
[adj 必须的]
默认传播行为,指的是若当前存在事务,则加入该事务;如果当前没有事务,则创建一个新事务。 -
PROPAGATION_REQUIRES_NEW:
[v 需要新的]
需要创建一个新的,若当前有事务,则将当前事务挂起。 -
PROPAGATION_SUPPORTS:
[v 支持]
当前存在事务,就在事务中运行;当前不存在事务,则不在事务中运行。 -
PROPAGATION_NOT_SUPPORTED
[v 不被支持]
不运行在事务中,当前有事务,则挂掉当前事务。 -
PROPAGATION_NEVER:
[adv 绝不]
不运行在事务中,如果当前有事务,则抛出异常。 -
PROPAGATION_MANDARORY
[[ˈmændətəri]
强制的]`必须运行在事务中,如果当前方法没有事务,则抛出异常。 -
PROPAGATION_NESTED
[[nestɪd] 嵌套的]
当前存在事务,则创建一个事务作为当前事务的嵌套事务运行,如果当前没有事务,则创建一个新的事务。
4. Spring事务隔离级别
隔离级别是指若干个并发事务之间的隔离
(ISOLATION [ˌaɪsəˈleɪʃn])
程度。
- READ_UNCOMMITTED:读未提交,一个事务可以读取到另一个事务未提交的数据。(脏读,不可重复读,幻读)。
- READ_COMMITTED:读已提交,一个事务只能读取另一个事务已经提交的数据。(不可重复读,幻读)。
-
REPEATABLE_READ:可重复读
[rɪˈpi:təbl]
一个事务在整个过程中多次重复执行某个查询,每次返回的结果都相同。(幻读) -
SERIALIZABLE:序列化
[sɪərɪəlaɪ'zəbl]
所有事务依次逐个执行,这样事务之间不可能存在干扰。
5. Spring事务回滚规则
5.1 Exception异常回滚事务
指示spring事务管理器回滚一个事务的推荐方法是在当前事务的上下文抛出异常。Spring事务管理器会捕获任何未处理的异常,然后依据规则决定是否回滚抛出异常的事务。
默认配置下,spring只有在抛出运行时异常(RuntimeException
及其子类)或者Error异常时才会回滚,但是可以配置rollbackFor=Exception.class
将检查时异常进行回滚。
@Transactional(rollbackFor = Exception.class)
@Override
public int addEmployee(Employee record) {
int delResult = employeeMapper.deleteByPrimaryKey(11);
int addResult = employeeMapper.insert(record);
if (delResult == 0 || addResult == 0) {
throw new RuntimeException("123");
}
return addResult;
}
5.2 手动回滚事务
Spring事务原理就是AOP,即当Spring捕获住RuntimeException异常后,自动执行【回滚】操作。但是若不想抛出异常(捕获处理异常),但依旧想回滚事务,该如何处理?
我们可以使用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
手动进行回滚,这样上层便无需处理异常。
@Transactional
@Override
public int addEmployee(Employee record) {
int delResult = employeeMapper.deleteByPrimaryKey(11);
int addResult = employeeMapper.insert(record);
if (delResult == 0 || addResult == 0) {
//手动回滚,上层无需要处理异常。
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return addResult;
}
5.3 套嵌事务的回滚
情况大概是这样的:
- 【事务嵌套调用】事务A方法调用了事务B方法;
- 【事务B出现异常】事务A捕获事务B的异常(其实事务A不想回滚);
- 【事务A回滚操作】最终事务A还是回滚了;
public void testA() throws BussinessException {
try {
bService.testB();
} catch (BussinessException e) {
System.out.println(e.getErrorCode());
}
pictureService.addPicture("", "", "a.jpg", "", new File("d:/1.jpg"));
}
<bean id="txManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="*" rollback-for="com.soft.core.BussinessException" />
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="all" expression="execution(* com.soft.*.*.*ServiceImpl.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="all"/>
</aop:config>
回答:
事务B方法的Transaction配置propagation属性是使用的默认值(required)。这样的话,本质上事务A和事务B共用了一个Transaction
。transactionManager中有一个参数:globalRollbackOnParticipationFailure(参与者失败导致全局回滚)
f (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
if (defStatus.isDebug()) {
logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");
}
processRollback(defStatus);
// Throw UnexpectedRollbackException only at outermost transaction boundary
// or if explicitly asked to.
if (status.isNewTransaction() || isFailEarlyOnGlobalRollbackOnly()) {
throw new UnexpectedRollbackException(
"Transaction rolled back because it has been marked as rollback-only");
}
return;
}
解决办法:
- 【方案一】如果想要事务B失败不影响事务A,可以将事务B的传播行为设置为propagation=Propagation.REQUIRES_NEW。
- 【方案二】将globalRollbackOnParticipationFailure参数设置为false。
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
<property name="globalRollbackOnParticipationFailure" value="false"/>
</bean>
5.4 内部方法调用事务方法
上面说到,【外部方法】调用【事务方法】事务才会生效;【内部方法】调用【本类的事务方法】,事务不会起作用。
service层代码
@Service
public class TestTransactionService {
@Resource
private UsertMapper usertMapper;
//业务逻辑方法
public void businessUsert(Usert usert) {
insertUsert(usert);
// 调用远程接口
}
@Transactional
public void insertUsert(Usert usert) {
usertMapper.insert(usert);
throw new RuntimeException("抛出异常");
}
}
Controller层代码
@ResponseBody
@RequestMapping(value = "user", method = RequestMethod.GET)
public String showUserName() {
Usert usert = new Usert();
usert.setId(4);
usert.setAge(20);
usert.setUserName("李吉吉");
usert.setPassword("123");
testTransactionService.businessUsert(usert);
return "success";
}
执行结果
16:21:31.251 [http-bio-8081-exec-1] DEBUG org.mybatis.spring.SqlSessionUtils - Creating a new SqlSession
16:21:31.264 [http-bio-8081-exec-1] DEBUG org.mybatis.spring.SqlSessionUtils - SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5b9581da] was not registered for synchronization because synchronization is not active
16:21:31.354 [http-bio-8081-exec-1] DEBUG org.mybatis.spring.transaction.SpringManagedTransaction - JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@429902b9] will not be managed by Spring
16:21:31.357 [http-bio-8081-exec-1] DEBUG com.springmvc.generic.mybatis.mapper.UsertMapper.insert - ooo Using Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@429902b9]
16:21:31.365 [http-bio-8081-exec-1] DEBUG com.springmvc.generic.mybatis.mapper.UsertMapper.insert - ==> Preparing: insert into user_t (id, user_name, password, age) values (?, ?, ?, ?)
16:21:31.552 [http-bio-8081-exec-1] DEBUG com.springmvc.generic.mybatis.mapper.UsertMapper.insert - ==> Parameters: 4(Integer), 李吉吉(String), 123(String), 20(Integer)
16:21:31.593 [http-bio-8081-exec-1] DEBUG org.mybatis.spring.SqlSessionUtils - Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5b9581da]
16:21:31.597 [http-bio-8081-exec-1] INFO com.springmvc.common.GlobalExceptionHandler - 全局捕获异常日志打印...
================全局捕获异常=================
java.lang.RuntimeException: 抛出异常
at com.springmvc.service.TestTransactionService.insertUsert(TestTransactionService.java:34)
at com.springmvc.service.TestTransactionService.businessUsert(TestTransactionService.java:28)
at com.springmvc.service.TestTransactionService$$FastClassBySpringCGLIB$$75b3ded0.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:640)
at com.springmvc.service.TestTransactionService$$EnhancerBySpringCGLIB$$1b4acae.businessUsert(<generated>)
at com.springmvc.web.UserController.showUserName(UserController.java:52)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
我们可以看到,事务方法没有生效(即没有AOP增强,也没有Rollback操作)。因为我们本类方法调用事务方法。
实际开发中,我们的【业务方法】中可能存在调用远程接口等耗时操作,需要将【事务】抽取出来,但【业务方法】调用【事务方法】事务不起作用。那么如何解决?
解决方法:
@Service
public class TestTransactionService {
@Resource
private UsertMapper usertMapper;
/**
* 内部方法调用本类事务方法的关键,重新进行AOP增强
*/
@Resource
TestTransactionService testTransactionService;
//业务逻辑方法
public void businessUsert(Usert usert) {
testTransactionService.insertUsert(usert);
}
@Transactional
public void insertUsert(Usert usert) {
usertMapper.insert(usert);
throw new RuntimeException("抛出异常");
}
}
日志打印
16:39:00.643 [http-bio-8081-exec-1] DEBUG org.mybatis.spring.SqlSessionUtils - Creating a new SqlSession
16:39:00.643 [http-bio-8081-exec-1] DEBUG org.mybatis.spring.SqlSessionUtils - Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@508ee7ba]
16:39:00.644 [http-bio-8081-exec-1] DEBUG org.mybatis.spring.transaction.SpringManagedTransaction - JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@4be855b6] will be managed by Spring
16:39:00.644 [http-bio-8081-exec-1] DEBUG com.springmvc.generic.mybatis.mapper.UsertMapper.insert - ooo Using Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@4be855b6]
16:39:00.644 [http-bio-8081-exec-1] DEBUG com.springmvc.generic.mybatis.mapper.UsertMapper.insert - ==> Preparing: insert into user_t (id, user_name, password, age) values (?, ?, ?, ?)
16:39:00.644 [http-bio-8081-exec-1] DEBUG com.springmvc.generic.mybatis.mapper.UsertMapper.insert - ==> Parameters: 4(Integer), 李吉吉(String), 123(String), 20(Integer)
16:39:00.646 [http-bio-8081-exec-1] DEBUG org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@508ee7ba]
16:39:00.656 [http-bio-8081-exec-1] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization rolling back SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@508ee7ba]
16:39:00.656 [http-bio-8081-exec-1] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@508ee7ba]
16:39:00.658 [http-bio-8081-exec-1] INFO com.springmvc.common.GlobalExceptionHandler - 全局捕获异常日志打印...
================全局捕获异常=================
java.lang.RuntimeException: 抛出异常
at com.springmvc.service.TestTransactionService.insertUsert(TestTransactionService.java:37)
at com.springmvc.service.TestTransactionService$$FastClassBySpringCGLIB$$75b3ded0.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:708)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:98)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:262)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:95)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:644)
at com.springmvc.service.TestTransactionService$$EnhancerBySpringCGLIB$$d0b071e3.insertUsert(<generated>)
at com.springmvc.service.TestTransactionService.businessUsert(TestTransactionService.java:31)
在service类中,将【本类对象】引入【本类】中,可以看到内部方法调用事务方法时,事务方法进行了AOP增强,即出现异常时回滚。
小伙伴会问,若是使用@Autowired注解会出现什么情况?
当使用@Autowired注解时,用来注入已有的bean。但是有些时候,会注入失败。原因就是@Autowired默认就是@Autowired(required=true)
,表示注入的时候,该bean必须存在,否则就会注入失败。
org.springframework.beans.factory.BeanCreationException: Error creating
bean with name 'testTransactionService': Injection of autowired dependencies
failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire
field: com.springmvc.service.TestTransactionService
com.springmvc.service.TestTransactionService.testTransactionService; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException:
No qualifying bean of type [com.springmvc.service.TestTransactionService]
found for dependency: expected at least 1 bean which qualifies as autowire
candidate for this dependency. Dependency annotations:
{@org.springframework.beans.factory.annotation.Autowired(required=true)}
使用@Autowired注入属性之后,会但是在bean容器中并没有该类型的对象,于是在项目启动的时候出现上图的错误,但若是我们设置@Autowired(required=false)后,本质上,该对象并未注入进去,会出现空指针异常。
故:我们要使用@Resource属性。
有些小伙伴会问,会不会出现循环依赖呢?
spring是将Bean对象实例化(依赖无参构造函数),在设置对象属性的值。避免setter和field的循环依赖。
循环依赖其实就是循环引用,也就是两个或者两个以上的bean互相持有对象,最终形成闭环。
解决Spring循环依赖的依据其实是基于Java的引用传递,当我们获取对象引用时,对象的field是可以延后设置的。【但构造器必须是在获取引用之前】。
- createBeanInstance:实例化,其实就是调用对象的构造方法实例化对象。
- populateBean:填充对象,这一步主要是多bean的依赖属性进行填充。
- initializeBean:调用spring xml的init方法。
我们要解决循环引用也应该是从初始化过程着手,对于单例来说,在spring容器整个生命周期内,只有一个对象。保存在Cache中。spring为解决单例的循环依赖问题,使用了三级缓存。
在createBeanInstance之后,其实单例对象已经被创建出来(调用了构造器),虽然不够完美(未进行初始化的第二步和第三步),但是已经能够被认出来(根据对象引用能定位到堆中的对象)。