Spring学习笔记 - 第三章 - AOP与Spring事务

2023-07-29,,

原文地址:Spring学习笔记 - 第三章 - AOP与Spring事务

Spring 学习笔记全系列传送门:

Spring学习笔记 - 第一章 - IoC(控制反转)、IoC容器、Bean的实例化与生命周期、DI(依赖注入)

Spring学习笔记 - 第二章 - 注解开发、配置管理第三方Bean、注解管理第三方Bean、Spring 整合 MyBatis 和 Junit 案例

【本章】Spring学习笔记 - 第三章 - AOP与Spring事务

目录
1、AOP 简介
1.1 什么是 AOP
1.2 AOP 的作用
1.3 AOP 核心概念
2、AOP 入门案例
2.1 需求分析
2.2 思路分析
2.3 环境准备
2.4 AOP 实现步骤
2.5 相关知识点
3、AOP 工作流程与核心概念
3.1 AOP 工作流程
3.2 AOP 核心概念
4、AOP 配置管理
4.1 AOP 切入点表达式
4.1.1 语法格式
4.1.2 通配符
4.1.3 书写技巧
4.2 AOP 通知类型
4.2.1 类型介绍
4.2.2 环境准备
4.2.3 通知类型的使用
4.2.3.1 通知类型
4.2.3.2 通知类型相关知识点总结
4.3 案例:测试业务层接口万次执行效率
4.3.1 需求分析
4.3.2 环境准备
4.3.3 功能开发
4.4 AOP 通知获取数据
4.4.1 环境准备
4.4.2 获取参数
4.4.2.1 非环绕通知获取方式
4.4.2.2 环绕通知获取方式
4.4.3 获取返回值
4.4.3.1 环绕通知获取返回值
4.4.3.2 非环绕通知获取返回值
4.4.4 获取异常
4.4.4.1 环绕通知获取异常
4.4.4.2 抛出异常后通知获取异常
4.5 百度网盘密码数据兼容处理
4.5.1 需求分析
4.5.2 环境准备
4.5.3 具体实现
5、AOP 总结
5.1 AOP 的核心概念
5.2 切入点表达式
5.3 五种通知类型
5.4 通知中获取参数、返回值以及异常信息
6、AOP 事务管理
6.1 Spring 事务简介及案例
6.1.1 相关概念介绍
6.1.2 转账案例
6.1.2.1 需求分析
6.1.2.2 环境搭建
6.1.2.3 事务管理
6.1.2.4 相关知识点
6.2 Spring 事务角色
6.3 Spring 事务属性及案例
6.3.1 事务配置
6.3.2 转账业务追加日志案例
6.3.2.1 需求分析
6.3.2.2 环境准备
6.3.3 事务传播行为
6.3.3.1 修改 logService 改变事务的传播行为
6.3.3.2 事务传播行为的可选值


1、AOP 简介

AOP是在不改原有代码的前提下对其进行增强

1.1 什么是 AOP

AOP(Aspect Oriented Programming)面向切面编程,一种编程范式,指导开发者如何组织程序结构。

根据 Java 学习路线的内容,要区别于 OOP(Object Oriented Programming)面向对象编程,它们两个是不同的编程范式

1.2 AOP 的作用

在不惊动原始设计的基础上为其进行功能增强(无侵入式编程 / 无入侵式编程)

前面的内容中有技术就可以实现这样的功能,如代理模式

1.3 AOP 核心概念

AOP的核心概念包含:连接点、切入点、通知、通知类、切面

案例:测试代码的执行效率(运行案例,调用不同方法,观察输出结果)

依赖

<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
</dependencies>

Dao 实现类(接口不表)

@Repository
public class BookDaoImpl implements BookDao {
public void save() {
//记录程序当前执行执行(开始时间)
Long startTime = System.currentTimeMillis();
//业务执行万次
for (int i = 0;i<10000;i++) {
System.out.println("book dao save ...");
}
//记录程序当前执行时间(结束时间)
Long endTime = System.currentTimeMillis();
//计算时间差
Long totalTime = endTime-startTime;
//输出信息
System.out.println("执行万次消耗时间:" + totalTime + "ms");
}
public void update(){
System.out.println("book dao update ...");
}
public void delete(){
System.out.println("book dao delete ...");
}
public void select(){
System.out.println("book dao select ...");
}
}

Spring 配置类

@Configuration
@ComponentScan("priv.dandelion")
@EnableAspectJAutoProxy
public class SpringConfig {
}

aop

package priv.dandelion.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component; @Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void priv.dandelion.dao.BookDao.*te())")
private void pt() {
} @Around("pt()")
public void around(ProceedingJoinPoint pjp) throws Throwable {
//记录程序当前执行执行(开始时间)
Long startTime = System.currentTimeMillis();
//业务执行万次
for (int i = 0;i<10000;i++) {
// 表示对原始操作的调用
pjp.proceed();
}
//记录程序当前执行时间(结束时间)
Long endTime = System.currentTimeMillis();
//计算时间差
Long totalTime = endTime-startTime;
//输出信息
System.out.println("执行万次消耗时间:" + totalTime + "ms"); }
}

运行类

public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
bookDao.save();
// bookDao.update();
// bookDao.delete();
// bookDao.select();
}
}

AOP 核心概念图示与解释

图示

概念

连接点:在 AOP 中,将所以原始方法称为连接点(如右侧的 save,update,deleteselect 方法)。
切入点:在上述四个方法中,除 save 方法之外,仅 updatedelete 方法执行了万次,即追加了功能。这些追加了同一种功能的方法(可以是一个也可以是多个,此处为两个)称为一个切入点,反之,如 save, select 方法就不是切入点。
通知:共性功能,即左边的方法。
通知类:通知是一个方法,必须写在一个类中。
切面:通知与切入点的关系,一个通知对应一个切入点,称为一个切面。

