AOP介绍

什么是AOP?

AOP(Aspect Oriented Programming)直译过来就是面向切面编程,指导开发者如何组织程序结构。

我们先回顾一下OOP(Object Oriented Programming)面向对象编程,OOP作为面向对象编程的模式,获得了巨大的成果,OOP的主要功能是数据封装、继承和多态。

而AOP是一种编程思想,是面向对象编程(OOP)的一种补充,面向对象编程将程序抽象成各个层次,而面向切面编程将程序抽象成各个切面。

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

Spring理念:无侵入式/无入侵式

《Spring实战(第4版)》图书

从该图可以很形象地看到,所谓漆面,相当于应用对象的横切点,我们可以将其单独抽象为单独的模块。

为什么需要AOP?

我们在做开发时,经常会遇到在多个模块之间有某段重复的代码,在传统的面向过程编程中,我们一般的做法是将这些重复的代码封装成一个方法,然后在需要的地方直接调用这个方法,这样当这段代码需要修改时,我们只需要修改这个方法就可以了。

然而需求总是多变的,有一天,我们需要在这个重复的代码进行增加某功能,但原来的方法不能改变,我们又需要将新功能加进去,再独立一个方法出来,然后在需要的地方分别调用这个新方法,又或者说,哪一天我们不需要这个方法了,我们还得删除每一处调用该方法的地方,这样的处理方式,显然是非常繁琐的。

AOP就出现了,在涉及到多个地方具有相同的修改问题我们都可以通过AOP解决。

AOP术语?

AOP领域中的特性术语:

  • 连接点(JoinPoint):程序执行过程中的能够插入切面的任意位置(或者叫点),这个点(粒度)可以是方法的调用、异常抛出、设置变量。

    • 在SpringAOP中,理解为方法的执行
  • 通知(Advice):在切入点处执行的操作,也就是共性功能。

    • 在SpringAOP中,功能最终以方法的形势呈现。
  • 通知类:定义通知的类。

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

    • 在SpringAOP中,一个切入点可以只描述一个具体方法,也可以匹配多个方法。
      • 一个具体方法:site.hikki.dao包下的BookDao接口中的无形参返回值的save方法。
      • **匹配多个方法:**所有的save,所有的get开头的方法,所有以Dao结尾的接口中任意方法,所有带有一个参数的方法。
  • 切面(Aspect):描述通知和切入点的对应关系。

  • 引入(Introduction):引入允许我们向享有的类添加新的方法或属性。

  • 织入(Weaving):将增强处理添加到目标对象中,并创建一个呗增强的对象,这个过程叫织入。

AOP实现分类

AOP要达到的效果是,保证开发者不修改源代码的前提下,去为系统中的业务组件增加某种通用的功能。

AOP的本质是由AOP框架修改业务组件的多个方法的源代码,我们叫他为代理模式。

  • 静态AOP实现。AOP框架在编译阶段对程序源代码进行修改,生成了静态的AOP代理类(生成的.class文件已经被改掉了,需要使用特定的编译器),比如AspectJ。
  • 动态AOP实现。AOP框架在运行阶段对动态生成代理对象(在内存中已JDK动态代理,或Cglib动态地生成AOP代理类),如SpringAOP。

AOP实现比较:

类别 机制 原理 优点 缺点
静态AOP 静态织入 在编译期,切面直接以字节码的形式编译到目标字节码文件中 对系统性能影响 灵活性不够
动态AOP JDK动态代理 在运行期,目标类加载后,为接口动态生成代理类,将切面织入到代理类中 相对静态AOP更加灵活 切入的关注点需要实现接口。对系统有一点性能影响
动态字节码生成 CGLIB 在运行期,目标类加载后,动态生成目标类的子类,将且没按逻辑加入到子类中 没有接口也可以织入 扩展类的示例方法用final修饰时,则无法进行织入
自定义类加载器 在运行期,目标类加载前,将切面逻辑加到目标字节码里 可以对绝大部分类进行织入 代码中如果使用了其他类加载器,则这些类不会织入
字节码转换 在运行期,所有类加载器加载字节码前进行拦截 可以对所有类进行织入

AOP案例

需求

创建一个简单的Spring项目,在不改变updateStudent()方法的前提下,在调用updateStudent()方法执行时,先执行findByNum()方法进行数据的查找。

步骤

  1. 导入AOP相关依赖
  2. 定义通知类,制作通知
  3. 定义切入点
  4. 绑定切入点和通知关系
  5. 定义通知类收Spring容器管理,并且定义当前类为切面类
  6. 开启Spring注解对AOP注解驱动支持

项目结构分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
E:.                            
App.java # 程序入口

├─advice # 通知类
DaoAdvice.java

├─config # Spring配置
SpringConfig.java

└─dao # 数据层
StudentDao.java

└─impl
StudentDaoImpl.java

导入相关依赖

需要两个依赖

  • Spring框架
  • AOP编译器
1
2
3
4
5
6
7
8
9
10
11
12
<!--    Spring框架-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<!-- AOP编译器-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>

定义Dao接口和实现类

StudentDao接口

1
2
3
4
5
6
7
8
9
package site.hikki.dao;

public interface StudentDao {
void findAll();
void findByNum(int num);
void updateStudent();
void addStudent();
void delStudent();
}

StudentDaoImpl实现类

并表明该类是一个bean对象。(在数据层一般使用@Repository而不使用@Controller)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package site.hikki.dao.impl;

import org.springframework.stereotype.Repository;
import site.hikki.dao.StudentDao;

@Repository
public class StudentDaoImpl implements StudentDao {
@Override
public void findAll() {
System.out.println("findALl..");
}
@Override
public void findByNum(int num) {
System.out.println("findByNum:"+num);
}
@Override
public void updateStudent() {
System.out.println("updateStudent...");
}

@Override
public void addStudent() {
System.out.println("addStudent...");
}

@Override
public void delStudent() {
System.out.println("delStudent...");
}
}

定义通知类

定义通知

定义通知的意思就是:需要执行的方法或操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
package site.hikki.advice;

import org.springframework.beans.factory.annotation.Autowired;
import site.hikki.dao.StudentDao;

