前言

本文概括

  • 理解并掌握AOP相关概念
  • 能够说出AOP的工作流程
  • 能运用AOP相关知识完成对应的案例编写
  • 重点掌握Spring的声明式事务管理

SSM学习目录

  1. Spring(一)-SSM框架(了解即可(๑・̀ㅂ・́)و✧ ) 点击我查看
  2. Spring(二)-SSM框架(了解即可ヾ (≧▽≦*) o) 点击我查看
  3. Spring(三)-SSM框架(了解即可(^▽^)) 点击我查看(当前位置)
  4. SpringMVC-SSM框架(~(〃∀`)ノ) 点击我查看

    AOP

    AOP(Aspect Oriented Programming),即面向切面编程,是OOP的补充,OOP是把对象作为程序的主要构建模块,而AOP是把切面作为程序的主要构建模块。AOP能在不改动原始代码的情况下,对程序进行功能增强。

AOP相关概念

  • 连接点(Joinpoint):程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等。
  • 切入点(Pointcut):匹配连接点的式子,粒度为类、方法、异常、变量等。
  • 通知(Advice):增强的逻辑代码,粒度为方法、异常、变量等。
  • 切面(Aspect):切入点和通知的结合。
  • 引入(Introduction):在不修改代码的前提下,给类添加新的属性和方法。
  • 目标对象(Target):被代理的对象。
  • 代理对象(Proxy):代理对象。
  • 织入(Weaving):把切面应用到目标对象的过程。
  • 切面的执行顺序:先执行前置通知,再执行原始方法,最后执行后置通知。

AOP了解

为了能让我们更加深入的了解AOP,我们来通过具体的事例代码来学习.
需求为:使用SpringAOP的注解方式完成在方法执行的前打印出当前系统时间

环境准备

点击查看代码和思路分析

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

1.pom.xml添加Spring依赖

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
</dependencies>

2.添加BookDao和BookDaoImpl类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  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 ...");
}
}

3.创建Spring的配置类

1
2
3
4
@Configuration
@ComponentScan("com.itheima")
public class SpringConfig {
}

4.编写App运行类

1
2
3
4
5
6
7
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
bookDao.save();
}
}

AOP实现步骤

步骤1:添加依赖

1
2
3
4
5
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>

步骤2:定义接口与实现类
见环境准备下面的查看代码

步骤3:定义通知类和通知
通知就是将共性功能抽取出来后形成的方法,共性功能指的就是当前系统时间的打印。

1
2
3
4
5
public class MyAdvice {
public void method(){
System.out.println(System.currentTimeMillis());
}
}

步骤4:定义切入点
BookDaoImpl中有两个方法,分别是save和update,我们要增强的是update方法,该如何定义呢?

1
2
3
4
5
6
7
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
public void method(){
System.out.println(System.currentTimeMillis());
}
}

说明:

  • 切入点定义依托一个不具有实际意义的方法进行,即无参数、无返回值、方法体无实际逻辑。
  • execution及后面编写的内容,后面会有章节专门去学习。

步骤5:设计切面

1
2
3
4
5
6
7
8
9
10
public class MyAdvice {
//定义切入点
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
//前置通知,绑定pt() 切入点
@Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());
}
}

步骤6:将通知类配给容器并标识其为切面类

1
2
3
4
5
6
7
8
9
10
11
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}

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

步骤7:开启注解格式AOP功能

1
2
3
4
5
@Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy
public class SpringConfig {
}

步骤8:运行程序

1
2
3
4
5
6
7
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
bookDao.update();
}
}

知识点1:@EnableAspectJAutoProxy

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

知识点2:@Aspect

名称 @Aspect
类型 类注解
位置 切面类定义上方
作用 设置当前类为AOP切面类

知识点3:@Pointcut

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

知识点4:@Before

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

AOP工作流程与核心概念

这一节我们主要讲解两个知识点:AOP工作流程AOP核心概念。其中核心概念是对前面了解的补充。

AOP工作流程

由于AOP是基于Spring容器管理的bean做的增强,所以整个工作过程需要从Spring加载bean说起:
流程1:Spring容器启动
流程2:读取所有切面配置中的切入点
流程3:初始化bean,判定bean对应的类中的方法是否匹配到任意切入点
流程4:获取bean执行方法

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

AOP核心概念

在上面介绍AOP的工作流程中,我们提到了两个核心概念,分别是:

  • 目标对象(Target):原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的
  • 代理(Proxy):目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现
    上面这两个概念比较抽象,简单来说,

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

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

AOP配置管理

AOP切入点表达式

对于AOP中切入点表达式,我们总共会学习三个内容,分别是语法格式通配符书写技巧

语法格式

切入点表达式是用来匹配连接点的式子,其格式如下:

1
2
3
execution([权限修饰符][返回类型][类全路径][方法名称]([参数列表]))
//例如
execution(public User com.itheima.service.UserService.findById(int))

  • 权限修饰符:可以省略
  • 返回类型:可以省略

通配符

通配符是用来匹配连接点的符号,含义如下:

:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现
..:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写,也可以作为前缀或者后缀的匹配符出现
+:专用于匹配子类类型(这个使用很少)[
Service+,表示所有以Service结尾的接口的子类。]

相关例子如下

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
execution(void com.itheima.dao.BookDao.update())
匹配接口,能匹配到
execution(void com.itheima.dao.impl.BookDaoImpl.update())
匹配实现类,能匹配到
execution(* com.itheima.dao.impl.BookDaoImpl.update())
返回值任意,能匹配到
execution(* com.itheima.dao.impl.BookDaoImpl.update(*))
返回值任意,但是update方法必须要有一个参数,无法匹配,要想匹配需要在update接口和实现类添加参数
execution(void com.*.*.*.*.update())
返回值为void,com包下的任意包三层包下的任意类的update方法,匹配到的是实现类,能匹配
execution(void com.*.*.*.update())
返回值为void,com包下的任意两层包下的任意类的update方法,匹配到的是接口,能匹配
execution(void *..update())
返回值为void,方法名是update的任意包下的任意类,能匹配
execution(* *..*(..))
匹配项目中任意类的任意方法,能匹配,但是不建议使用这种方式,影响范围广
execution(* *..u*(..))
匹配项目中任意包任意类下只要以u开头的方法,update方法能满足,能匹配
execution(* *..*e(..))
匹配项目中任意包任意类下只要以e结尾的方法,update和save方法能满足,能匹配
execution(void com..*())
返回值为void,com包下的任意包任意类任意方法,能匹配,*代表的是方法
execution(* com.itheima.*.*Service.find*(..))
将项目中所有业务层方法的以find开头的方法匹配
execution(* com.itheima.*.*Service.save*(..))
将项目中所有业务层方法的以save开头的方法匹配

书写技巧

对于切入点表达式的编写其实是很灵活的,那么在编写的时候,有没有什么好的技巧让我们用用:

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

AOP通知类型

在AOP中,通知类型分为以下几种:

  • 前置通知(@Before):在原始方法执行前运行
  • 后置通知(@After):在原始方法执行后运行
  • 环绕通知(@Around)(重点):在原始方法前后运行
  • 返回通知(@AfterReturning)(了解):在原始方法返回后运行
  • 异常通知(@AfterThrowing)(了解):在原始方法抛出异常后运行

环境准备

4.2.2 环境准备

点击查看代码

pom.xml添加Spring依赖

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

添加BookDao和BookDaoImpl类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  public interface BookDao {
public void update();
public int select();
}

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

创建Spring的配置类

1
2
3
4
5
  @Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy
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
  @Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.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运行类

1
2
3
4
5
6
7
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
bookDao.update();
}
}

前置通知(@Before)

修改MyAdvice,在before方法上添加@Before注解

1
2
3
4
5
6
7
8
9
10
11
12
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}

@Before("pt()")
//此处也可以写成 @Before("MyAdvice.pt()"),不建议
public void before() {
System.out.println("before advice ...");
}
}

后置通知(@After)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}

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

环绕通知(@Around)

环绕通知是最强大的通知类型,它可以在原始方法前后运行,并且可以控制原始方法是否运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}

@Pointcut("execution(int com.itheima.dao.BookDao.select())")
private void pt2(){}

@Around("pt2()")
public Object aroundSelect(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice ...");
//表示对原始操作的调用
Object ret = pjp.proceed();
System.out.println("around after advice ...");
return ret;
}
}

注意:返回值为Object类型,表示原始操作的返回值,返回值类型不对的话,程序会报错.

返回通知(@AfterReturning)

返回通知是在原始方法返回后运行,并且可以获取原始方法的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}

@Pointcut("execution(int com.itheima.dao.BookDao.select())")
private void pt2(){}

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

注意:返回后通知是需要在原始方法select正常执行后才会被执行,如果select()方法执行的过程中出现了异常,那么返回后通知是不会被执行.

异常通知(@AfterThrowing)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}

@Pointcut("execution(int com.itheima.dao.BookDao.select())")
private void pt2(){}

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

注意:异常后通知是需要原始方法抛出异常,可以在select()方法中添加一行代码int i = 1/0即可。如果没有抛异常,异常后通知将不会被执行.

AOP通知类型注解知识点

知识点1:@After

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

知识点2:@AfterReturning

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

知识点3:@AfterThrowing

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

知识点4:@Around

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

AOP获取通知数据

目前我们写AOP仅仅是在原始方法前后追加一些操作,接下来我们要说说AOP中数据相关的内容,我们将从获取参数获取返回值获取异常三个方面来研究切入点的相关信息。

环境准备

点击查看代码

pom.xml添加Spring依赖

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

添加BookDao和BookDaoImpl类

1
2
3
4
5
6
7
8
9
10
11
  public interface BookDao {
public String findName(int id);
}
@Repository
public class BookDaoImpl implements BookDao {

public String findName(int id) {
System.out.println("id:"+id);
return "itcast";
}
}

创建Spring的配置类

1
2
3
4
5
  @Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy
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
  @Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.itheima.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() 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 ...");
}
}

编写App运行类

1
2
3
4
5
6
7
8
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);
}
}

获取参数

非环绕通知获取方式
在方法上添加JoinPoint,通过JoinPoint来获取参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
private void pt(){}

@Before("pt()")
public void before(JoinPoint jp)
Object[] args = jp.getArgs();
System.out.println(Arrays.toString(args));
System.out.println("before advice ..." );
}
//...其他的略

环绕通知获取方式
环绕通知使用的是ProceedingJoinPoint,因为ProceedingJoinPoint是JoinPoint类的子类,所以对于ProceedingJoinPoint类中应该也会有对应的getArgs()方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
private void pt(){}

@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;
}
//其他的略
}

注意:

pjp.proceed()方法是有两个构造方法,分别是: pjp.proceed()pjp.proceed(Object[] args)
第一个构造方法表示执行原始方法,当原始方法有参数,会在调用的过程中自动传入参数,第二个构造方法表示执行原始方法时传入指定的参数.
对于环绕通知来说,我们可以通过第二个构造方法来修改原始方法的参数.

获取返回值

对于返回值,只有返回后AfterReturing和环绕Around这两个通知类型可以获取.
环绕通知(@Around)获取返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
private void pt(){}

@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;
}
//其他的略
}

上述代码中,ret就是方法的返回值,我们是可以直接获取,不但可以获取,如果需要还可以进行修改。

返回后通知(@AfterReturning)获取返回值

1
2
3
4
5
6
7
8
9
10
11
12
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
private void pt(){}

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

获取异常

对于获取抛出的异常,只有抛出异常后AfterThrowing和环绕Around这两个通知类型可以获取.
环绕通知(@Around)获取异常
这块比较简单,以前我们是抛出异常,现在只需要将异常捕获,就可以获取到原始方法的异常信息了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
private void pt(){}

@Around("pt()")
public Object around(ProceedingJoinPoint pjp){
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
args[0] = 666;
Object ret = null;
try{
ret = pjp.proceed(args);
}catch(Throwable throwable){
t.printStackTrace();
}
return ret;
}
//其他的略
}

在catch方法中就可以获取到异常,至于获取到异常以后该如何处理,这个就和你的业务需求有关了。

抛出异常后通知(@AfterThrowing)获取异常

1
2
3
4
5
6
7
8
9
10
11
12
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
private void pt(){}

@AfterThrowing(value = "pt()",throwing = "t")
public void afterThrowing(Throwable t) {
System.out.println("afterThrowing advice ..."+t);
}
//其他的略
}

至此,AOP通知如何获取数据就已经讲解完了,数据中包含参数返回值异常(了解)

AOP总结

AOP的知识就已经讲解完了,接下来对于AOP的知识进行一个总结:
AOP的核心概念

  • 概念:AOP(Aspect Oriented Programming)面向切面编程,一种编程范式
  • 作用:在不惊动原始设计的基础上为方法进行功能==增强==
  • 核心概念
    • 代理(Proxy):SpringAOP的核心本质是采用代理模式实现的
    • 连接点(JoinPoint):在SpringAOP中,理解为任意方法的执行
    • 切入点(Pointcut):匹配连接点的式子,也是具有共性功能的方法描述
    • 通知(Advice):若干个方法的共性功能,在切入点处执行,最终体现为一个方法
    • 切面(Aspect):描述通知与切入点的对应关系
    • 目标对象(Target):被代理的原始对象成为目标对象

切入点表达式

  • 切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数)异常名)

    1
    execution(* com.itheima.service.*Service.*(..))
  • 切入点表达式描述通配符:

    • 作用:用于快速描述,范围描述
    • *:匹配任意符号(常用)
    • .. :匹配多个连续的任意符号(常用)
    • +:匹配子类类型
  • 切入点表达式书写技巧

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

五种通知类型

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

通知中获取参数

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

AOP事务管理

Spring事务简介

事务是数据库操作的最小工作单元,是一组逻辑操作单元,使数据从一种状态变换到另一种状态。
事务作用:在数据层保障一系列的数据库操作同成功同失败
事务特征:

  1. 原子性:事务是数据库的逻辑工作单位,事务中包含的各操作要么都做,要么都不做
  2. 一致性:事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态
  3. 隔离性:多个事务之间不能互相干扰,一个事务的运行不能被其他事务干扰

接下来我们用一个具体需求来学习和事务相关的知识。

环境准备

实现任意两个账户间转账操作:A账户减钱,B账户加钱

点击查看代码

步骤1:准备数据库表和数据

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

步骤2:创建项目导入jar包

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

步骤3:根据表创建模型类

1
2
3
4
5
6
7
public class Account implements Serializable {

private Integer id;
private String name;
private Double money;
//setter...getter...toString...方法略
}

步骤4:创建Dao接口

1
2
3
4
5
6
7
8
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);
}

步骤5:创建Service接口和实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public interface AccountService {
/**
* 转账操作
* @param out 传出方
* @param in 转入方
* @param money 金额
*/
public void transfer(String out,String in ,Double money) ;
}

@Service
public class AccountServiceImpl implements AccountService {

@Autowired
private AccountDao accountDao;

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

}

步骤6:添加jdbc.properties文件

1
2
3
4
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_db?useSSL=false
jdbc.username=root
jdbc.password=root

步骤7:创建JdbcConfig配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;
}
}

步骤8:创建MybatisConfig配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MybatisConfig {

@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
ssfb.setTypeAliasesPackage("com.itheima.domain");
ssfb.setDataSource(dataSource);
return ssfb;
}

@Bean
public MapperScannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer msc = new MapperScannerConfigurer();
msc.setBasePackage("com.itheima.dao");
return msc;
}
}

步骤9:创建SpringConfig配置类

1
2
3
4
5
6
@Configuration
@ComponentScan("com.itheima")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
public class SpringConfig {
}

步骤10:编写测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
@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);
}

}

Spring事务管理

上述环境,运行单元测试类,会执行转账操作,Tom的账户会减少100,Jerry的账户会加100。
这是正常情况下的运行结果,但是如果在转账的过程中出现了异常,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class AccountServiceImpl implements AccountService {

@Autowired
private AccountDao accountDao;

public void transfer(String out,String in ,Double money) {
accountDao.outMoney(out,money);
int i = 1/0;
accountDao.inMoney(in,money);
}

}

这个时候就模拟了转账过程中出现异常的情况,正确的操作应该是转账出问题了,Tom应该还是900,Jerry应该还是1100,但是真正运行后会发现,并没有像我们想象的那样,Tom账户为800而Jerry还是1100,100块钱凭空消息了,银行乐疯了。如果把转账换个顺序,银行就该哭了。
不管哪种情况,都是不允许出现的,所以需要解决这个问题,这就需要用到事务管理。
事务管理的具体步骤为

步骤1:在需要被事务管理的方法上添加注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public interface AccountService {
/**
* 转账操作
* @param out 传出方
* @param in 转入方
* @param money 金额
*/
//配置当前接口方法具有事务
public void transfer(String out,String in ,Double money) ;
}

@Service
public class AccountServiceImpl implements AccountService {

@Autowired
private AccountDao accountDao;
@Transactional
public void transfer(String out,String in ,Double money) {
accountDao.outMoney(out,money);
int i = 1/0;
accountDao.inMoney(in,money);
}

}

注意:
@Transactional可以写在接口类上、接口方法上、实现类上和实现类方法上

  • 写在接口类上,该接口的所有实现类的所有方法都会有事务
  • 写在接口方法上,该接口的所有实现类的该方法都会有事务
  • 写在实现类上,该类中的所有方法都会有事务
  • 写在实现类方法上,该方法上有事务

步骤2:在JdbcConfig类中配置事务管理器

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
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使用的是jdbc事务
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
}

注意:事务管理器要根据使用技术进行选择,Mybatis框架使用的是JDBC事务,可以直接使用DataSourceTransactionManager

步骤3:开启事务注解
在SpringConfig的配置类中开启

1
2
3
4
5
6
7
8
@Configuration
@ComponentScan("com.itheima")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
//开启注解式事务驱动
@EnableTransactionManagement
public class SpringConfig {
}

步骤4:运行测试类
会发现在转换的业务出现错误后,事务就可以控制回顾,保证数据的正确性。

事务管理注解知识点小结

知识点1:@EnableTransactionManagement

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

知识点2:@Transactional

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

Spring事务角色

这节中我们重点要理解两个概念,分别是事务管理员事务协调员
事务管理员:发起事务方,在Spring中通常指代业务层开启事务的方法.
事务协调员:加入事务方,在Spring中通常指代数据层方法,也可以是业务层方法.

Spring事务属性

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

上面这些属性都可以在@Transactional注解的参数上进行设置。

  • readOnly:true只读事务,false读写事务,增删改要设为false,查询设为true。

  • timeout:设置超时时间单位秒,在多长时间之内事务没有提交成功就自动回滚,-1表示不设置超时时间。

  • rollbackFor:当出现指定异常进行事务回滚

  • noRollbackFor:当出现指定异常不进行事务回滚

    • 思考:出现异常事务会自动回滚,这个是我们之前就已经知道的

    • noRollbackFor是设定对于指定的异常不回滚,这个好理解

    • rollbackFor是指定回滚异常,对于异常事务不应该都回滚么,为什么还要指定?

      • 这块需要更正一个知识点,并不是所有的异常都会回滚事务,比如下面的代码就不会回滚
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
public interface AccountService {
/**
* 转账操作
* @param out 传出方
* @param in 转入方
* @param money 金额
*/
//配置当前接口方法具有事务
public void transfer(String out,String in ,Double money) throws IOException;
}

@Service
public class AccountServiceImpl implements AccountService {

@Autowired
private AccountDao accountDao;
@Transactional
public void transfer(String out,String in ,Double money) throws IOException{
accountDao.outMoney(out,money);
//int i = 1/0; //这个异常事务会回滚
if(true){
throw new IOException(); //这个异常事务就不会回滚
}
accountDao.inMoney(in,money);
}

}
  • 出现这个问题的原因是,Spring的事务只会对Error异常RuntimeException异常及其子类进行事务回顾,其他的异常类型是不会回滚的,对应IOException不符合上述条件所以不回滚
  • 此时就可以使用rollbackFor属性来设置出现IOException异常不回滚
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class AccountServiceImpl implements AccountService {

@Autowired
private AccountDao accountDao;
@Transactional(rollbackFor = {IOException.class})
public void transfer(String out,String in ,Double money) throws IOException{
accountDao.outMoney(out,money);
//int i = 1/0; //这个异常事务会回滚
if(true){
throw new IOException(); //这个异常事务就不会回滚
}
accountDao.inMoney(in,money);
}

}
  • rollbackForClassName等同于rollbackFor,只不过属性为异常的类全名字符串

  • noRollbackForClassName等同于noRollbackFor,只不过属性为异常的类全名字符串

  • isolation设置事务的隔离级别

    • DEFAULT :默认隔离级别, 会采用数据库的隔离级别
    • READ_UNCOMMITTED : 读未提交
    • READ_COMMITTED : 读已提交
    • REPEATABLE_READ : 重复读取
    • SERIALIZABLE: 串行化

事务传播行为

事务传播行为的概念

事务传播行为是指在一个事务方法被另一个事务方法调用时,这个事务应该如何进行。
例如,一个事务方法A调用了另一个事务方法B,如果B方法抛出异常,A方法应该如何处理?
事务传播行为就是用来解决这个问题的。
例如:我们在记录操作日志的时候,在转账需求时我们想要的是无论成功还是失败都要记录一份日志,这个时候我们就需要用到事务的传播行为
修改logService改变事务的传播行为

1
2
3
4
5
6
7
8
9
10
11
@Service
public class LogServiceImpl implements LogService {

@Autowired
private LogDao logDao;
//propagation设置事务属性:传播行为设置为当前操作需要新事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void log(String out,String in,Double money ) {
logDao.log("转账操作由"+out+"到"+in+",金额:"+money);
}
}

运行后,就能实现我们想要的结果,不管转账是否成功,都会记录日志

事务传播行为的可选值

传播属性 事务管理员 事务协调员
REQUIRED(默认) 开启T
加入T
新建T2
REQUIRES_NEW 开启T
新建T2
新建T2
SUPPORTS 开启T
加入T
NOT_SUPPORTED 开启T

MANDATORY 开启T
加入T
ERROR
NEVER 开启T
ERROR
NESTED 设置savePoint,一旦事务回滚,事务将回滚到savePoint处,交由客户响应提交/回滚