AOP 核心概念总结

连接点(JoinPoint):程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等

在SpringAOP中,理解为方法的执行

切入点(Pointcut):匹配连接点的式子

在SpringAOP中,一个切入点可以描述一个具体方法,也可也匹配多个方法,如以下描述:

一个具体的方法:如priv.dandelion.dao包下的BookDao接口中的无形参无返回值的save方法
匹配多个方法:所有的save方法,所有的get开头的方法,所有以Dao结尾的接口中的任意方法,所有带有一个参数的方法
连接点范围要比切入点范围大,是切入点的方法也一定是连接点,但是是连接点的方法就不一定要被增强,所以可能不是切入点。

通知(Advice):在切入点处执行的操作,也就是共性功能

在SpringAOP中,功能最终以方法的形式呈现

通知类:定义通知的类

切面(Aspect):描述通知与切入点的对应关系

2、AOP 入门案例

2.1 需求分析

在方法执行前输出当前系统时间。(使用 AOP )

2.2 思路分析

    导入坐标(pom.xml)
    制作连接点(原始操作,Dao接口与实现类)
    制作共性功能(通知类与通知)
    定义切入点
    绑定切入点与通知关系(切面)

2.3 环境准备

环境搭建完成后:

打印save方法的时候,因为方法中有打印系统时间,所以运行的时候是可以看到系统时间
对于update方法来说,就没有该功能

依赖

<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
</dependencies>

Dao 实现类 (接口细节不表)

public interface BookDao {
public void save();
public void update();
} @Repository
public class BookDaoImpl implements BookDao { public void save() {
System.out.println(System.currentTimeMillis());
System.out.println("book dao save ...");
} public void update(){
System.out.println("book dao update ...");
}
}

Spring 配置类

@Configuration
@ComponentScan("priv.dandelion")
public class SpringConfig {
}

运行类

public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
bookDao.save();
}
}

2.4 AOP 实现步骤

    添加依赖

    AspectJ 是 AOP 思想的一个具体实现

    Spring 有自己的 AOP 实现,但是相比于 AspectJ 来说比较麻烦,所以我们直接采用Spring 整合 ApsectJ 的方式进行 AOP 开发

    <!-- 因为spring-context依赖aop,aop的包已经导入 -->
    <!-- 此处我们额外导入aspectjweaver包 -->
    <dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.4</version>
    </dependency>

    定义接口与实现类

    环境准备已经完成

    定义通知类和通知

    package priv.dandelion.aop;
    
    public class MyAdvice {
    public void method(){
    // 获取系统时间
    System.out.println(System.currentTimeMillis());
    }
    }

    定义切入点

    切入点定义依托一个不具有实际意义的方法进行,即无参数、无返回值、方法体无实际逻辑
    execution及后面编写的内容,将在后续的章节中详细介绍

    public class MyAdvice {
    
        // 定义切入点,注解中execution后面的内容即为对切入点的描述(即当执行到这个方法的时候)
    @Pointcut("execution(void priv.dandelion.dao.BookDao.update())")
    private void pt(){} public void method(){
    System.out.println(System.currentTimeMillis());
    }
    }

    制作切面

    绑定切入点与通知关系

    public class MyAdvice {
    
        @Pointcut("execution(void priv.dandelion.dao.BookDao.update())")
    private void pt(){} // 定义执行时间(在运行到哪一个切入点的什么时间执行,如此处使用Before就是在运行pt()切入点前执行)
    @Before("pt()")
    public void method(){
    System.out.println(System.currentTimeMillis());
    }
    }

    将通知类配给容器并标识其为切面类

    // 告诉Spring,这是一个Bean,需要被管理
    @Component
    // 告诉Spring,扫描到该Bean时,当作AOP处理
    @Aspect
    public class MyAdvice { @Pointcut("execution(void priv.dandelion.dao.BookDao.update())")
    private void pt(){} @Before("pt()")
    public void method(){
    System.out.println(System.currentTimeMillis());
    }
    }

    开启注解格式 AOP 功能

    @Configuration
    @ComponentScan("priv.dandelion")
    // 告诉Spring,包含注解开发的AOP,即启动了@Aspect注解
    @EnableAspectJAutoProxy
    public class SpringConfig {
    }

2.5 相关知识点

@EnableAspectJAutoProxy

名称 @EnableAspectJAutoProxy
类型 配置类注解
位置 配置类定义上方
作用 开启注解格式AOP功能

@Aspect

名称 @Aspect
类型 类注解
位置 切面类定义上方
作用 设置当前类为AOP切面类(需要已经有@EnableAspectJAutoProxy)

@Pointcut

名称 @Pointcut
类型 方法注解
位置 切入点方法定义上方
作用 设置切入点方法
属性 value(默认):切入点表达式

@Before

名称 @Before
类型 方法注解
位置 通知方法定义上方
作用 设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前运行

3、AOP 工作流程与核心概念

SpringAOP的本质:代理模式

3.1 AOP 工作流程

AOP 是基于 Spring 容器管理的 bean 做的增强,所以整个工作过程需要从 Spring 加载 bean 说起

执行流程分解

    Spring 容器启动

    读取所有切面配置中的切入点(仅包含已经被使用到了的切入点,具体案例如下)

    @Component
    @Aspect
    public class MyAdvice { // ptx()未被使用到,不会被读取
    @Pointcut("execution(void priv.dandelion.dao.BookDao.save())")
    private void ptx(){} // pt()被使用到,会被读取
    @Pointcut("execution(void priv.dandelion.dao.BookDao.update())")
    private void pt(){} // 仅使用了pt()
    @Before("pt()")
    public void method(){
    System.out.println(System.currentTimeMillis());
    }
    }

    初始化 bean,判定 bean 对应类中的方法是否匹配到任意切入点

    匹配失败时,初始化bean
    匹配成功时,创建原始对象【即目标对象】的代理对象

    获取 bean 的执行方法

    获取 bean 调用方法并执行,完成操作
    获取的 bean 是代理对象时,根据代理对象的运行模式运行原始方法与增强内容,完成操作