public class DaoAdvice {
@Autowired //自动装配
private StudentDao studentDao;

public void before(){ // 定义通知
studentDao.findByNum(5);
}
}

定义切入点

定义切入点的意思就是:在哪里进行操作,在运行到哪一个方法进行上面的通知操作。

继续在DaoAdvice类添加以下内容:

1
2
@Pointcut("execution(* site.hikki.dao.StudentDao.updateStudent())")
private void pt(){}

绑定通知和切入点的关系 & 定义切面类

绑定关系,只需要在通知方法上添加通知类型即可。

3-spring – DaoAdvice.j20230110-010

绑定关系后,还需要在该类上面声明该类受Spring容器管理,定义该类为切面类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package site.hikki.advice;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import site.hikki.dao.StudentDao;

@Component // 5.定义通知类受Spring容器管理
@Aspect // 5.定义该类为切面类
public class DaoAdvice { //2. 定义定制类
@Autowired
private StudentDao studentDao;

// 3. 定义切入点
@Pointcut("execution(* site.hikki.dao.StudentDao.updateStudent())")
private void pt(){}

// 4.绑定切入点和通知关系
@Before("pt()")
public void before(){ // 2.定义通知
studentDao.findByNum(5);
}
}

定义配置类 & 开启AOP切面功能

  • @Configuration:表示该类是配置类
  • @ComponentScan("site.hikki"):扫描bean对象范围
  • @EnableAspectJAutoProxy:开启AOP切面功能
1
2
3
4
5
6
7
8
9
10
11
package site.hikki.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan("site.hikki")
@EnableAspectJAutoProxy // 6.开启AOP切面功能
public class SpringConfig {
}

程序入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package site.hikki;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import site.hikki.config.SpringConfig;
import site.hikki.dao.StudentDao;

public class App {

public static void main(String[] args) {
AnnotationConfigApplicationContext stx = new AnnotationConfigApplicationContext(SpringConfig.class);
StudentDao bean = stx.getBean(StudentDao.class);
bean.updateStudent();
}
}

// 输出结果:
// findByNum:5
// updateStudent...

从结果可以看到,我们在调用updateStudent()方法前,先执行了findByNum()方法。

AOP工作流程

流程图

4-AOP工作流程-20230111-012

  1. Spring容器启动

  2. 读取所有切面配置中的切入点

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

    • 匹配失败,创建对象

    • 匹配成功,创建原始对象(目标对象)的代理对象

  4. 获取bean执行方法

    • 获取bean,调用方法并执行,完成操作

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

流程说明

1.Spring容器启动

Spring容器启动:

很好理解,在程序入口AnnotationConfigApplicationContext stx = new AnnotationConfigApplicationContext(SpringConfig.class);已经创建了Ioc容器。

2.读取所有切面配置中的切入点

读取所有切面配置中的切入点:

即那些定义好了切入点,并且也和通知绑定了关系才会被读取。

如果有部分是定义好了切入点,但是没有和通知绑定关系的,则不会被读取。

如下案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Autowired
private StudentDao studentDao;

@Pointcut("execution(* site.hikki.dao.StudentDao.updateStudent())")
private void ptx(){}

// 3. 定义切入点
@Pointcut("execution(* site.hikki.dao.StudentDao.updateStudent())")
private void pt(){}

// 4.绑定切入点和通知关系
@Before("pt()")
public void before(){
studentDao.findByNum(5);
}

在上面代码中,只有pt()方法和通知绑定了关系,ptx()方法没有被使用,即没有和通知绑定关系,这时,Spring只会读取pt这个切入点。

其实很好理解,你可以这么想,如果我定义了一千一万个切入点,但是他们都没有被使用,我如果都扫描,岂不是需要消耗我大量的时间,并且这些切入点都没没有被使用,读取了也是浪费时间,我为什么要读取?是吧,所以,我们只读取那些会被使用的切入点,这样才能提高运行效率。

3.初始化bean

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

如果匹配失败了,Spring会自动创建一个新的对象。

Q:什么是匹配失败了?

A:好比我们使用下面的代码,我们定义的切入点的位置不存在就是匹配失败,找不到这个切入点,或者这个切入点的路径写错了,反正就是找不到这个切入点的位置。

匹配失败后,Spring就会帮我们新建一个对象。

Q:那新建的对象是什么呢?

A:新建的对象是在程序入口调用的那个Bean对象对应的实现类。

下面定义的切入点具体到方法是updateStudent1111(),实际上我们是没有这个方法的,我们去打印一下该对象看看是否会自动新建对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
@Aspect
public class DaoAdvice {
@Autowired
private StudentDao studentDao;

//定义切入点
@Pointcut("execution(* site.hikki.dao.StudentDao.updateStudent1111())")
private void pt(){}

// 绑定切入点和通知关系
@Before("pt()")
public void before(){
studentDao.findByNum(5);
}
}

打印对象结果:

1
2
3
4
5
6
7
8
9
    public static void main(String[] args) {
AnnotationConfigApplicationContext stx = new AnnotationConfigApplicationContext(SpringConfig.class);
StudentDao studentDao = stx.getBean(StudentDao.class);
System.out.println(studentDao);
System.out.println(studentDao.getClass());
}
// 输出结果:
// site.hikki.dao.impl.StudentDaoImpl@51fadaff
// class site.hikki.dao.impl.StudentDaoImpl

从打印结果来看,匹配失败了,Spring自动帮我们新建了一个对象,并且和原来的对象不是同一个。

匹配成功后,Spring会创建一个原始对象(目标对象)的代理对象。

简单来说,就是copy原来的对象形成一个新的对象,后续的操作都在新对象操作,这个新对象也叫代理对象

查看代理对象:

1
2
3
4
5
6
7
8
9
10
    public static void main(String[] args) {
AnnotationConfigApplicationContext stx = new AnnotationConfigApplicationContext(SpringConfig.class);
StudentDao studentDao = stx.getBean(StudentDao.class);
System.out.println(studentDao);
System.out.println(studentDao.getClass());
}

// 输出结果:
// site.hikki.dao.impl.StudentDaoImpl@598bd2ba
// class com.sun.proxy.$Proxy21

