spring有编程式事务和声明式事务,一般都比较推荐使用声明式事务。因为编程式事务与业务代码具有一定的耦合性质,在做改动的时候势必会牵连到主业务,所以一般都会使用推荐的声明式事务,即使用注解的方法来进行结构。这篇只看声明式事务的办法。即@Transactional注解。
首先看下这个注解类。
public @interface Transactional {
/**
* Alias for {@link #transactionManager}.
* @see #transactionManager
*/
@AliasFor("transactionManager")
String value() default "";
/**
* A qualifier value for the specified transaction.
* May be used to determine the target transaction manager,
* matching the qualifier value (or the bean name) of a specific
* {@link org.springframework.transaction.PlatformTransactionManager}
* bean definition.
* @since 4.2
* @see #value
*/
@AliasFor("value")
String transactionManager() default "";
//事务的传播属性
Propagation propagation() default Propagation.REQUIRED;
//事务的隔离级别
Isolation isolation() default Isolation.DEFAULT;
//超时时间
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
/**
* A boolean flag that can be set to {@code true} if the transaction is
* effectively read-only, allowing for corresponding optimizations at runtime.
* Defaults to {@code false}.
*
This just serves as a hint for the actual transaction subsystem;
* it will not necessarily cause failure of write access attempts.
* A transaction manager which cannot interpret the read-only hint will
* not throw an exception when asked for a read-only transaction
* but rather silently ignore the hint.
* @see org.springframework.transaction.interceptor.TransactionAttribute#isReadOnly()
* @see org.springframework.transaction.support.TransactionSynchronizationManager#isCurrentTransactionReadOnly()
*/
boolean readOnly() default false;
//指定异常的回滚
Class<? extends Throwable>[] rollbackFor() default {};
//指定类名的异常回滚
String[] rollbackForClassName() default {};
//指定异常不回滚
Class<? extends Throwable>[] noRollbackFor() default {};
//指定类名的异常不回滚
String[] noRollbackForClassName() default {};
}
事务的传播属性以及事务的隔离级别可以自行百度。这里不做测试。
我们一般使用@Transactional注解都是用在类的方法上。官网也不建议使用在接口类上面,注解肯定都是用到了aop的思想,即使用了动态代理。而如果使用cglib动态代理肯定没有办法代理接口类。所以我们真正使用的时候都在类的方法上面。
首先写测试的实例。
主要就是按照下面的测试来写的。使用是springboot+mybatis。因为这个用得熟练一点,主要看测试方法。都是简单的测试代码。过一遍就好。
package com.test.dao;
public class Dao {
private Integer id;
private String name;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
package com.test.mapper;
import org.apache.ibatis.annotations.Insert;
import com.test.dao.Dao;
public interface DaoMapper {
@Insert("INSERT INTO testtransaction (name) VALUES (#{name})")
int insertSql(Dao dao);
}
package com.test.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.test.dao.Dao;
import com.test.mapper.DaoMapper;
@Service
public class TestService {
@Autowired
private DaoMapper daoMapper;
@Transactional
public void testpub() {
Dao dao=new Dao();
dao.setName("testpub");
daoMapper.insertSql(dao);
throw new RuntimeException("has RuntimeException");
}
@Transactional
protected void testpro() {
Dao dao=new Dao();
dao.setName("testpro");
daoMapper.insertSql(dao);
throw new RuntimeException("has RuntimeException");
}
@Transactional
public void testNotThis() {
Dao dao=new Dao();
dao.setName("testNotThis");
daoMapper.insertSql(dao);
throw new RuntimeException("has RuntimeException");
}
@Transactional
public void testThis1() {
Dao dao=new Dao();
dao.setName("testThis");
daoMapper.insertSql(dao);
throw new RuntimeException("has RuntimeException");
}
public void testThis() {
this.testThis1();
}
}
package com.test;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.test.mapper")
public class AppTransaction {
public static void main(String[] args) {
SpringApplication.run(AppTransaction.class, args);
}
}
application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driverClassName=com.mysql.jdbc.Driver
#logging.level.root=debug
package transaction;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.test.AppTransaction;
import com.test.service.TestService;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = AppTransaction.class)
public class Test extends TestService {
@Autowired
private TestService testService;
@org.junit.Test
public void testPublic() {
testService.testpub();
}
@org.junit.Test
public void testProtectd() {
super.testpro();
}
@org.junit.Test
public void testNotThis() {
testService.testNotThis();
}
@org.junit.Test
public void testThis() {
testService.testThis();
}
}
初始的数据库。
不加注解会是什么情况都知道,这个就不测试了。
首先我们写业务的时候都是使用public方法,极少使用protectd和private方法,但是如果我们使用protectd和private方法呢?
首先都知道private的权限非常的小,而且基本也不会使用。这里就明确的说事务不起作用。
我们执行以下testPublic和testProtectd方法。执行完后数据库如下:
证明了对于protectd方法事务注解是不起作用的。
因为原理这个注解是通过aop代理的,代理的2种方式不细说,因为对protected以及private方法不能调用,所以注解无法生效,即aop拿到了这个class类,却不能对这2种方法进行操作。
清空数据库。
执行方法。
发现this方法调用后的注解没有生效,依然插入进去了。没有做回滚。说明this调用方法的注解不能使用。这个还是跟aop有关联,首先aop代理了方法testThis这个方法,而方法里面再调用其它方法的时候,这个在aop里面就是一个普通方法了,即aop只对这个方法起作用,而这个方法调用了本类的其它方法只能作为普通方法处理了,但是如果调用其他类的方法aop依然起作用,即注解依然生效。
测试如下:加一个service。
package com.test.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.test.dao.Dao;
import com.test.mapper.DaoMapper;
@Service
public class TestService2 {
@Autowired
private DaoMapper daoMapper;
@Transactional
public void testpub() {
Dao dao=new Dao();
dao.setName("testpub");
daoMapper.insertSql(dao);
throw new RuntimeException("has RuntimeException");
}
@Transactional
protected void testpro() {
Dao dao=new Dao();
dao.setName("testpro");
daoMapper.insertSql(dao);
throw new RuntimeException("has RuntimeException");
}
@Transactional
public void testNotThis() {
Dao dao=new Dao();
dao.setName("testNotThis");
daoMapper.insertSql(dao);
throw new RuntimeException("has RuntimeException");
}
@Transactional
public void testThis1() {
Dao dao=new Dao();
dao.setName("testThis");
daoMapper.insertSql(dao);
throw new RuntimeException("has RuntimeException");
}
public void testThis() {
testThis1();
}
}
TestService 改为:
package com.test.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.test.dao.Dao;
import com.test.mapper.DaoMapper;
@Service
public class TestService {
@Autowired
private DaoMapper daoMapper;
@Transactional
public void testpub() {
Dao dao=new Dao();
dao.setName("testpub");
daoMapper.insertSql(dao);
throw new RuntimeException("has RuntimeException");
}
@Transactional
protected void testpro() {
Dao dao=new Dao();
dao.setName("testpro");
daoMapper.insertSql(dao);
throw new RuntimeException("has RuntimeException");
}
@Transactional
public void testNotThis() {
Dao dao=new Dao();
dao.setName("testNotThis");
daoMapper.insertSql(dao);
throw new RuntimeException("has RuntimeException");
}
@Transactional
public void testThis1() {
Dao dao=new Dao();
dao.setName("testThis");
daoMapper.insertSql(dao);
throw new RuntimeException("has RuntimeException");
}
public void testThis() {
TestService2 testService2=new TestService2();
testService2.testThis1();
}
}
清空数据库。再执行测试类的testThis方法。
发现生效了,这就是aop代理的机制,详细原理要在spring的源码中才能体现,以后有空来详细阐述一下这方面的aop,现在就只需要了解这个机制就行了。
先给结论:
如果指定异常回滚,出现了这种异常就会回滚。
如果指定异常不回滚,出现了这种异常就不会回滚。
如果都不指定,默认是RuntimeException异常回滚。即便是继承了RuntimeException的异常类也不会出发默认回滚!
所以我们就以继承了RuntimeException的异常类作为示例。
自定义一个异常:
package com.test.service;
public class MyException extends Exception {
public MyException(String msg) {
super(msg);
}
}
测试类(Test)添加如下代码:
@org.junit.Test
public void testRollback() {
try {
testService.testRollbackFor();
} catch (MyException e) {
e.printStackTrace();
}
}
@org.junit.Test
public void testNoRollback() {
try {
testService.testNoRollbackFor();
} catch (MyException e) {
e.printStackTrace();
}
}
@org.junit.Test
public void testNoRollbackAndRuntimeExcep() {
try {
testService.testNoRollbackForAndRuntimeException();
} catch (MyException e) {
e.printStackTrace();
}
}
TestService添加如下代码:
@Transactional(rollbackFor = MyException.class)
public void testRollbackFor() throws MyException {
Dao dao=new Dao();
dao.setName("testRollbackFor");
daoMapper.insertSql(dao);
throw new MyException("has RuntimeException");
}
@Transactional(noRollbackFor = MyException.class)
public void testNoRollbackFor() throws MyException {
Dao dao=new Dao();
dao.setName("testNoRollbackFor");
daoMapper.insertSql(dao);
throw new MyException("has RuntimeException");
}
@Transactional(noRollbackFor = MyException.class)
public void testNoRollbackForAndRuntimeException() throws MyException {
Dao dao=new Dao();
dao.setName("testNoRollbackForAndRuntimeException");
daoMapper.insertSql(dao);
throw new RuntimeException("has RuntimeException");
}
执行testRollback和testNoRollback方法。
数据库如下:
证明可以指定异常回滚或不回滚。
当然了RuntimeException是最后一部分,如果前面都没问题,最后还是会判断是不是RuntimeException异常。
借用一张网上的图:
清空数据库:
执行testNoRollbackAndRuntimeExcep方法;
发现依然回滚了。说明RuntimeException是在最后作为判断的依据,即便前面判断不回滚,但最后在判断是RuntimeException还是要回滚的。
@Transactional主要运用了aop,所以如果对aop熟悉的话这些问题自然立马就能想清楚。如果对aop代理不熟悉的话,可能理解起来稍稍困难一点,建议多尝试几次可能性以后看源码的话印象会更加深刻。当然没有举完所有的可能性。如有疑惑请自行尝试。
项目链接:
https://download.csdn.net/download/qq_37822914/12454277