实验:验证容器中是否为代理对象

    修改 App 类,获取类的类型

    不能直接打印对象,直接打印对象走的是对象的toString方法,不管是不是代理对象打印的结果都是一样的,原因是内部对toString方法进行了重写
    此处使用getClass()方法

    public class App {
    public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
    BookDao bookDao = ctx.getBean(BookDao.class); System.out.println(bookDao.getClass());
    }
    }

    修改 MyAdvice 类,不增强

    @Component
    @Aspect
    public class MyAdvice { // Dao中无该方法,bean中的方法匹配不到当前切入点
    @Pointcut("execution(void priv.dandelion.dao.BookDao.update1())")
    private void pt(){} @Before("pt()")
    public void method(){
    System.out.println(System.currentTimeMillis());
    }
    /**
    * 控制台输出为原始对象:
    * class priv.dandelion.dao.impl.BookDaoImpl
    */
    }

    修改 MyAdvice 类,增强

    @Component
    @Aspect
    public class MyAdvice { @Pointcut("execution(void priv.dandelion.dao.BookDao.update())")
    private void pt(){} @Before("pt()")
    public void method(){
    System.out.println(System.currentTimeMillis());
    }
    /**
    * 控制台输出为代理对象:
    * class com.sun.proxy.$Proxy19class com.sun.proxy.$Proxy19
    */
    }

3.2 AOP 核心概念

目标对象(Target):原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的

简单来说,目标对象就是要增强的类 [如:BookServiceImpl 类] 对应的对象,也叫原始对象,不能说它不能运行,只能说它在运行的过程中对于要增强的内容是缺失的。

代理(Proxy):目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现

SpringAOP是在不改变原有设计 (代码) 的前提下对其进行增强的,它的底层采用的是代理模式实现的,所以要对原始对象进行增强,就需要对原始对象创建代理对象,在代理对象中的方法把通知 [如:MyAdvice 中的 method 方法] 内容加进去,就实现了增强,这就是我们所说的代理 (Proxy)

4、AOP 配置管理

4.1 AOP 切入点表达式

切入点表达式在上面的章节出现过,即@Pointcut中的内容

@Pointcut("execution(void priv.dandelion.dao.BookDao.update())")
private void pt(){}

对于AOP中的切入点表达式,本文将详细说明三个内容,分别是语法格式、通配符和书写技巧

4.1.1 语法格式

概念明确:

切入点:要进行增强的方法
切入点表达式:要进行增强的方法的描述方式

描述方式

因为调用接口方法的时候最终运行的还是其实现类的方法,所以下面两种描述方式都是可以的。

执行接口中的方法(按接口描述)

execution(void priv.dandelion.dao.BookDao.update())

执行实现类中的方法(按实现类描述)

execution(void priv.dandelion.dao.impl.BookDaoImpl.update())

切入点表达式的语法

标准格式

动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)

示例及说明

解释下方的代码段:

execution:动作关键字,描述切入点的行为动作,例如 execution 表示执行到指定切入点
public:访问修饰符,还可以是 public,private 等(可以省略,默认 public)
User:返回值,写返回值类型
priv.dandelion.service:包名,多级包使用点连接
UserService:类/接口名称
findById:方法名
int:参数,直接写参数的类型,多个类型用逗号隔开
异常名:方法定义中抛出指定异常(可以省略)

execution(public User priv.dandelion.service.UserService.findById(int))

4.1.2 通配符

通配符详解

*:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀

匹配 priv.dandelion 包下的任意包中的 UserService 类或接口中所有 find 开头的带有一个参数的方法

execution(public * priv.dandelion.*.UserService.find*(*))

..:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写

匹配 priv 包下的任意包中的 UserService 类或接口中所有名称为 findById 的方法

execution(public User priv..UserService.findById(..))

+:用于匹配子类类型

这个使用率较低,描述子类的,做JavaEE开发,继承机会就一次,使用都很慎重,所以很少用它。*Service+,表示所有以Service结尾的接口的子类。

execution(* *..*Service+.*(..))

示例

以案例中使用到的切入点为例

@Repository
public class BookDaoImpl implements BookDao { public void save() {
System.out.println(System.currentTimeMillis());
System.out.println("book dao save ...");
} public void update(){
System.out.println("book dao update ...");
}
}
// 匹配接口,能匹配到
execution(void priv.dandelion.dao.BookDao.update())
// 匹配实现类,能匹配到
execution(void priv.dandelion.dao.impl.BookDaoImpl.update())
// 返回值任意,能匹配到
execution(* priv.dandelion.dao.impl.BookDaoImpl.update())
// 返回值任意,但是update方法必须要有一个参数,无法匹配,要想匹配需要在update接口和实现类添加参数
execution(* priv.dandelion.dao.impl.BookDaoImpl.update(*))
// 返回值为void,priv包下的任意包三层包下的任意类的update方法,匹配到的是实现类,能匹配
execution(void priv.*.*.*.*.update())
// 返回值为void,priv包下的任意两层包下的任意类的update方法,匹配到的是接口,能匹配
execution(void priv.*.*.*.update())
// 返回值为void,方法名是update的任意包下的任意类,能匹配
execution(void *..update())
// 匹配项目中任意类的任意方法,能匹配,但是不建议使用这种方式,影响范围广
execution(* *..*(..))
// 匹配项目中任意包任意类下只要以u开头的方法,update方法能满足,能匹配
execution(* *..u*(..))
// 匹配项目中任意包任意类下只要以e结尾的方法,update和save方法能满足,能匹配
execution(* *..*e(..))
// 返回值为void,priv包下的任意包任意类任意方法,能匹配,*代表的是方法
execution(void priv..*())
// 将项目中所有业务层方法的以find开头的方法匹配
execution(* priv.dandelion.*.*Service.find*(..))
// 将项目中所有业务层方法的以save开头的方法匹配
execution(* priv.dandelion.*.*Service.save*(..))