从打印结果来看,新对象确实是一个代理对象。

4.获取bean执行方法

获取bean执行方法:

  • 获取bean,调用方法并执行,完成操作

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

在第三步中,我们可以知道,匹配成功后会得到一个代理对象,我们之后的操作都是基于这个代理对象运行原始方法和增强的内容,这也是AOP的核心本质,使用代理对象进行操作,不影响原来的方法。

做到了不侵入式/不入侵式

AOP核心概念

  • 目标对象(Target):原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的。
  • 代理(Proxy):目标对象无法直接完成工作。需要对其进行功能回填,通过原始对象的代理对象实现。

AOP切入点表达式

我们上面有说道切入点的简单使用方法,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
@Aspect
public class DaoAdvice {
@Autowired
private StudentDao studentDao;

//定义切入点
@Pointcut("execution(void site.hikki.dao.StudentDao.updateStudent())")
private void pt(){}

// 绑定切入点和通知关系
@Before("pt()")
public void before(){
studentDao.findByNum(5);
}
}

切入点可以只描述一个具体的方法,也可以匹配多个方法,以上的例子只是描述一个具体的方法,如果我们现在有个需求,需要在多个方法内增加某个功能,这时候不可能每一个方法都添加一个切入点,这样效率太低了。

这时候就需要切入点表达式了,切入点表达式,可以通过通配符匹配多个描述方法。

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

切入点的接口和实现类

StudentDao接口:

1
2
3
public interface StudentDao {
void findAll();
}

StudentDao接口实现类:

1
2
3
4
5
6
7
8
9
10
11
12
package site.hikki.dao.impl;

import org.springframework.stereotype.Repository;
import site.hikki.dao.StudentDao;

@Repository
public class StudentDaoImpl implements StudentDao {
@Override
public void findAll() {
System.out.println("findALl..");
}
}

描述方式一:

执行site.hikki.dao包下StudentDao接口中的无参数findAll方法

1
2
@Pointcut("execution(void site.hikki.dao.StudentDao.findAll())")
private void pt(){}

描述方式二:

执行site.hikki.dao.impl包下StudentDaoImpl实现类中的无参数findAll方法

1
2
@Pointcut("execution(void site.hikki.dao.impl.StudentDaoImpl.findAll())")
private void pt(){}

我们在描述方法时,按照接口和实现类的方式都是可以的。

但有一点不同,在描述实现类时,只会在运行到该类的切入点才会被执行。

如果描述的是接口,那在运行到该接口的切入点都会被执行。如果有这个接口有多个实现类,那这多个实现类在运行到某切入点都是会被执行的。

切入点表达式格式

切入点表达式标准格式:

1
2
动作关键字 (访问修饰符  返回值  包名.类/接口名.方法名(参数)异常名)
execution (public void site.hikki.dao.StudentDao.findByNum(int))
参数 备注
动作关键词 描述切入点的行为动作,例如execution表示执行到指定切入点
访问修饰符 访问修饰符,如public、private等,可省略(一般默认是public)
返回值
包名
类/接口名
方法名
参数
异常名 方法定义中抛出执行异常,可省略

切入点通配符

我们可以使用通配符描述切入点,快速描述。

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

1
2
@Pointcut("execution(public void site.*.*.*.findByNum(int))")
private void pt(){}

**匹配内容:**匹配site包下的任意包中含有一个int类型参数的findByNum()所有方法。

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

1
2
@Pointcut("execution(public void site..findByNum(int))")
private void pt(){}

**匹配内容:**匹配site包下的任意包中含有一个int类型参数的findByNum()所有方法。

+:专用于匹配子类类型

1
2
@Pointcut("execution(public void site.hikki.dao.StudentDao+.*(..))")
private void pt(){}

**匹配内容:**匹配site.hikki.dao包下的StudentDao接口下的所有方法

书写技巧

  • 所有代码按照标准规范开发,否则以下技巧全部失效

  • 描述切入点通常描述接口,而不描述实现类

  • 访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述

  • 返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用*通配快速描述

  • 包名书写尽量不使用..匹配,效率过低,常用*做单个包描述匹配,或精准匹配

  • 接口名/类名书写名称与模块相关的采用*匹配,例如UserService书写成*Service,绑定业务层接口名

  • 方法名书写以动词进行精准匹配,名词采用*匹配,例如getById书写成getBy*selectAll书写成selectAll

  • 参数规则较为复杂,根据业务方法灵活调整

  • 通常不使用异常作为匹配规则

AOP通知类型

通知类型

Spring AOP 中有 5 中通知类型,分别如下:

注解 通知
@Before 通知方法会在目标方法调用之前执行
@After 通知方法会在目标方法返回或异常后调用
@Around 通知方法前会将目标方法封装起来
@AfterReturning 通知方法会在目标方法返回后调用
@AfterThrowing 通知方法会在目标方法抛出异常后调用

初始化项目

项目结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
E:.
└─site
└─hikki
App.java #程序入口

├─aop # AOP配置类
StudentAdvice.java

├─config #Spring配置类
SpringConfig.java

└─dao # 数据层
StudentDao.java

└─impl
StudentDaoImpl.java

Spring配置类

1
2
3
4
5
6
7
8
9
10
11
package site.hikki.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan("site.hikki")
@EnableAspectJAutoProxy
public class SpringConfig {
}

StudentDao接口

1
2
3
4
5
6
package site.hikki.dao;

public interface StudentDao {
void findAll();
int update();
}

StudentDaoImpl实现类

记得设置为bean对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package site.hikki.dao.impl;

import org.springframework.stereotype.Repository;
import site.hikki.dao.StudentDao;
@Repository
public class StudentDaoImpl implements StudentDao {
@Override
public void findAll() {
System.out.println("findAll...");
}
@Override
public int update() {
System.out.println("update...");
return 200;
}
}

App程序入口:

1
2
3
4
5
6
7
8
    public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
StudentDao studentDao = ctx.getBean(StudentDao.class);
studentDao.findAll();
}

// 运行结果:
// findAll...

@Before & @After

