1. AOP概述
AOP ( Aspect Oriented Programming) : 面向切面编程
指在程序运行期间, 将某段代码动态地切入到指定方法的指定位置进行运行的这种编程方式,叫做面向切面编程
应用场景: 日志记录, 权限控制, 事务控制等等
我们希望在程序运行期间, 在指定方法运行前后, 有对应的日志输出, 提醒程序员程序运行到了一个什么阶段
需求: 希望在每个方法运行前, 告诉我们程序开始了, 程序结束后, 告诉我们程序运行结束
以前的做法:
public class MyCalculator{
//加法
public Integer add(int i,int j) {
System.out.println("程序开始运行");
int result = i + j;
System.out.println("程序运行结束");
return result;
}
//减法
public Integer sub(int i,int j) {
System.out.println("程序开始运行");
int result = i - j;
System.out.println("程序运行结束");
return result;
}
}
缺点:
- 需要在每个方法的指定位置添加上输出语句, 编写输出的内容
- 该类中如果方法很多, 添加或者修改这些输出语句的工作量非常繁重, 可谓牵一发而动全身
- 影响了方法中主要的程序逻辑, 因为输出语句只是一个辅助功能和我们业务本身并没有直接关联
我们的希望:
业务逻辑( 核心功能 ) 在运行期间, 日志模块( 辅助功能 ) 能够自动添加到指定位置
2. JDK动态代理
public static Calculator getProxy(final Calculator calculator) {
//被代理对象calculator的类加载器
ClassLoader loader = calculator.getClass().getClassLoader();
//被代理对象calculator实现的接口
Class<?>[] interfaces = calculator.getClass().getInterfaces();
//方法执行器,帮被代理对象执行目标方法
InvocationHandler handler = new InvocationHandler() {
/**
* @param proxy 代理对象,留给JDK使用,我们任何时候都不要使用
* @param method 当前将要执行目标对象的方法
* @param args
* @return
* @throws Throwable
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("["+method.getName()+"]方法开始执行了");
/**
* 利用反射,执行目标方法
* obj: 要执行哪个对象的方法
* args: 要执行的方法的参数
* result: 目标方法执行后的返回值
*/
Object result = method.invoke(calculator, args);
System.out.println("["+method.getName()+"]方法执行完毕,结果是result:"+result);
//返回值必须返回出去,外界才能真正拿到执行后的结果
return result;
}
};
//创建了calculator的代理对象
Object proxy = Proxy.newProxyInstance(loader, interfaces, handler);
return (Calculator) proxy;
}
注意:
- 在JDK自带的动态代理中,被代理对象( Calculator )必须要实现接口
- InvocationHandler是增强方法, 是动态代理对象执行的
- 代理对象和被代理对象唯一的关联就是实现了同一个接口 (所以最后返回的时候可以放心大胆得转换类型)
3. AOP中的概念
连接点: 相当于数据库中的每一条记录
切入点: 我们感兴趣的记录, 或者说要查找的记录
切入点表达式: SQL语句
4. 使用AOP
4.1 通知
通知类型 | 名称 | 说明 |
---|---|---|
@Before | 前置通知 | 在方法执行前通知 |
@AfterReturning | 返回通知 | 在方法正常返回后通知 |
@AfterThrowing | 异常通知 | 在方法异常后通知 |
@After | 后置通知 | 在方法执行结束后通知 |
@Around | 环绕通知 | 环绕通知 |
try {
//方法执行前
@Before
method.invoke(obj,args)
//方法正常返回后
@AfterReturning
}
catch (){
//方法异常
@AfterThrowing
}
finally {
//方法结束
@After
}
4.2 使用步骤
步骤一: 引入依赖
<!--spring的切面编程基础包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<!--切面相关的三个增强包-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.8</version>
</dependency>
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>
步骤二: 配置
- 将目标类和切面类加入到IOC容器中去
- 告诉Spring哪个是切面类 ( 在切面类上标注@Aspect注解)
- 告诉Spring切面类中的各个方法都何时运行(通知), 标注上对应的注解
切面类:
//1.将切面类加入到IOC容器中
@Component
//2. 告诉Spring该类是切面类
@Aspect
public class logUtils {
//3.告诉各个切面类方法在何时何地执行
//在add方法执行前,执行该方法
@Before("execution(public Integer com.oneway.service.MyCalculator.add(int,int))")
public static void start() {
System.out.println("方法执行开始");
}
//在add方法正常执行并返回,执行该方法
@AfterReturning("execution(public Integer com.oneway.service.MyCalculator.add(int,int))")
public static void returning() {
System.out.println("方法执行完毕并返回");
}
//在add方法执行结束后,执行该方法
@After(""execution(public Integer com.oneway.service.MyCalculator.add(int,int))")
public static void end() {
System.out.println("方法执行结束");
}
//在add方法抛异常后,执行该方法
@AfterThrowing(""execution(public Integer com.oneway.service.MyCalculator.add(int,int))")
public static void throwing() {
System.out.println("方法执行异常");
}
}
测试:
@Test
public void test01() {
ApplicationContext ioc = new ClassPathXmlApplicationContext("applicationContext.xml");
Calculator myCalculator = ioc.getBean(Calculator.class);
myCalculator.div(1,0);
}
**注意: **
- 测试类中, 对象必须从ioc容器中获取
- 如果用类型获取, 必须使用接口类型, 否则获取不到
- 通知方法不严格, 使用private修饰符, 照样可以打印出通知, 但是参数一定要正确, 不知道的参数,一定要告诉Spring框架, 详情见7
5. IOC容器中保存的是组件的代理对象
情况一: 组件有并且实现了接口
接口:
//计算接口
public interface Calculator {
public Integer add(int i, int j);
public Integer sub(int i, int j);
}
实现类:
//实现了上述接口
@Service
public class MyCalculator implements Calculator {
public Integer add(int i,int j) {
int result = i + j;
return result;
}
public Integer sub(int i,int j) {
int result = i - j;
return result;
}
}
测试类:
@Test
public void test01() {
ApplicationContext ioc = new ClassPathXmlApplicationContext("applicationContext.xml");
//如果用类型获取组件,必须使用其接口类型获取
Calculator myCalculator = ioc.getBean(Calculator.class);
System.out.println(myCalculator);
System.out.println(myCalculator.getClass());
}
输出: com.oneway.service.MyCalculator@342c38f8 //看似是MyCalculator类的对象
class com.sun.proxy.$Proxy22 //实际上是JDK自带的动态代理对象
情况二: 组件没有实现接口
组件:
//实现了上述接口
@Service
public class MyCalculator{
public Integer add(int i,int j) {
int result = i + j;
return result;
}
public Integer sub(int i,int j) {
int result = i - j;
return result;
}
}
测试类:
@Test
public void test01() {
ApplicationContext ioc = new ClassPathXmlApplicationContext("applicationContext.xml");
//如果用类型获取组件,必须使用其接口类型获取
MyCalculator myCalculator = ioc.getBean(MyCalculator.class);
System.out.println(myCalculator);
System.out.println(myCalculator.getClass());
}
输出: com.oneway.service.MyCalculator@342c38f8 //看似是MyCalculator类的对象
class com.oneway.service.MyCalculator$$EnhancerBySpringCGLIB$$603cea08 //实际上是cglib下的动态代理对象
结论: 有上述可见, 被切面切过的组件, 其在IOC容器中保存的是其代理对象,并非本类对象
6. 切入点表达式的写法
固定格式: execution( [ 权限访问符 ] 返回值类型 方法全类名 ( 参数列表 ) )
-
*通配符 :
- 匹配一个或者多个字符
- 匹配任意一个参数
- 只能匹配一层路径
- 权限访问符不能用*替代, 但是可以省略不写
- 如果是以*开头的, 可以指代多层路径, 如下最模糊的切入点表达式中
-
. . 通配符 :
- 匹配任意多个参数, 并且是任意类型
- 匹配任意多层路径
最精确的切入点表达式写法: execution ( public void com.oneway.service.MyCalculator.add(int , int) )
最模糊的切入点表达式写法: execution ( * * ( . . ) ) ( 不推荐使用 )
- 切入点表达式可以通过 “&&”、“||”、“!”等操作符结合起来
7. 抽取可重用的切入点表达式
- 随便声明一个返回void的空方法
- 加上注解@Pointcut
- 在@Pointcut属性上写上对应的切入点表达式
- 在通知方法上写上声明的这个空方法的名字调用即可
//2,3加上注解并写上对应的切入点表达式
@Pointcut("execution(* com.oneway.service.MyCalculator.*(..))")
//1.声明一个空方法,返回值void,方法名自己随便命名
public void myPointcut() {}
//4. 在通知方法上掉用
@Before("myPointcut()")
public static void start(JoinPoint joinPoint) {
System.out.println(joinPoint.getSignature().getName() + "方法执行开始"
}
优点: 这么做的好处就是, 如果切入点表达式改变, 不用去每个通知方法上面去更改, 只需要改一处即可!
8. JoinPoint获取目标方法详情
JoinPoint : 就是切入点的意思, 切入点就是我们在众多连接点中最想关注的, 通过切入点, 可以获取该点方法的详情
方法名称 | 作用 |
---|---|
getArgs() | 获取方法参数列表 |
getSignature() | 获取方法详情( 具体如下 ) |
getName() | 获取方法的名称 |
getDeclaringType() | 获取方法的返回值类型 |
getDeclaringTypeName() | 获取方法的返回值类型名称 |
getModifiers() | 获取方法的修饰符 |
切面类:
@AfterReturning(value = "execution(* com.oneway.service.MyCalculator.*(..))",returning = "result")
public static void returning(JoinPoint joinPoint,Object result) {
System.out.println(joinPoint.getSignature().getName() + "方法执行完毕,结果是:" + result);}
@AfterThrowing(value = "execution(* com.oneway.service.MyCalculator.*(..))",throwing = "e")
public static void throwing(JoinPoint joinPoint,Exception e) {
System.out.println(joinPoint.getSignature().getName() + "方法执行异常," + e);}
想要获得方法的返回值 ( 结果 ), 异常信息 , 需要分两步:
- 在通知方法的参数列表中添加上对应的参数类名和参数名称
- 告知Spring参数列表中的哪个参数是用来接收返回值和异常的
- returning: 接收结果的参数
- throwing: 接收异常的参数
注意:
接收结果的参数类型可以用Object, Integer等等类型, 但是接收异常的参数类型只能是Exception
在通知方法中, 其实不那么严格, 修饰符即使是private也可以打印出通知, 但是唯一要求严格的是参数列表,一定不能出错, 对于Spring不知道的参数, 一定要告知Spring框架, 例如上面的returning , throwing
接收异常的时候, 类型一定写大异常, 如写Exception, 不写NullPointerException,否则, 该通知方法只在空指针异常的时候打印异常信息
9. 环绕通知
- 环绕通知这个思路实际上就是利用反射, 去执行目标方法
- 环绕通知是所有通知类型中最强大的,能够全面地控制连接点,甚至可以控制是否执行连接点(即目标方法)。
- 对于环绕通知来说,连接点的参数类型必须是ProceedingJoinPoint。它是 JoinPoint的子接口,允许控制何时执行,是否执行连接点
- 在环绕通知中需要明确调用ProceedingJoinPoint的proceed()方法来执行被代理的方法 , 如果忘记这样做就会导致通知被执行了,但目标方法没有被执行
//1. 整个环绕通知,就是以反射去执行目标方法
@Around(value = "myPointcut()")
//3.参数必须使用ProceedingJoinPoint类型
public Object around(ProceedingJoinPoint point) throws Throwable {
Object[] args = point.getArgs();
String name = point.getSignature().getName();
Object result = null;
try {
System.out.println("[环绕通知]"+name+"方法开始了");
//2.调用目标方法,相当于method.invoke(obj,args)返回值就是结果
//3.如果不调用proceed方法,那么目标方法也就不会执行
result = point.proceed(args);
System.out.println("[环绕通知]"+name+"方法返回,结果:"+result);
} catch (Exception e) {
System.out.println("[环绕通知]"+name+"方法异常");
e.printStackTrace();
}finally {
System.out.println("[环绕通知]"+name+"方法结束");
}
//这里必须返回结果,如果不将结果返回,可能会抛空指针异常,此处结果将返回给调用目标方法的地方(如测试类中)
return result;
}
注意:
- 环绕通知的方法需要返回目标方法执行之后的结果,即调用point.proceed()的返回值,否则会出现空指针异常
- 环绕通知和普通通知同时存在, 执行的顺序如下:
【环绕通知】前置通知 = => 【普通通知】前置通知 = => 目标方法执行 = => 【普通通知】异常/返回通知 = =>
【普通通知】后置通知 = => 【环绕通知】异常/返回通知 = => 【环绕通知】后置通知
10.多切面运行顺序
默认顺序:
- 默认执行顺序, 是根据切面类的全类名进行比较, 谁的名字字母在前, 就先执行
- 先执行, 后结束
自定义顺序:
- 实现Order接口, 重写getOrder() 方法, 返回值是一个int类型的数字, 数字越小, 优先级越高
- 通过@Order(num)注解实现, 同样num是一个int类型数字, 数字越小,优先级越高
图示执行顺序:
由图可以看出:
LogAspect前置通知 ---> ValidateAspect 前置通知 ---> 执行目标方法 ---> ValidateAspect 返回/异常通知 ---> ValidateAspect 结束通知 ---> LogAspect 返回/异常通知 ---> LogAspect结束通知
11. 基于XML配置的AOP
无论是使用注解, 还是使用XML配置, 都需要完成下面三步:
- 将目标类和切面类加入到IOC容器中去
- 告诉Spring哪个是切面类
- 告诉Spring切面类中的各个方法都何时运行(通知), 标注上对应的注解
<!--开启aop的切面自动代理功能-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
<!--1.将目标类加入到IOC容器-->
<bean class="com.oneway.service.MyCalculator"></bean>
<!--2.将切面类加入到IOC容器-->
<bean id="logUtils" class="com.oneway.log.LogUtils"></bean>
<!--AOP的具体配置-->
<aop:config>
<!--切入点配置-->
<aop:pointcut id="log" expression="execution(* com.oneway.service.*.*(..))"/>
<!--2.切面类配置-->
<aop:aspect ref="logUtils" order="1">
<!--3.配置各通知-->
<aop:after method="end" pointcut-ref="log" ></aop:after>
<aop:before method="start" pointcut-ref="log"></aop:before>
<aop:after-returning method="returning" pointcut-ref="log" returning="result"/>
<aop:after-throwing method="throwing" pointcut-ref="log" throwing="e"/>
</aop:aspect>
</aop:config>
注意:
- 多切面的默认顺序是根据配置文件中的先后顺序决定的
- 一个切面中普通通知和环绕通知的先后顺序,也是跟配置文件中的顺序相关联的
- 配置的顺序都是先进后出( 类似栈 )
12. 注解和XML配置的优点
注解: 快速方便
XML配置: 功能完善
推荐: 重要的用XML配置 ( 如权限认证 ); 不重要的用注解
13. 事务
13.1 事务的特点
- 原子性:一个事务涉及的多个操作在逻辑上缺一不可,要么所有操作都执行,要么都不执行
- 一致性:如果一个事务中,一个或者某几个操作失败了,为了保证数据的正确,必须将所有操作都撤销,这就叫做事务的回滚
- 隔离性:多个事务在并发执行过程中互不干扰
- 持久性:事务对数据的修改必须持久化到数据库中
13.2 编程式与声明式事务管理
-
编程式事务管理:
- 获取数据库连接Connection对象
- 取消事务的自动提交
- 执行操作
- 操作正常完成时手动提交事务
- 执行失败时回滚事务
- 释放资源
-
声明式事务管理:
- 配置事务管理器
- 将执行操作的组件加入IOC容器中
- 写上对应注解
事务类型 | 优点 | 缺点 |
---|---|---|
编程式事务管理 | 对于底层能够精细化处理 | 1.复杂<br />2.代码冗杂,业务逻辑不清晰 |
声明式事务管理 | 无法对底层精细化处理 | 1.简单易上手<br />2.无需过多关心底层如何处理事务<br />3.业务代码逻辑清晰 |
如下是Spring框架为我们准备好的事务管理器 (也就是事务切面类)
13.3 Spring事务管理步骤
步骤:
- 配置依赖
- 配置事务管理器(切面)让其管理事务
- 开启基于注解的事务管理模式
- 给事务方法加注解
1. 配置依赖:
<!--Spring面向切面编程的基础包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.1.9.RELEASE</version>
</dependency>
<!--面向切面编程的加强包-->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.8</version>
</dependency>
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>
<!--Spring的事务管理包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.1.9.RELEASE</version>
</dependency>
2. 配置:
<!--配置Jdbc原生事务管理器(切面)让其管理事务-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--注入数据源,管理Connectiion连接-->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--开启事务注解驱动, 使用上面的事务管理器-->
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
3. 加注解:
@Service
public class BookService {
@Autowired
private BookDao bookDao;
//开启事务管理,表明该方法需要事务管理
@Transactional
public void checkOut(String username,String isbn) {
bookDao.sellBook(isbn);
Integer price = bookDao.getPrice(isbn);
bookDao.payMoney(username,price);
}
}
13.4 @Transactional 注解的常用属性
1. 作用:对方法进行事务管理
属性名称 | 类型 | 说明 |
---|---|---|
isolation | Isolation | 事务的隔离级别 |
propagation | Propagation | 事务的传播行为 |
timeout | int | 超时:超过制定时长后自动终止并回滚 |
readOnly | boolean | 设置事务为只读事务 |
rollbackFor | Class[] | 设置哪些事务回滚 |
rollbackForClassName | String[] | 设置哪些事务回滚 ( 全类名 ) ( 不常用 ) |
noRollbackFor | Class[] | 设置哪些事务不回滚 |
noRollbackForClassName | String[] | 设置哪些事务不回滚 ( 全类名 ) ( 不常用 ) |
2. timeout属性
//设置单位为秒,此处即3秒若为执行完成该方法,则回滚,数据库不会发生改变
@Transactional(timeout = 3)
public void checkOut(String username,String isbn) throws InterruptedException {
bookDao.sellBook(isbn);
Integer price = bookDao.getPrice(isbn);
//让线程睡眠3秒
Thread.sleep(3000);
bookDao.payMoney(username,price);
}
3.readOnly属性
默认值为false, 一般用在只有查询的方法上, 这样可以提高查询效率, 数据库不用去校验事务相关的内容, 也不用担心数据库锁的内容, 但是如果该方法中有对数据库修改的操作, 那么加上这个属性后, 会抛出异常
4. 事务回滚的情况
- 运行时异常 : 默认回滚
- 编译时异常 : 默认不回滚
@Transactional
public void checkOut(String username,String isbn) throws Exception {
bookDao.sellBook(isbn);
Integer price = bookDao.getPrice(isbn);
bookDao.payMoney(username,price);
//这里时运行时异常, 默认上面的所有方法都会回滚,所以数据库数据不会改变
int a = 1/0;
}
@Transactional
public void checkOut(String username,String isbn) throws Exception {
bookDao.sellBook(isbn);
Integer price = bookDao.getPrice(isbn);
bookDao.payMoney(username,price);
//这里的异常是编译时异常, 默认不会回滚,数据库中的数据会发生变化
new FileInputStream("D://haha.java");
}
5. rollbackFor 与 noRollbackFor
rollbackFor:指定遇到时必须进行回滚的异常类型,可以为多个
noRollbackFor:指定遇到时不回滚的异常类型,可以为多个
//2.由于这里设置了ArithmeticException异常不回滚,那么checkOut中的所有方法都不会回滚,数据会发生变化
@Transactional(noRollbackFor = ArithmeticException.class)
public void checkOut(String username,String isbn) throws Exception {
bookDao.sellBook(isbn);
Integer price = bookDao.getPrice(isbn);
bookDao.payMoney(username,price);
//1.这里会发生java.lang.ArithmeticException异常,默认运行时异常会回滚
int a = 1/0;
}
//2. 设置了IOException回滚,那么checkOut中的所有方法都要回滚,数据不会发生变化
@Transactional(rollbackFor = IOException.class)
public void checkOut(String username,String isbn) throws Exception {
bookDao.sellBook(isbn);
Integer price = bookDao.getPrice(isbn);
bookDao.payMoney(username,price);
//1.这里的异常是编译时异常, 默认不会回滚,数据库中的数据会发生变化
new FileInputStream("D://haha.java");
}
13.5 事务的隔离级别 ( isolation)
概念: 数据库必须有隔离并发运行各个事务的能力, 使他们互不影响, 避免各种并发问题, 一个事务与其他事务隔离的程度, 我们称之为隔离级别
数据库事务并发问题:
脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
丢失修改(Lost to modify): 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。
不可重复读(Unrepeatableread): 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ UNCOMMITTED ( 读未提交 ) | 有 | 有 | 有 |
READ COMMITTED ( 读已提交 ) | 无 | 有 | 有 |
REPEATABLE READ ( 可重复读 ) | 无 | 无 | 有 |
SERIALIZABLE ( 串行化 ) | 无 | 无 | 无 |
mysql默认的隔离级别是REPEATABLE READ ( 可重复读 )
13.6 事务的传播行为 ( propagation )
概念: 如果有多个事务进行嵌套运行, 子事务和父事务是否共用一个事务 ( 也就是共用一个Connection对象 )
传播行为 | 说明 |
---|---|
REQUIRED ( 默认 ) | 如果当前有事务,就在该事务中运行,否则,自己创建一个事务运行 |
REQUIRES_NEW | 当前方法新创建一个事务运行, 若有事务在运行, 则原事务挂起 |
SUPPORTS | 若有事务在运行, 则在该事务中运行, 否则可以不在事务中运行,不抛异常 |
NOT_SUPPORTED | 当前的方法不应在事务中运行, 若有事务在运行, 则挂起,不抛异常 |
MANDATORY | 当前的方法必须在事务中运行, 若没有运行的事务, 则抛出异常 |
NEVER | 当前的方法不能在事务中运行, 若有正在运行的事务, 则抛出异常 |
NESTED | 不怎么用, 可以自己去了解 |
案例1:
//REQUIRED
tx01() {
//REQUIRED_NEW
tx02() {}
//REQUIRED
tx03() {
....
int a = 1/0;
}
}
分析:
- tx01是父级事务, 其中有两个子事务, 对应的事务行为如上标注所示
- tx01与tx03是同一个事务, 而tx02是自己新开的事务
- tx03事务中抛出异常后, 在tx01事务中所有的方法以及事务回滚 ( tx02不会回滚, 因为自己开的新事务, 不在tx01事务中 )
案例2:
//REQUIRED
tx01() {
//REQUIRED
tx02() {}
//REQUIRED_NEW
tx03() {
....
int a = 1/0;
}
}
分析:
- tx03新开一个事务, 而tx01与tx02共用同一个事务
- 但是此时整个tx01事务都会发生回滚
- 因为tx03抛出的异常会继续向外抛出, tx01感知到异常后, 整个tx01事务回滚
注意: 例1与例2不同在于, 例1中的tx02事务是自己新开的, 并且在tx03抛出异常后, tx02事务早就执行完毕了, 所以不会回滚
案例3:
tx {
//REQUIRED
tx_A {
//REQUIRED
tx_B {}
//REQUIRED_NEW
tx_C {}
}
//REQUIRED_NEW
tx_D {
//REQUIRED
tx_E {}
//REQUIRED_NEW
tx_F {
//情况二:
10/0;
}
}
//情况一:
10/0;
}
- 情况一: ( 异常都是抛出去的, 忽略 try catch )
- 10/0抛异常, tx事务要回滚
- A, B都跟tx共一个事务, 所以A,B不能执行成功
- C开了一个新事务, 并且在抛异常的时候,已经执行完成了, 不会回滚
- D,E,F都是新开事务,并且都已经执行完毕, 都不会回滚
- 所以能正常执行的事务是C,D,E,F
- 情况二: ( 异常都是抛出去的, 忽略 try catch )
- F事务中抛出异常, 异常会正常往外抛
- F回滚,异常抛给D, D,E都要回滚
- 异常继续抛给tx, A,B 都会回滚
- 但是发生异常时, C已经执行完毕, 所以C正常执行
案例四:
@Transactional
public void mulBook() throws Exception {
bookService.checkOut("tom","ISBN-001");
bookService.updatePrice("ISBN-001",200);
}
//正常情况下3秒后回滚
@Transactional(propagation = Propagation.REQUIRED,timeout = 3)
public void checkOut(String username,String isbn) throws Exception {
bookDao.sellBook(isbn);
Integer price = bookDao.getPrice(isbn);
//休眠3秒
Thread.sleep(3);
bookDao.payMoney(username,price);
}
分析:
- checkOut事务中的属性跟随mulBook事务的属性, 因为checkOut方法用的就是mulBook事务中的Connection对象
- 注意前提, checkOut事务一定要是REQUIRED
总结:
1. 异常会按照正常流程往外抛出
2. 已经执行完的REQUIRED_NEW事务不会回滚
3.REQUIRED事务属性, 来源于父事务
13.7 基于XML的事务配置
步骤:
- 配置依赖
- 配置事务管理器(切面)让其管理事务
- 配置出事务方法
- 告诉Spring哪些方法是事务方法
<!--配置事务管理器-->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"></bean>
<!--事务增强, 指定事务管理器,并配置每一个事务方法的详细属性-->
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="checkOut"/>
</tx:attributes>
</tx:advice>
<!--配置切入点-->
<aop:config>
<aop:pointcut id="pt" expression="execution(* com.oneway.service.*.*(..))"/>
<!--告知Spring用事务增强, 去切哪个切面-->
<aop:advisor advice-ref="txAdvice" pointcut-ref="pt"></aop:advisor>
</aop:config>