4.1.3 书写技巧

所有代码按照标准规范开发,否则以下技巧全部失效
描述切入点通常描述接口,而不描述实现类,如果描述到实现类,就出现紧耦合了
访问控制修饰符针对接口开发均采用 public 描述(可省略访问控制修饰符描述
返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用*通配快速描述
包名书写尽量不使用..匹配,效率过低,常用*做单个包描述匹配,或精准匹配
接口名/类名书写名称与模块相关的采用*匹配,例如 UserService 书写成 *Service ,绑定业务层接口名
方法名书写以动词进行精准匹配,名词采用*匹配,例如 getById 书写成 getBy*,selectAll 就书写成 selectAll 即可
参数规则较为复杂,根据业务方法灵活调整
通常不使用异常作为匹配规则

4.2 AOP 通知类型

前面的案例中,涉及到如下内容:

@Before("pt()")

其代表的含义是将通知添加到切入点方法执行的前面

4.2.1 类型介绍

关于 AOP 通知的回顾

AOP 通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码时要将其加入到合理的位置

通知类型(共5种)

前置通知
后置通知
环绕通知(重要)
返回后通知(不常用)
抛出异常后通知(不常用)

AOP 代码加入位置演示

// 环绕通知功能比较强大,它可以追加功能到方法执行的前后,是比较常用的方式,它可以实现其他四种通知类型的功能,具体的实现方式将在下面进行说明
public int methodName() {
// 前置通知追加位置1,即方法执行前
try {
// 前置通知追加位置2,即方法执行前 // code,原始的业务操作,即要被增强的方法本体
return; // 返回后通知追加位置,即方法执行后(只有方法正常执行结束后才进行,如果方法执行抛出异常,返回后通知将不会被添加)
} catch (Exception e) {
// 抛出异常后通知追加位置,即抛出异常后(只有方法抛出异常后才会被添加)
}
// 后置通知追加位置,在方法执行后(不管方法执行的过程中有没有抛出异常都会执行)
}

4.2.2 环境准备

添加依赖

<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
</dependencies>

添加 Dao 层接口和实现类(接口不表)

@Repository
public class BookDaoImpl implements BookDao {
@Override
public void update() {
System.out.println("book dao update ...");
} @Override
public int select() {
System.out.println("book dao select is running ...");
return 100;
}
}

创建 Spring 的配置类

@Configuration
@ComponentScan("priv.dandelion")
@EnableAspectJAutoProxy
public class SpringConfig {
}

创建通知类

@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void priv.dandelion.dao.BookDao.update())")
private void pt(){} public void before() {
System.out.println("before advice ...");
} public void after() {
System.out.println("after advice ...");
} public void around(){
System.out.println("around before advice ...");
System.out.println("around after advice ...");
} public void afterReturning() {
System.out.println("afterReturning advice ...");
} public void afterThrowing() {
System.out.println("afterThrowing advice ...");
}
}

编写 App 运行类

public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
bookDao.update();
}
}

4.2.3 通知类型的使用

4.2.3.1 通知类型

前置通知

@Before("pt()")
public void before() {
System.out.println("before advice ...");
}

后置通知

@After("pt()")
public void after() {
System.out.println("after advice ...");
}

环绕通知

基本使用

@Pointcut("execution(void priv.dandelion.dao.BookDao.update())")
private void pt() {
} @Pointcut("execution(int priv.dandelion.dao.BookDao.select())")
private void pt2() {
} // 环绕通知方法需要有一个ProceedingJoinPoint类型的参数,用于调用原始对象
@Around("pt()")
public void around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice ..."); // 表示对原始操作的调用,环绕通知必须手动调用原始方法
pjp.proceed(); System.out.println("around after advice ...");
} // 有返回值的方法在增强时,环绕通知方法必须要有Object的返回值
@Around("pt2()")
public Object around2(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice ..."); // 表示对原始操作的调用,并接收返回值
Object proceed = pjp.proceed(); System.out.println("around after advice ..."); return proceed;
}

注意事项

环绕通知方法需要有一个 ProceedingJoinPoint 类型的参数,用于调用原始对象,其中,可以使用pjp.proceed()来调用原始方法。
环绕通知不手动调用原始方法时,会自动跳过原始方法(利用这一特性可完成权限校验与内容分配等功能)
由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须要处理 Throwable 异常
有返回值的方法在增强时,环绕通知方法必须要有 Object 的返回值
无返回值的原始方法对应的通知类一般返回 void,但也可以写返回值,正常获取并返回后得到的结果为null,通常会返回 Object

返回后通知

若原始方法在运行过程中发生异常,则该通知不会被执行

@AfterReturning("pt2()")
public void afterReturning() {
System.out.println("afterReturning advice ...");
}

异常后通知

仅在原始方法发生异常会被执行

@AfterThrowing("pt2()")
public void afterThrowing() {
System.out.println("afterThrowing advice ...");
}
4.2.3.2 通知类型相关知识点总结

@After

名称 @After
类型 方法注解
位置 通知方法定义上方
作用 设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法后运行

@AfterReturning

名称 @AfterReturning
类型 方法注解
位置 通知方法定义上方
作用 设置当前通知方法与切入点之间绑定关系,当前通知方法在原始切入点方法正常执行完毕后执行

@AfterThrowing

名称 @AfterThrowing
类型 方法注解
位置 通知方法定义上方
作用 设置当前通知方法与切入点之间绑定关系,当前通知方法在原始切入点方法运行抛出异常后执行名称

@Around

名称 @Around
类型 方法注解
位置 通知方法定义上方
作用 设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前后运行