属性

  • 名称:@Before
  • 类型:方法注解
  • 位置:通知方法定义上方
  • 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法运行。
  • 相关属性:value(默认):切入点方法名,格式为类名.方法名()
  • 案例:
1
2
3
4
@Before("pt()")
public void Before(){
System.out.println("Before...");
}
  • 名称:@After
  • 类型:方法注解
  • 位置:通知方法定义上方
  • 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法后运行。
  • 相关属性:value(默认):切入点方法名,格式为类名.方法名()
  • 案例:
1
2
3
4
@After("pt()")
public void After(){
System.out.println("After...");
}

实操

在StudentAdvice切面类定义通知和切入点。

site.hikki.dao.StudentDao.findAll()方法定义切入点,切入点方法命名为pt

定义两个通知,分别在原始切入点方法前运行、在切入点方法后运行。预期结果应该是如下的:

1
2
3
Before...
findAll...
After...

编写通知和切入点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package site.hikki.aop;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class StudentAdvice {

@Pointcut("execution(void site.hikki.dao.StudentDao.findAll())")
void pt(){}

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

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

重新运行项目:

输出结果如下:

1
2
3
Before...
findAll...
After...

看到结果和预期一样,先是运行@Before(“pt()”)通知方法,再运行@After(“pt()”)通知方法。

环绕通知(重点)

属性

  • 名称:@Around(重点、常用)
  • 类型:方法注解
  • 位置:通知方法定义上方
  • 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前后运行。
  • 案例:
1
2
3
4
5
6
7
@Around("pt()")
public Object Around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("Around before ...");
Object proceed = pjp.proceed();
System.out.println("Around after ...");
return proceed;
}

实操

在定义通知时,有时候,可能会遇到原始方法返回值没有返回值,接下来我们看看这个案例有返回值和没有返回值有什么区别。

在StudentAdvice切面类定义通知和切入点。

site.hikki.dao.StudentDao.findAll()方法定义切入点,切入点方法命名为pt,该原始方法没有返回值。

定义一个通知,方法内容在切入点的前后运行。

编写通知:

1
2
3
4
5
6
7
8
9
   @Pointcut("execution(void site.hikki.dao.StudentDao.update())")
void pt(){}
@Around("pt()")
public Object Around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("Around before ...");
Object proceed = pjp.proceed();
System.out.println("Around after ...");
return proceed;
}

注意点:

  • 设置通知类型。通知方法的返回类型需要用Object类型,因为你不知道原始方法是否有返回值,如果有,可以使用Object代替,如果没有返回值,也无所谓。
  • 调用原始方法。引入ProceedingJoinPoint pjp参数,pjp.proceed();代表的就是对原始方法的调用。
  • 抛出异常。在调用原始方法时,需要抛出异常,因为你不知道原始方法会不会出错。
  • 返回值。调用pjp.proceed();原始方法后需要接收返回值,可能有返回值,可能也没有返回值,如果有返回值,则在通知最后返回原始值。

01-around

我们根据上面的案例追加一个情况,将find()方法改为update()方法,并且

site.hikki.dao.StudentDao.update()方法定义切入点,切入点方法命名为pt,该原始方法有返回值。

定义一个通知,方法内容在切入点的前后运行。

1
2
3
4
5
6
7
8
9
10
@Pointcut("execution(int site.hikki.dao.StudentDao.update())")
void pt(){}

@Around("pt()")
public Object Around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("Around before ...");
Object proceed = pjp.proceed();
System.out.println("Around after ...");
return proceed;
}

在这个切入点和通知中,有一点点不一样。在切入点中的返回类型由之前的void改成了int,表示在执行原始方法(update方法)后会返回一个值。

而在通知方法中,我们知道pjp.proceed();是调用原始方法的,那调用原始方法就会返回一个值啊,这个值原本是int类型的,现在我们使用Object来接收也是可以的,如果我们修改通知方法的返回结果会怎么样?(我们在update方法默认返回值是200)

很明显,修改通知方法的结果,在运行到update()方法时,返回的值不再是200了,而是我们修改后的值了。

02-around-有返回值

@Around注意事项:

  1. 环绕通知必须依赖形参ProceedingJoinpPoint才能实现对原始方法的调用,进而原始方法调用前后同时添加通知
  2. 通知中如果过未使用ProceedingJoinPoint对原始方法进行调用将跳过原始方法的执行
  3. 对原始方法的调用可以不接受返回值,通知方法设置成void即可,如果接受返回值,必须设定为Object类型
  4. 原始方法的返回值如果是void类型,通知方法的返回值类型可以设置成void,也可以设置为Object
  5. 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须抛出Throwable对象

正确的案例代码:

1
2
3
4
5
6
7
@Around("pt()")
public Object Around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("Around before ...");
Object proceed = pjp.proceed();
System.out.println("Around after ...");
return proceed;
}

抛出异常后通知

  • 名称:@AfterThrowing(了解)

  • 类型:方法注解

  • 位置:通知方法定义上方

  • 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法运行抛出异常后执行

  • 范例:

1
2
3
4
@AfterThrowing("pt()")
public void afterThrowing(){
System.out.println("afterThrowing...");
}
  • 相关属性:value(默认):切入点方法名,格式为类名.方法名()

返回后通知

  • 名称:@AfterReturning(了解)

  • 类型:方法注解

  • 位置:通知方法定义上方

  • 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法正常执行完毕后运行。

  • 范例:

1
2
3
4
@AfterReturning("pt()")
public void afterReturning(){
System.out.println("afterReturning...");
}
  • 相关属性:vallue(默认):切入点方法名,格式为类名.方法名()

AOP案例一:测量业务层接口万次执行效率

需求分析

需求:任意业务层接口执行均可显示其执行效率(执行时长)

分析:

​ ①:业务功能:业务层接口执行前后分别记录时间,求差值得到执行效率
​ ②:通知类型选择前后均可以增强的类型——环绕通知

代码实现

环境准备

  1. Spring整合mybatis对spring_db数据库中的Student进行CRUD操作

  2. Spring整合Junit测试CRUD是否OK。

  3. 在pom.xml中添加aspectjweaver切入点表达式依赖

  4. … …

编写通知类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package site.hikki.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class StudentAdvice {
//切入点
@Pointcut("execution(* site.hikki.service.StudentService.find())")
private void pt(){}

//通知方法
@Around("StudentAdvice.pt()")
public Object runSpeed(ProceedingJoinPoint pjp) throws Throwable {
Signature signature = pjp.getSignature();
String MethodName = signature.getName();
String typeName = signature.getDeclaringTypeName();
// System.out.println(MethodName);
// System.out.println(typeName);
long start = System.currentTimeMillis();
for (int i = 0; i < 20000; i++) {
pjp.proceed();
}
long end = System.currentTimeMillis();
System.out.println(typeName+"."+MethodName+"运行两万次需要消耗的时间:"+(end-start)+"ns");
return pjp.proceed();
}
}

在SpringConfig配置类上开启AOP注解功能

1
2
3
4
5
6
7
8
9
10
11
package site.hikki.config;

import org.springframework.context.annotation.*;

@Configuration //设置该类是配置类
@ComponentScan("site.hikki") //扫描bean
@PropertySource("classpath:jdbc.properties") //导入properties文件
@Import({JdbcConfig.class, MybatisConfig.class}) // 导入第三方的bean
@EnableAspectJAutoProxy //开启AOP注解功能
public class SpringConfig {
}

编写测试类,查看结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package site.hikki.service;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import site.hikki.config.SpringConfig;
import site.hikki.entity.Student;

import java.util.List;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class StudentServiceTest {
@Autowired
private StudentService studentService;

@Test
public void find() {
List<Student> studentList = studentService.find();
for (Student s :studentList) {
System.out.println(s);
}
}

@Test
public void findByNum() {
Student student = studentService.findByNum("3");
System.out.println(student);
}

@Test
public void addStudent() {
Student student = new Student();
student.setAge("25");
student.setMajor("网络工程");
student.setName("小码同学");
int i = studentService.addStudent(student);
System.out.println(i);
}

@Test
public void delStudentBynum() {
int i = studentService.delStudentBynum("6");
System.out.println(i);
}

@Test
public void updateStudentBynum() {
studentService.updateStudentBynum("吴小黑","5");
}
}

03-AOP案例测试-20230111-709

AOP通知获取数据

初始化项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
E:.
└─site
└─hikki
App.java #程序入口

├─aop # AOP切面类
MyAdvice.java

├─config # Spring配置类
SpringConfig.java

└─dao # 数据层
StudentDao.java

└─impl
StudentDaoImpl.java

导入依赖

1
2
3
4
5
6
7
8
9
10
<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>

创建StudentDao接口

1
2
3
4
5
6
package site.hikki.dao;

public interface StudentDao {
void find();
String update(int num,String name);
}

实现StudentDao接口

同时,在该类创建bean对象(在类上方注解@Repository)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package site.hikki.dao.impl;

import org.springframework.stereotype.Repository;
import site.hikki.dao.StudentDao;
@Repository
public class StudentDaoImpl implements StudentDao {
@Override
public void find() {
System.out.println("find...");
}

@Override
public String update(int num,String name) {
System.out.println("update...num:"+num+",name:"+name);
return "https://blog.hikki";
}
}

创建AOP切面类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package site.hikki.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Component // 定义通知类受Spring容器管理
@Aspect // 定义该类为切面类
public class MyAdvice {
@Pointcut("execution(void site.hikki.dao.StudentDao.find())")
private void pt(){}

@Before("pt()")
public void Before(){
System.out.println("before..");
}
@After("pt()")
public void after(){
System.out.println("after...");
}
@Around("pt()")
public Object around(ProceedingJoinPoint pjp){
System.out.println("around...start");
Object proceed = null;
try {
proceed = pjp.proceed();
} catch (Throwable e) {
System.out.println("around:"+e);
}
System.out.println("around...end");
return proceed;
}
@AfterReturning("pt()")
public void afterReturning(){
System.out.println("afterReturning...");
}
@AfterThrowing("pt()")
public void afterThrowing(){
System.out.println("afterThrowing...");
}
}

开启AOP注解功能

1
2
3
4
5
6
7
8
9
10
11
package site.hikki.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.test.context.ContextConfiguration;

@ContextConfiguration
@ComponentScan("site.hikki")
@EnableAspectJAutoProxy // 开启AOP切面功能
public class SpringConfig {
}

程序入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package site.hikki;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import site.hikki.config.SpringConfig;
import site.hikki.dao.StudentDao;

public class App {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
StudentDao studentDao = ctx.getBean(StudentDao.class);
studentDao.find();
}
}

// 运行结果:
// around...start
// before..
// find...
// afterReturning...
// after...
// around...end

我们刚刚运行的项目都是没有参数的,也没有返回值的,我们在实际的项目开发中,在原始方法中,一般都有参数的,有返回值的,我们在使用AOP通知方法时,应该怎么获取这些参数呢?可以更改通知方法的返回值吗?

在上面的案例中,我们在程序入口调用find()方法,使用了AOP增强find的方法,我们下面调用update()方法试试,该方法有返回值,也有参数。

1
2
3
4
5
6
7
8
9
10
    public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
StudentDao studentDao = ctx.getBean(StudentDao.class);
String update = studentDao.update(2, "小码同学");
System.out.println(update);
}

// 运行结果:
// update...num:2,name:小码同学
// https://blog.hikki

我们正常的调用方法,得到的返回值是这样的,我们还没有使用AOP给update()设置增强方法,下面我们试试。

AOP获取参数数据

设置切入点

MyAdvice切面类添加一个切入点,方法名命名为pt2,方法返回值类型使用*通配符,参数使用..,匹配任意多个参数。

1
2
@Pointcut("execution(* site.hikki.dao.StudentDao.update(..))")
private void pt2(){}

设置前置通知(@Before)

在通知方法的参数添加JoinPoint jp可以拿到原始方法的参数,然后通过getArgs()拿到,我们一般将拿到的参数一般是用Object类型定义。

JoinPoint对象描述了连接点方法的运行状态,可以获取到原始方法的调用参数。