环绕通知注意事项

环绕通知方法需要有一个 ProceedingJoinPoint 类型的参数,用于调用原始对象,其中,可以使用pjp.proceed()来调用原始方法。
环绕通知不手动调用原始方法时,会自动跳过原始方法(利用这一特性可完成权限校验与内容分配等功能)
由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须要处理 Throwable 异常
有返回值的方法在增强时,环绕通知方法必须要有 Object 的返回值
无返回值的原始方法对应的通知类可以返回 void,但也可以写返回值,正常获取并返回后得到的结果为null,通常会返回 Object

4.3 案例:测试业务层接口万次执行效率

4.3.1 需求分析

测试万次执行时间则需要知道程序运行前后的时间

因此此处应使用环绕通知,以便于原始方法执行前后都可以进行增强

环绕通知需要包含返回值,因为原始方法需要返回值

4.3.2 环境准备

依赖

实体类

public class Account implements Serializable {

    private Integer id;
private String name;
private Double money; // setter, getter, toString
}

Dao接口

public interface AccountDao {

    @Insert("insert into tbl_account(name,money)values(#{name},#{money})")
void save(Account account); @Delete("delete from tbl_account where id = #{id} ")
void delete(Integer id); @Update("update tbl_account set name = #{name} , money = #{money} where id = #{id} ")
void update(Account account); @Select("select * from tbl_account")
List<Account> findAll(); @Select("select * from tbl_account where id = #{id} ")
Account findById(Integer id);
}

Service实现类(接口不表)

@Service
public class AccountServiceImpl implements AccountService { @Autowired
private AccountDao accountDao; public void save(Account account) {
accountDao.save(account);
} public void update(Account account){
accountDao.update(account);
} public void delete(Integer id) {
accountDao.delete(id);
} public Account findById(Integer id) {
return accountDao.findById(id);
} public List<Account> findAll() {
return accountDao.findAll();
}
}

properties 配置文件

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_db?useSSL=false
jdbc.username=root
jdbc.password=123456

基础配置类

@Configuration
@ComponentScan("priv.dandelion")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
public class SpringConfig {
}

JDBC配置类

public class JdbcConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String userName;
@Value("${jdbc.password}")
private String password; @Bean
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(userName);
ds.setPassword(password);
return ds;
}
}

Mybatis配置类

public class MybatisConfig {

    @Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
ssfb.setTypeAliasesPackage("priv.dandelion.entity");
ssfb.setDataSource(dataSource);
return ssfb;
} @Bean
public MapperScannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer msc = new MapperScannerConfigurer();
msc.setBasePackage("priv.dandelion.dao");
return msc;
}
}

4.3.3 功能开发

开启 SpringAOP 的注解功能

在 Spring 的主配置文件 SpringConfig 类中添加注解

@EnableAspectJAutoProxy

创建通知类并编写通知

该类要被Spring管理,需要添加@Component

要标识该类是一个AOP的切面类,需要添加@Aspect

配置切入点表达式,需要添加一个方法,并添加@Pointcut

在runSpeed()方法上添加@Around

@Component
@Aspect
public class ProjectAdvice { // 匹配业务层所有方法
@Pointcut("execution(* priv.dandelion.service.*Service.*(..))")
private void servicePt(){} @Around("servicePt()")
public Object runSpeed(ProceedingJoinPoint pjp) throws Throwable {
Signature signature = pjp.getSignature();
String className = signature.getDeclaringTypeName();
String methodName = signature.getName(); long start = System.currentTimeMillis();
Object proceed = null;
for (int i = 0; i < 10000; i++) {
proceed = pjp.proceed();
}
long end = System.currentTimeMillis(); System.out.println("万次执行:" + className + "." + methodName + "----->" + (end - start) + "ms");
return proceed;
}
}

测试

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTestCase {
@Autowired
private AccountService accountService; @Test
public void testFindById(){
Account ac = accountService.findById(2);
System.out.println(ac);
} @Test
public void testFindAll(){
List<Account> all = accountService.findAll();
System.out.println(all);
}
}

4.4 AOP 通知获取数据

获取参数(所有的通知类型都可以获取参数)

JoinPoint:适用于前置、后置、返回后、抛出异常后通知
ProceedingJoinPoint:适用于环绕通知
获取返回值(前置和抛出异常后通知没有返回值,后置通知可有可无,所以不做研究)
返回后通知
环绕通知
获取异常信息(获取切入点方法运行异常信息,前置和返回后通知不会有,后置通知可有可无,所以不做研究)
抛出异常后通知
环绕通知

4.4.1 环境准备

依赖

<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
</dependencies>

Dao

@Repository
public class BookDaoImpl implements BookDao { public String findName(int id) {
System.out.println("id:"+id);
return "dandelion";
}
}

配置类

@Configuration
@ComponentScan("priv.dandelion")
@EnableAspectJAutoProxy
public class SpringConfig {
}

通知类

@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* priv.dandelion.dao.BookDao.findName(..))")
private void pt(){} @Before("pt()")
public void before() {
System.out.println("before advice ..." );
} @After("pt()")
public void after() {
System.out.println("after advice ...");
} @Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
Object ret = pjp.proceed();
return ret;
}
@AfterReturning("pt()")
public void afterReturning() {
System.out.println("afterReturning advice ...");
} @AfterThrowing("pt()")
public void afterThrowing() {
System.out.println("afterThrowing advice ...");
}
}

运行类

public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
String name = bookDao.findName(100);
System.out.println(name);
}
}

4.4.2 获取参数

4.4.2.1 非环绕通知获取方式
@Before("pt()")
public void before(JoinPoint jp) {
Object[] args = jp.getArgs();
System.out.println(Arrays.toString(args));
System.out.println("before advice ..." );
} @After("pt()")
public void after() {
Object[] args = jp.getArgs();
System.out.println(Arrays.toString(args));
System.out.println("after advice ...");
}
4.4.2.2 环绕通知获取方式

基本的获取参数

ProceedingJoinPoint 继承自 JoinPoint,可直接获取参数

@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
Object ret = pjp.proceed();
return ret;
}

参数的修正

ProceedingJoinPoint 的 proceed() 方法用于调用原始方法,但是该方法除了空参的形式还包括有参形式 proceed(Object[] args),二者本质区别不大,后者用于手动传入参数

@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
// 假设原始方法被调用时传入的参数有问题,此处进行处理
args[0] = 666;
// 手动传入参数,执行原始方法时将使用被手动传入的参数
Object ret = pjp.proceed(args);
return ret;
}

4.4.3 获取返回值

只有返回后AfterReturing和环绕Around这两个通知类型可以获取返回值

4.4.3.1 环绕通知获取返回值

环绕通知获取返回值时,在调用原始方法时接收返回值,并且为通知方法定义返回值类型 Object,如果有需要可以进行修改

@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
// 假设原始方法被调用时传入的参数有问题,此处进行处理
args[0] = 666;
// 手动传入参数,执行原始方法时将使用被手动传入的参数
Object ret = pjp.proceed(args);
return ret;
}
4.4.3.2 非环绕通知获取返回值

仅获取返回值

非环绕通知获取返回值时

需要设置参数用于接收返回值
需要在对应的通知注解中注明用于接收返回值的形参名称

@AfterReturning(value = "pt()", returning = "ret")
public void afterReturning(Object ret) {
System.out.println("afterReturning advice ..." + ret);
}

获取参数的同时获取返回值

同时获取时需要注意

用于获取参数列表的 JoinPoint 类型的形参必须放在第一位,否则会报错

@AfterReturning(value = "pt()", returning = "ret")
public void afterReturning(JoinPoint jp, Object ret) {
Object[] args = jp.getArgs();
System.out.println(Arrays.toString(args));
System.out.println("afterReturning advice ..." + ret);
}

4.4.4 获取异常

对于获取抛出的异常,只有抛出异常后AfterThrowing和环绕Around这两个通知类型可以获取

假设 Dao 层代码中存在异常

@Repository
public class BookDaoImpl implements BookDao { public String findName(int id, String password) {
System.out.println("id:"+id);
if(true){
throw new NullPointerException();
}
return "dandelion";
}
}
4.4.4.1 环绕通知获取异常
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
// 假设原始方法被调用时传入的参数有问题,此处进行处理
args[0] = 666; Object ret = null;
try {
// 手动传入参数,执行原始方法时将使用被手动传入的参数
ret = pjp.proceed(args);
} catch (Throwable t) {
// 捕获异常
t.printStackTrace();
}
return ret;
}
4.4.4.2 抛出异常后通知获取异常
@AfterThrowing(value = "pt()", throwing = "t")
public void afterThrowing(Throwable t) {
System.out.println("afterThrowing advice ..." + t);
}

4.5 百度网盘密码数据兼容处理

4.5.1 需求分析

百度网盘选取分享密码时会多带一个空格,影响正常使用

在业务方法执行之前对所有的输入参数使用trim()进行格式处理
后续类似的操作可能很多,故此处采用 AOP 进行统一处理
需要先处理参数再调用原始方法,故此处采用环绕通知

4.5.2 环境准备

依赖

<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
</dependencies>

Dao(接口不表)

@Repository
public class ResourcesDaoImpl implements ResourcesDao {
@Override
public boolean readResources(String url, String password) {
//模拟校验
return password.equals("root");
}
}

Service(接口不表)

@Service
public class ResourcesServiceImpl implements ResourcesService {
@Autowired
private ResourcesDao resourcesDao; public boolean openURL(String url, String password) {
return resourcesDao.readResources(url,password);
}
}

Spring 配置类

@Configuration
@ComponentScan("priv.dandelion")
public class SpringConfig {
}

运行类

public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
ResourcesService resourcesService = ctx.getBean(ResourcesService.class);
boolean flag = resourcesService.openURL("http://pan.baidu.com/haha", "root");
System.out.println(flag);
}
}

4.5.3 具体实现

    开启 SpringAOP 的注解功能

    @EnableAspectJAutoProxy

    编写通知类

    @Component
    @Aspect
    public class DataAdvice {
    }

    定义切入点

    // 定义切入点,假设对业务层任意包含字符串的参数都进行处理
    @Pointcut("execution(boolean priv.dandelion.service.*Service.*(..))")
    private void servicePt(){}

    添加环绕通知

    @Around("servicePt()")
    public Object trimStr(ProceedingJoinPoint pjp) throws Throwable {
    // 获取参数,若参数为字符串类型,则去除空格
    Object[] args = pjp.getArgs();
    for (int i = 0; i < args.length; i++) {
    if (args[i].getClass().equals(String.class))
    args[i] = args[i].toString().trim();
    }
    // 调用原始方法,手动传入修改后的参数
    Object proceed = pjp.proceed(args); return proceed;
    }

5、AOP 总结

5.1 AOP 的核心概念

概念:AOP(Aspect Oriented Programming) 面向切面编程,一种编程范式

作用:在不惊动原始设计的基础上为方法进行功能增强

核心概念

代理(Proxy):SpringAOP 的核心本质是采用代理模式实现的
连接点(JoinPoint):在 SpringAOP 中,理解为任意方法的执行
切入点(Pointcut):匹配连接点的式子,也是具有共性功能的方法描述
通知(Advice):若干个方法的共性功能,在切入点处执行,最终体现为一个方法
切面(Aspect):描述通知与切入点的对应绑定关系
目标对象(Target):被代理的原始对象成为目标对象

5.2 切入点表达式

切入点表达式标准格式:

动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数)异常名)

execution(* priv.dandelion.service.*Service.*(..))