1
2
3
4
5
6
@Before("pt2()")
public void Before2(JoinPoint jp){
Object[] args = jp.getArgs();
System.out.println(Arrays.toString(args));
System.out.println("before2..");
}

运行结果:

1
2
3
4
[2, 小码同学]
before2..
update...num:2,name:小码同学
https://blog.hikki

结果可以看到,我们拿到参数是一个数组类型,我们看看JoinPoint对象和ProceedingJoinPoint对象有什么区别,从名字上看着很像。

设置环绕通知(@Around)

Around通知方法也是可以获取到原始方法的参数数据的。Around需要使用ProceedingJoinPoint对象,获取数据的方法一样,都是使用getArgs()

Ctrl +H,可以看到,ProceedingJoinPointJoinPoint的子类。

1
2
3
4
5
6
7
@Around("pt2()")
public Object around2(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
Object ret = pjp.proceed();
return ret;
}

AOP通知获取返回值数据

设置返回后通知(@AfterReturning)

我们在编写通知方法时,一般在注解中,我们只是写上切入点的方法名,这个方法名是默认value值的,如果需要添加多个值,可以点击注解,查看注解的参数类型有哪些,比如@AfterReturning注解有value、pointcut、returning、argNames四个参数类型,根据需求可以设置不同的参数。

其中returning是获取返回值,value我们也用过,也就是切入点。

1
2
3
4
@AfterReturning(value = "pt2()",returning = "ret")
public void afterReturning2(String ret){
System.out.println("afterReturning2..."+ret);
}

注释掉其他通知方法,只留下afterReturning2,查看运行效果:

1
2
update...num:2,name:小码同学
afterReturning2...https://blog.hikki

运行结果可以看到,我们使用returning = "ret"可以获取到原始方法的返回值。

设置环绕通知(@Around)

上面我们也举例过Around获取原始方法的参数数据,不仅如此,Around还可以获取到原始方法的返回值。

1
2
3
4
5
6
@Around("pt2()")
public Object around3(ProceedingJoinPoint pjp) throws Throwable {
Object ret = pjp.proceed();
System.out.println("around2..."+ret);
return ret;
}

AOP通知获取异常数据(了解)

设置返回后通知(@AfterReturning)

1
2
3
4
@AfterThrowing(value = "pt2()",throwing = "e")
public void afterThrowing2(Throwable e){
System.out.println("afterThrowing..."+e);
}

设置AfterReturning通知方法了,我们要怎么捕捉错误呢?错误哪里来?

所以我们先去new 一个错误,写BUG我在行,这就来写一个。

StudentDaoImpl实现类中,在update方法的返回前,添加if (true) throw new NullPointerException();,表示抛出一个空指针错误。这回错误有了,运行测试一下。

1
2
3
4
5
6
@Override
public String update(int num,String name) {
System.out.println("update...num:"+num+",name:"+name);
if (true) throw new NullPointerException();
return "https://blog.hikki";
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
update...num:2,name:小码同学
afterThrowing...java.lang.NullPointerException
Exception in thread "main" java.lang.NullPointerException
at site.hikki.dao.impl.StudentDaoImpl.update(StudentDaoImpl.java:15)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344)
at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:198)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.aop.aspectj.AspectJAfterThrowingAdvice.invoke(AspectJAfterThrowingAdvice.java:62)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:95)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212)
at com.sun.proxy.$Proxy25.update(Unknown Source)
at site.hikki.App.main(App.java:11)

进程已结束,退出代码1

抛出异常后通知可以获取切入点方法中出现的异常信息,使用形参可以接收对应的异常对象。

设置环绕通知(@Around)

使用Around可以同样捕捉异常数据,我们之前使用throws Throwable抛出异常的,现在使用try{}catch{}捕捉异常。

1
2
3
4
5
6
7
8
9
10
@Around("pt2()")
public Object around4(ProceedingJoinPoint pjp) {
Object ret = null;
try {
ret = pjp.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
return ret;
}

抛出异常后通知可以获取切入点方法运行的异常信息,使用形参可以接收运行时抛出的异常对象。

总结

获取切入点方法的参数

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

  • ProceedJointPoint:适用于环绕通知

获取切入点方法返回值

  • 返回后通知

  • 环绕通知

获取切入点方法运行异常信息

  • 抛出异常后通知

  • 环绕通知

AOP案例二:百度云盘密码数据兼容处理

需求分析

需求:对百度网盘分享链接输入提取码时尾部多输入的空格做兼容处理。

1
2
3
链接: https://pan.baidu.com/s/13iYjxy02TnBhyhA_0JObKA?pwd=root 
提取码: root
复制这段内容后打开百度网盘手机App,操作更方便哦

分析:

  1. 在业务方法执行前对所有的输入参数进行格式处理,使用trim方法
  2. 使用处理后的参数调用原始方法–环绕通知中存在对原始方法的调用。

代码实现

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
E:.
└─site
└─hikki
App.java #程序入口

├─aop # AOP切面类
DataAdvice.java

├─config # Spring配置类
SpringConfig.java

├─dao # 数据层
CodeDao.java

└─impl
CodeDaoImpl.java

└─service # 业务层
DataStr.java

└─impl
DataStrImpl.java

编写数据层

创建一个CodeDao接口:

1
2
3
4
5
package site.hikki.dao;

public interface CodeDao {
boolean readPassword(String url,String password);
}

CodeDaoImpl实现类:

我们不使用连接数据库的方式来验证提取码,直接将提取码写死,模拟一下业务即可。

记得给该类配置bean对象

1
2
3
4
5
6
7
8
9
10
11
package site.hikki.dao.impl;

import org.springframework.stereotype.Repository;
import site.hikki.dao.CodeDao;
@Repository
public class CodeDaoImpl implements CodeDao {
@Override
public boolean readPassword(String url, String password) {
return password.equals("root");
}
}

编写业务层

接口:

1
2
3
4
5
package site.hikki.service;

public interface DataStr {
boolean URL(String url,String password);
}

实现类:

记得给该类配置bean对象,并且添加CodeDao自动装配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package site.hikki.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import site.hikki.dao.CodeDao;
import site.hikki.service.DataStr;

@Service
public class DataStrImpl implements DataStr {
@Autowired
private CodeDao codeDao;
@Override
public boolean URL(String url,String password) {
return codeDao.readPassword(url,password);
}
}

编写切面类

@Aspect:定义该类为切面类
@Component:定义该类为bean,收Spring容器管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package site.hikki.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;

@Aspect
@Component
public class DataAdvice {
@Pointcut("execution(* site.hikki.service.DataStr.URL(..))")
private void pt(){}

@Around("pt()")
public Object trimString(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();
}
}
return pjp.proceed(args);
}
}

开启AOP注解功能

1
2
3
4
5
6
7
8
9
10
11
package site.hikki.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan("site.hikki")
@EnableAspectJAutoProxy
public class SpringConfig {
}

创建入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package site.hikki;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import site.hikki.config.SpringConfig;
import site.hikki.service.DataStr;

public class App {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
DataStr dataStr = ctx.getBean(DataStr.class);
boolean b = dataStr.URL("https://pan.baidu.com/s/13iYjxy02TnBhyhA_0JObKA?pwd=root"," root ");
System.out.println(b);
}
}

// 运行结果:
// true

可以看出,我们在提取码增加空格,系统也是可以识别到提取码的内容。

做到在不改变原始的方法下,给该方法增加功能。

AOP总结

概念

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

作用

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

核心概念

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

切入点表达式

切入点表达式标准格式

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

如:execution(* site.hikki.service.DataStr.URL(..))

通配符

  • 作用:用于快速描述,范围描述
通配符 备注
* 匹配任意一个名称或符号(常用)
匹配多个连续的任意符号(常用)
+ 匹配子类类型

书写技巧

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

通知类型

注解 名称 通知
@Before 前置通知 通知方法会在目标方法调用之前执行
@After 后置通知 通知方法会在目标方法返回或异常后调用
@Around(重点) 环绕通知 通知方法前会将目标方法封装起来
@AfterReturning 返回后通知 通知方法会在目标方法返回后调用
@AfterThrowing 抛出异常通知 通知方法会在目标方法抛出异常后调用

通知获取数据

获取切入点方法的参数

  • JoinPoint:适用于前置、后置、返回后、抛出异常后通知,设置为方法的第一个形参。
  • ProceedJoinPoint:使用于环绕通知

获取切入点方法返回值

  • 返回后通知
  • 环绕通知

获取切入点方法运行异常信息

  • 抛出异常后通知
  • 环绕通知

Spring事务管理

事务作用:在数据层保障一系列的数据库操作同成功同失败

Spring事务作用:在数据层或业务层保障一系列的数据库操作同成功同失败

接下来我们使用一个模拟银行账户间转账业务案例来讲解一下Spring的事务管理。

需求分析

需求:实现任意两个账户间转账操作
需求微缩:A账户减钱,B账户加钱
分析:
①:数据层提供基础操作,指定账户减钱(outMoney),指定账户加钱(inMoney)
②:业务层提供转账操作(transfer),调用减钱与加钱的操作
③:提供2个账号和操作金额执行转账操作
④:基于Spring整合MyBatis环境搭建上述操作
结果分析:
①:程序正常执行时,账户金额A减B加,没有问题
②:程序出现异常后,转账失败,但是异常之前操作成功,异常之后操作失败,整体业务失败

代码实现

导入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<!--    mariadb数据库-->
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>3.1.0</version>
</dependency>
<!-- 数据库连接池管理器-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.15</version>
</dependency>
<!-- mybatis-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.7</version>
</dependency>
<!-- spring-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>

<!-- spring管理jdbc-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<!-- mybatis整合spring-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.0</version>
</dependency>
<!-- Junit4-->
<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>

新建jdbc.properties数据库连接资源

数据库SQL文件在该章节最后,数据库连接用户名和密码,根据自己的情况调整

1
2
3
4
jdbc.driver=org.mariadb.jdbc.Driver
jdbc.url=jdbc:mariadb://localhost:3307/spring_db?useSSL=false
jdbc.username=root
jdbc.password=root

创建实体类Account

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package site.hikki.entity;

public class Account {
private Integer id;
private String name;
private String money;

@Override
public String toString() {
return "Account{" +
"id=" + id +
", name='" + name + '\'' +
", money='" + money + '\'' +
'}';
}

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;
}

public String getMoney() {
return money;
}

public void setMoney(String money) {
this.money = money;
}
}

创建Dao层

创建Dao层,对账户余额进行修改。

1
2
3
4
5
6
7
8
9
10
11
12
package site.hikki.dao;

import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;

public interface AccountDao {
@Update("update tb_account set money = money - #{money} where name =#{outName}")
int outMoney(@Param("outName") String outName, @Param("money") Double money);

@Update("update tb_account set money = money + #{money} where name =#{inName}")
int inMoney(@Param("inName") String inName, @Param("money") Double money);
}

创建service层

AccountService接口:

  • Spring注解式事务通常添加在业务层接口中而不会添加到业务层实现类中,降低耦合

  • 注解式事务可以添加到业务方法上表示当前方法开启事务,也可以添加到接口上表示当前接口所有方法开启事务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package site.hikki.service;

import org.springframework.transaction.annotation.Transactional;

public interface AccountService {
/**
* 转账操作
* @param out 传出方
* @param in 转入方
* @param money 金额
*/
//配置当前接口方法具有事务
@Transactional
public void transfer(String out,String in ,Double money) ;
}

AccountServiceImpl实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package site.hikki.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import site.hikki.dao.AccountDao;
import site.hikki.service.AccountService;

@Service
public class AccountServiceImpl implements AccountService {
@Autowired //自动注入
private AccountDao accountDao;
@Override
public void transfer(String out,String in,Double money){
accountDao.outMoney(out,money);
int i = 1/0; //主动制造错误
accountDao.inMoney(in,money);
}
}

创建MybatisConfig第三方Bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package site.hikki.config;

import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.context.annotation.Bean;
import javax.sql.DataSource;