切入点表达式描述通配符:

作用:用于快速描述,范围描述
*:匹配任意符号(常用)
.. :匹配多个连续的任意符号(常用)
+:匹配子类类型

切入点表达式书写技巧

    标准规范开发
    查询操作的返回值建议使用*匹配
    减少使用..的形式描述包
    对接口进行描述,使用*表示模块名,例如UserService的匹配描述为*Service
    方法名书写保留动词,例如get,使用*表示名词,例如getById匹配描述为getBy*
    参数根据实际情况灵活调整

5.3 五种通知类型

前置通知

后置通知

环绕通知(重点)

环绕通知依赖形参ProceedingJoinPoint才能实现对原始方法的调用
环绕通知可以隔离原始方法的调用执行
环绕通知返回值设置为Object类型
环绕通知中可以对原始方法调用过程中出现的异常进行处理

返回后通知

抛出异常后通知

5.4 通知中获取参数、返回值以及异常信息

获取切入点方法的参数,所有的通知类型都可以获取参数

JoinPoint:适用于前置、后置、返回后、抛出异常后通知
ProceedingJoinPoint:适用于环绕通知

获取切入点方法返回值,前置和抛出异常后通知是没有返回值,后置通知可有可无,所以不做研究

返回后通知
环绕通知

获取切入点方法运行异常信息,前置和返回后通知是不会有,后置通知可有可无,所以不做研究

抛出异常后通知
环绕通知

6、AOP 事务管理

6.1 Spring 事务简介及案例

6.1.1 相关概念介绍

概念

事务作用:在数据层保障一系列的数据库操作的一致性

Spring事务作用:在数据层业务层保障一系列的数据库操作的一致性

业务层本质上是组合了多个数据层的功能,因此业务层也需要事务以保证操作的一致性

使用到的接口

Spring 提供了接口 PlatformTransactionManager 以及其简单实现类 DataSourceTransactionManager(内部使用 JDBC 事务)

public interface PlatformTransactionManager {
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}

6.1.2 转账案例

6.1.2.1 需求分析
6.1.2.2 环境搭建

    数据库

    create database spring_db character set utf8;
    use spring_db;
    create table tbl_account(
    id int primary key auto_increment,
    name varchar(35),
    money double
    );
    insert into tbl_account values(1,'Tom',1000);
    insert into tbl_account values(2,'Jerry',1000);

    创建项目导入 jar 包

    <dependencies>
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.10.RELEASE</version>
    </dependency>
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.16</version>
    </dependency> <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.6</version>
    </dependency> <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.47</version>
    </dependency> <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.2.10.RELEASE</version>
    </dependency> <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>1.3.0</version>
    </dependency> <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
    </dependency> <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.2.10.RELEASE</version>
    </dependency> </dependencies>

    根据表创建模型类

    public class Account implements Serializable {
    
        private Integer id;
    private String name;
    private Double money;
    // setter
    // getter
    // toString
    }

    Dao接口

    public interface AccountDao {
    
        @Update("update tbl_account set money = money + #{money} where name = #{name}")
    void inMoney(@Param("name") String name, @Param("money") Double money); @Update("update tbl_account set money = money - #{money} where name = #{name}")
    void outMoney(@Param("name") String name, @Param("money") Double money);
    }

    Service 接口和实现类(接口不表)

    @Service
    public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountDao accountDao; @Override
    public void transfer(String out,String in ,Double money) {
    accountDao.outMoney(out,money);
    accountDao.inMoney(in,money);
    }
    }

    jdbc.properties 配置文件

    jdbc.driver=com.mysql.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:3306/spring_db?useSSL=false
    jdbc.username=root
    jdbc.password=123456

    JDBCConfig 配置类

    public class JdbcConfig {
    @Value("${jdbc.driver}")
    private String driver;
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.username}")
    private String userName;
    @Value("${jdbc.password}")
    private String password; @Bean
    public DataSource dataSource(){
    DruidDataSource ds = new DruidDataSource();
    ds.setDriverClassName(driver);
    ds.setUrl(url);
    ds.setUsername(userName);
    ds.setPassword(password);
    return ds;
    }
    }

    MybatisConfig 配置类

    public class MybatisConfig {
    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
    SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
    ssfb.setTypeAliasesPackage("priv.dandelion.entity");
    ssfb.setDataSource(dataSource);
    return ssfb;
    } @Bean
    public MapperScannerConfigurer mapperScannerConfigurer(){
    MapperScannerConfigurer msc = new MapperScannerConfigurer();
    msc.setBasePackage("priv.dandelion.dao");
    return msc;
    }
    }

    SpringConfig 配置类

    @Configuration
    @ComponentScan("priv.dandelion")
    @PropertySource("classpath:jdbc.properties")
    @Import({JdbcConfig.class,MybatisConfig.class})
    public class SpringConfig {
    }

    Service 测试类

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes = SpringConfig.class)
    public class AccountServiceTest { @Autowired
    private AccountService accountService; @Test
    public void testTransfer() throws IOException {
    accountService.transfer("Tom","Jerry",100D);
    } }
6.1.2.3 事务管理

    再需要被事务管理的方法上添加@Transactional注解(一般加在接口而非实现类中)

    public interface AccountService {
    /**
    * 转账操作
    * @param out 传出方
    * @param in 转入方
    * @param money 金额
    */ @Transactional
    public void transfer(String out,String in ,Double money);
    }

    在 JDBCConfig 类中配置事务管理器

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
    DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
    // 需要其他的Bean,直接写参数,详见Bean的注入
    transactionManager.setDataSource(dataSource);
    return transactionManager;
    }

    开启事务注解 @EnableTransactionManagement

    @Configuration
    @ComponentScan("priv.dandelion")
    @PropertySource("classpath:jdbc.properties")
    @Import({JdbcConfig.class,MybatisConfig.class})
    @EnableTransactionManagement
    public class SpringConfig {
    }
6.1.2.4 相关知识点

@EnableTransactionManagement

名称 @EnableTransactionManagement
类型 配置类注解
位置 配置类定义上方
作用 设置当前Spring环境中开启注解式事务支持

@Transactional

名称 @Transactional
类型 接口注解 类注解 方法注解
位置 业务层接口上方 业务层实现类上方 业务方法上方
作用 为当前业务层方法添加事务(如果设置在类或接口上方则类或接口中所有方法均添加事务)名称

6.2 Spring 事务角色

事务角色

事务管理员:发起事务方,在Spring中通常指代业务层开启事务的方法
事务协调员:加入事务方,在Spring中通常指代数据层方法,也可以是业务层方法

事务角色说明

Service 中方法 (事务管理员)

// @Transactional
public void transfer(String out,String in ,Double money) {
accountDao.outMoney(out,money);
accountDao.inMoney(in,money);
}

Dao 接口中的方法 (事务协调员)

@Update("update tbl_account set money = money + #{money} where name = #{name}")
void inMoney(@Param("name") String name, @Param("money") Double money); @Update("update tbl_account set money = money - #{money} where name = #{name}")
void outMoney(@Param("name") String name, @Param("money") Double money);

添加@Transactional@Transactional

Service 中运行到 outMoney() 时,调用Dao,操作数据库,该步骤是一个完整的事务
Service 中运行到 inMoney() 时,调用Dao,操作数据库,该步骤是一个完整的事务
outMoney()inMoney() 之间没有事务关系

添加@Transactional@Transactional

Service 中运行到 outMoney() 时,调用Dao,操作数据库,该步骤是一个完整的事务
Service 中运行到 inMoney() 时,调用Dao,操作数据库,该步骤是一个完整的事务
开启 Spring 的事务管理后,Service 的 transfer() 方法成为一个独立事务,此时其下事务将进行事务合并,outMoney()inMoney() 都加入到了transfer()事务中,合并成了完整的事务

6.3 Spring 事务属性及案例

6.3.1 事务配置

补充:

事务仅在发生 Error运行时异常 时才能正常回滚
此外的异常若想正常回滚需要设置 rollbackFor 属性

属性 作用 示例
readOnly 设置是否为只读事务 readOnly=true只读,默认非只读
timeOut 设置事务超时时间 值为 -1 表示永不超时
rollbackFor 设置遇到异常时回滚事务 rollbackFor={异常1.class, 异常2.class}
rollbackForClassName 设置遇到异常时回滚事务 参数为字符串列表,是异常的名称
noRollbackFor 设置不回滚的异常 参数为.class
noRollbackForClassName 设置不回滚的异常 参数为字符串,异常名称
propagation 设置事务传播行为 (详见6.3.2)

6.3.2 转账业务追加日志案例

改进转账案例

    添加日志记录功能
    不论事务是否回滚,都将记录日志
6.3.2.1 需求分析

禁止事务协调员的事务加入事务管理者的事务即可

6.3.2.2 环境准备

    创建日志表

    create table tbl_log(
    id int primary key auto_increment,
    info varchar(255),
    createDate datetime
    )

    添加 LogDao 接口

    public interface LogDao {
    @Insert("insert into tbl_log (info,createDate) values(#{info},now())")
    void log(String info);
    }

    添加 LogService 接口与实现类

    @Service
    public class LogServiceImpl implements LogService { @Autowired
    private LogDao logDao; @Transactional
    public void log(String out,String in,Double money ) {
    logDao.log("转账操作由"+out+"到"+in+",金额:"+money);
    }
    }

    在转账的业务中添加记录日志

    @Service
    public class AccountServiceImpl implements AccountService { @Autowired
    private AccountDao accountDao; @Autowired
    private LogService logService; @Transactional
    public void transfer(String out,String in ,Double money) {
    try{
    accountDao.outMoney(out,money);
    accountDao.inMoney(in,money);
    }finally {
    logService.log(out,in,money);
    }
    }
    }

6.3.3 事务传播行为

代码中,outMoney()inMoney()、三个为独立事务,但是都加入了事务管理者transfer()开启的事务中,合并为一个整体,出现异常时全部回滚,无法记录日志

@Transactional
public void transfer(String out,String in ,Double money) {
try{
accountDao.outMoney(out,money);
accountDao.inMoney(in,money);
}finally {
logService.log(out,in,money);
}
}

可以将log()对应的事务独立出来,不参与与事务管理者transfer()开启的的事务合并,自然可以不随之而回滚,可在任何时候完成事务

6.3.3.1 修改 logService 改变事务的传播行为
@Service
public class LogServiceImpl implements LogService { @Autowired
private LogDao logDao; @Transactional(propagation = Propagation.REQUIRES_NEW)
public void log(String out,String in,Double money ) {
logDao.log("转账操作由"+out+"到"+in+",金额:"+money);
}
}
6.3.3.2 事务传播行为的可选值
传播属性 事务管理员 事务协调员
REQUIRED(默认) 开启事务T 加入事务T
:: 新建事务T2
REQUIRES_NEW 开启事务T 另新建事务T2
:: 自主新建事务T2
SUPPORTS 开启事务T 加入事务T
:: 跟随事务管理员,不创建事务
NOT_SUPPORTED 开启事务T 无视事务管理员,不加入事务
::
MANDATORY 开启事务T 加入事务T
:: ERROR,事务管理员必须有事务
NEVER 开启事务T ERROR,事务管理员不可有事务
::
NESTED 设置savePoint,一旦回滚将回滚到存档点,有客户响应提交 / 回滚

Spring学习笔记 - 第三章 - AOP与Spring事务的相关教程结束。

《Spring学习笔记 - 第三章 - AOP与Spring事务.doc》

下载本文的Word格式文档,以方便收藏与打印。