public class MybatisConfig {

@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource){
SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
ssfb.setTypeAliasesPackage("site.hikki.entity"); // 对包进行别名
ssfb.setDataSource(dataSource);
return ssfb;
}

@Bean
public MapperScannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer msc = new MapperScannerConfigurer();
msc.setBasePackage("site.hikki.dao"); // 扫描mapper包
return msc;
}
}

创建JdbcConfig第三方Bean

在JdbcConfig配置中,我们比往常要多增加一个方法transactionManager,该方法表示设置Spring事务管理器,其中,我们需要给Spring设置事务技术,由于Mybatis框架使用的是JDBC事务,我们在该方法中,添加一个DataSource即可,Spring会自动注入,我们不需要操心后边。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package site.hikki.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
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管理
@Bean
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(username);
ds.setPassword(password);
return ds;
}

//配置事务管理器,mybatis使用的是jdbc事务
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
}

开启注解事务驱动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package site.hikki.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.PropertySource;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration //设置该类是配置类
@ComponentScan("site.hikki.service") //扫描bean范围
@PropertySource("classpath:jdbc.properties") //导入properties文件
@Import({JdbcConfig.class, MybatisConfig.class}) // 导入第三方的bean
@EnableTransactionManagement //开启注解式事务驱动
public class SpringConfig {
}

创建测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import site.hikki.config.SpringConfig;
import site.hikki.service.AccountService;

import java.io.IOException;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTest {

@Autowired
private AccountService accountService;

@Test
public void testTransfer() throws IOException {
accountService.transfer("小码同学","吴小白",100D);
}
}

测试结果

1
2
3
4
5
6
7
java.lang.ArithmeticException: / by zero
at site.hikki.service.impl.AccountServiceImpl.transfer(AccountServiceImpl.java:15)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
......省略后面结果

从结果可以看到,报错了,报错是肯定的,因为我们在AccountServiceImpl实现类主动制造了错误,添加了int i = 1/0;,按照正常逻辑,在该语句前面的方法已经是执行了的,该语句后面的方法则没有被执行。

用户的初始值都是1000,在执行后,该余额仍然是没有改变,符合我们的要求,在没有转账中失败了,两个用户的余额都应该是没有改变的。

04-事务管理-数据库-20230116-094

事务管理的原理是什么?

事务角色

事务管理分两个角色:事务管理员和事务协调员。

  • 事务管理员:发起事务方,在Spring中通常指代业务层开启事务的方法

  • 事务协调员:加入事务方,在Spring中通常指代数据层方法,也可以是业务层方法

原本接口中的两个方法都是独立的事务,每一次的调用都是一个独立事务。

但Spring事务中,使用@Transactional注解表示,在该方法中,调用到其他的全部方法,都统一加入到一个新的事务中,即该方法中,只有一个事务,如果该事务执行的过程中发生异常,则该方法的全部执行效果都不起作用。

05-事务管理配置

事务管理配置

@Transactional注解中,可以添加其他配置项(Ctrl + 左键),可以配置其他功能。

属性 作用 示例
readOnly 设置是否为只读事务 readOnly=true 只读事务
timeout 设置事务超时时间 timeout = -1(永不超时)
rollbackFor 设置事务回滚异常(class) rollbackFor = {NullPointException.class}
rollbackForClassName 设置事务回滚异常(String) 同上格式为字符串
noRollbackFor 设置事务不回滚异常(class) noRollbackFor = {NullPointException.class}
noRollbackForClassName 设置事务不回滚异常(String) 同上格式为字符串
propagation 设置事务传播行为 ……

说明:对于RuntimeException类型异常或者Error错误,Spring事务能够进行回滚操作。但是对于编译器异常,Spring事务是不进行回滚的,所以需要使用rollbackFor来设置要回滚的异常。

转账业务追加日志

需求分析

需求:实现任意两个账户间转账操作,并对每次转账操作在数据库进行留痕
需求微缩:A账户减钱,B账户加钱,数据库记录日志
分析:
①:基于转账操作案例添加日志模块,实现数据库中记录日志
②:业务层转账操作(transfer),调用减钱、加钱与记录日志功能
实现效果预期:

  • 无论转账操作是否成功,均进行转账操作的日志留痕

存在的问题:

  • 日志的记录与转账操作隶属同一个事务,同成功同失败

实现效果预期改进:

  • 无论转账操作是否成功,日志必须保留

事务传播行为

我们既然可以将多个事务统一加入到某个事务中,那么也可以决定某个事务不要加入到统一的事务中。

06-事务管理配置

代码实现

新建Dao层

将日志写入到数据库。

1
2
3
4
5
6
7
8
9
10
package site.hikki.dao;

import org.apache.ibatis.annotations.Insert;
import org.springframework.stereotype.Repository;

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

新建LogService接口

我们一般将@Transactional写在接口,而不是写在实现类中。

1
2
3
4
5
6
7
package site.hikki.service;

public interface LogService {
//propagation设置事务属性:传播行为设置为当前操作需要新事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
void log(String out,String in,Double money);
}

新建LogServiceImpl实现类

在创建log方法时,需要添加 @Transactional注解,并且属性和值分别添加propagationPropagation.REQUIRES_NEW,表示该事务独立新建一个事务,不统一添加到被调用的事务中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package site.hikki.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import site.hikki.dao.LogDao;
import site.hikki.service.LogService;

@Service
public class LogServiceImpl implements LogService {
@Autowired
private LogDao logDao;

@Override
public void log(String out, String in, Double money) {
logDao.log("转账操作由"+out+"到"+in+",金额:"+money);
}
}

修改AccountServiceImpl实现类

使用try{}finally{}括起来,保证log方法无论如何都会被执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package site.hikki.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import site.hikki.dao.AccountDao;
import site.hikki.service.AccountService;
import site.hikki.service.LogService;

@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Autowired
private LogService logService;
@Override
public void transfer(String out,String in,Double money){
try {
accountDao.outMoney(out,money);
int i = 1/0; //主动制造错误
accountDao.inMoney(in,money);
}finally {
logService.log(out,in,money);
}
}
}

测试方法后,即使出现错误,也可以将转账记录写入到数据中,这对于日志的存储非常有帮助。