avatar


16.Spring Framework [2/2]

本章主要讨论:AOP(Aspect Oriented Programming,面向切面编程)。

上一章《15.Spring Framework [1/2]》主要讨论:

  1. IoC(Inverse Of Control,控制反转)
  2. DI(Dependency Injection,依赖注入)

概述

定义

AOP,Aspect Oriented Programming,面向切面编程,是一种编程范式。其特点是,在不改动原始代码的基础上为其进行功能增强。

(与之类似的,还有OOP,Object Oriented Programming,面向对象编程,也是一种编程范式。)

例子

我们通过一个例子,来看看到底什么是"在不改动原始代码的基础上为其进行功能增强"。
完整的代码,我分享在GitHub上:
https://github.com/KakaWanYifan/spring-aop-demo

BookDao的实现类:

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
package com.kakawanyifan.dao.impl;

import com.kakawanyifan.dao.BookDao;
import org.springframework.stereotype.Repository;

@Repository
public class BookDaoImpl implements BookDao {

public void save() {
for (int i = 0; i < 3; i++) {
System.out.println("book dao save ...");
}
}

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 ...");
}
}

根据上述代码可知,我们执行save()方法,会打印三行,执行其他方法,只会打印一行。
再来看看实际的执行,示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.kakawanyifan;

import com.kakawanyifan.config.SpringConfig;
import com.kakawanyifan.dao.BookDao;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

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

运行结果:

1
2
3
4
5
6
7
book dao save ...
book dao save ...
book dao save ...
book dao select ...
book dao select ...
book dao select ...
book dao update ...

save()方法打印了三行,这个没有问题;但是select()方法也打印了三行;update()方法又只打印了一行。

而这一切,主要是由如下的代码实现的。

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
package com.kakawanyifan.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(* com.kakawanyifan.dao.BookDao.select())")
private void pt(){}

@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
for (int i = 0 ; i<3 ; i++) {
//调用原始操作
pjp.proceed();
}
return null;
}

}

这就是AOP,在不改动原始代码的前提下,即无侵入式,进行功能增强。

开始

环境准备

假设,现在存在一个Spring项目,如下:

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.kakawanyifan</groupId>
<artifactId>s_aop</artifactId>
<version>1.0-SNAPSHOT</version>

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

</project>

SpringConfig.java

1
2
3
4
5
6
7
8
9
10
package com.kakawanyifan;

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

@Configuration
@ComponentScan("com.kakawanyifan")
public class SpringConfig {

}

BookDao.java

1
2
3
4
5
6
7
8
package com.kakawanyifan.dao;

public interface BookDao {
public void save();
public void delete();
public void update();
public void select();
}

BookDaoImpl.java

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
package com.kakawanyifan.dao.impl;

import com.kakawanyifan.dao.BookDao;
import org.springframework.stereotype.Repository;

import java.time.Instant;

@Repository
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println(Instant.now());
System.out.println("save");
}

public void delete() {
System.out.println("delete");
}

public void update() {
System.out.println("update");
}

public void select() {
System.out.println("select");
}

}

App.java

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.kakawanyifan;

import com.kakawanyifan.dao.BookDao;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

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

运行结果:

1
2
2022-10-26T10:07:59.532Z
save

在上文中,先打印当前时间,然后打印save
现在,我们来对其他方法进行增强。

添加依赖

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

解释说明:

  • 在上一步导入的spring-context中,已经包含了spring-aop,所以不需要再单独导入spring-aop
  • aspectjweaver是AspectJ的jar包。虽然Spring也有AOP实现,但是相比于AspectJ来说比较麻烦,在本文中,采用Spring整合ApsectJ的方式进行AOP开发。

定义通知类和通知

通知是将共性功能抽取出来后形成的方法。
在我们的这个案例中,共性功能指的就是打印当前时间。

示例代码:

1
2
3
4
5
6
7
8
9
10
package com.kakawanyifan;

import java.time.Instant;

public class MyAdvice {
public void Method(){
System.out.println(Instant.now());
}

}
  • 类名和方法名没有要求,可以任意。

定义切入点

定义切入点依赖一个不具有实际意义(无参数、无返回值、方法体无实际逻辑)的方法。

在这里我们以增强BookDaoImpl中的delete方法为例,示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.kakawanyifan;

import org.aspectj.lang.annotation.Pointcut;

import java.time.Instant;

public class MyAdvice {

@Pointcut("execution(void com.kakawanyifan.dao.BookDao.delete())")
public void pt(){}

public void Method(){
System.out.println(Instant.now());
}

}
  • pt()方法就是切入点。
  • @Pointcut注解,我们会在下文专门讨论。

制作切面

切面是用来描述通知和切入点之间的关系。

根据通知类型,制作切面的注解也很多。在这个案例中,是先打印时间,制作切面的方法为在通知方法上加上注解@Before("pt()")pt()为切入点。

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

import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

import java.time.Instant;

public class MyAdvice {

@Pointcut("execution(void com.kakawanyifan.dao.BookDao.delete())")
public void pt(){}

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

}

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

在通知类上增加@Component@Aspect两个注解。

示例代码:

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

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

import java.time.Instant;

@Component
@Aspect
public class MyAdvice {

@Pointcut("execution(void com.kakawanyifan.dao.BookDao.delete())")
public void pt(){}

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

}

开启AOP功能

在Spring的配置类上新增注解@EnableAspectJAutoProxy

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
package com.kakawanyifan;

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

@Configuration
@ComponentScan("com.kakawanyifan")
@EnableAspectJAutoProxy
public class SpringConfig {

}

运行程序

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.kakawanyifan;

import com.kakawanyifan.dao.BookDao;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

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

运行结果:

1
2
2022-10-27T00:01:29.905Z
delete

至此,我们通过AOP完成了对原始方法的功能增强。

机制

过程

我们讨论Spring的工作过程。

一、Spring容器启动,加载Bean

包括需要被增强的类(如:BookDaoImpl)、通知类(如:MyAdvice)。
但在这一步,Bean对象还没有被创建。

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

注意,是切面配置中的切入点。
例如,在下面的代码中,只有pt()会被读取,pt2nd()不会被读取。

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 com.kakawanyifan;

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

import java.time.Instant;

@Component
@Aspect
public class MyAdvice {

@Pointcut("execution(void com.kakawanyifan.dao.BookDao.delete())")
public void pt(){}

@Pointcut("execution(void com.kakawanyifan.dao.BookDao.update())")
public void pt2nd(){}

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

}

三、初始化Bean

在这一步,会去判断Bean对应的类中的方法是否匹配到切入点。

  • 如果目标对象中的方法会被增强,那么容器中将存入的是目标对象的代理对象
  • 如果目标对象中的方法不被增强,那么容器中将存入的是目标对象本身。

验证

一、方法会增强,获取类的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.kakawanyifan;

import com.kakawanyifan.dao.BookDao;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

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);
System.out.println(bookDao.getClass());
}
}

运行结果:

1
2
com.kakawanyifan.dao.impl.BookDaoImpl@4009e306
class com.sun.proxy.$Proxy19

二、修改MyAdvice类,不增强,获取类型

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

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

import java.time.Instant;

@Component
@Aspect
public class MyAdvice {

@Pointcut("execution(void com.kakawanyifan.dao.BookDao.delete())")
public void pt(){}

// @Before("pt()")
public void Method(){
System.out.println(Instant.now());
}

}

运行结果:

1
2
com.kakawanyifan.dao.impl.BookDaoImpl@60d8c9b7
class com.kakawanyifan.dao.impl.BookDaoImpl
  • 切入点pt()并没有被使用,所以不会被读取。因为不属于切面配置中的切入点,因此不会被增强。
  • 不能直接打印对象,直接打印对象利用的是对象的toString方法,这个不管是不是代理对象打印的结果都是一样的,因为对toString方法进行了重写。

配置

切入点表达式

格式

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

例如:

1
execution(public User com.kakawanyifan.service.UserService.findById(int))

其中:

  • execution:动作关键字,描述切入点的行为动作。
  • public:访问修饰符。可以是public,private等,也可以省略。
  • User:返回类型。
  • com.kakawanyifan.service:包名。
  • UserServic:类名/接口名。
  • findById:方法名。
  • int:参数值类型,多个类型用逗号隔开。

通配符

  • *,单个独立的任意符号
    示例代码:
    1
    2
    ``` Java
    execution(public * com.kakawanyifan.*.UserService.find*(*))
    解释说明:匹配"com.kakawanyifan"包下的,任意包中的,UserService类或接口中,所有find开头的,带有一个参数的,返回值任意的,方法。
  • ..:多个连续的任意符号
    示例代码:
    1
    execution(public User com..UserService.findById(..))
    解释说明:匹配"com"包下的,任意包中的,UserService类或接口中,所有名称为findById的,参数任意,返回值类型是User的,方法。

通知类型

通知类型一共有5种:

  1. 前置通知
  2. 后置通知
  3. 环绕通知
  4. 返回后通知
  5. 抛出异常后通知

前置通知

前置通知,@Before

在上文,我们已经用过了前置通知,这里不做太多讨论。

后置通知

后置通知,@After

示例代码:

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

import org.aspectj.lang.annotation.After;
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 com.kakawanyifan.dao.BookDao.delete())")
public void pt(){}

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

}

环绕通知

环绕通知,@Around

使用方法

@Around,来吧!

示例代码:

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

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 com.kakawanyifan.dao.BookDao.delete())")
public void pt(){}

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

}
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.kakawanyifan;

import com.kakawanyifan.dao.BookDao;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

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

运行结果:

1
2
around before advice ...
around after advice ...

好像不太对。
通知的内容打印出来了,但是原始方法的内容却没有被执行。
因为环绕通知需要在原始方法的前后进行增强,所以环绕通知就必须要能对原始操作进行调用。

使用Aroud环绕通知,需要同时依赖ProceedingJoinPoint及其方法proceed()

示例代码:

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

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 com.kakawanyifan.dao.BookDao.delete())")
public void pt(){}

@Around("pt()")
public void around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice ...");
pjp.proceed();
System.out.println("around after advice ...");
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.kakawanyifan;

import com.kakawanyifan.dao.BookDao;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

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

运行结果:

1
2
3
around before advice ...
delete
around after advice ...

返回值的处理

如果原始方法是有返回值的呢?
例如,我们的select方法是有返回值的。

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
package com.kakawanyifan.dao.impl;

import com.kakawanyifan.dao.BookDao;
import org.springframework.stereotype.Repository;

import java.time.Instant;
import java.util.UUID;

@Repository
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println(Instant.now());
System.out.println("save");
}

public void delete() {
System.out.println("delete");
}

public void update() {
System.out.println("update");
}

public String select() {
System.out.println("select");
return UUID.randomUUID().toString();
}

}

我们用一个变量去接收原始方法的返回值,然后再在通知方法中返回即可。
示例代码:

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 com.kakawanyifan;

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(String com.kakawanyifan.dao.BookDao.select())")
public void pt(){}

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

}
  • 也可以对原始方法返回值进行修改。

返回后通知

返回后通知,@AfterReturning

顾名思义,一定要返回后才会执行。如果被增强的方法出现了异常,没有返回,那么返回后通知是不会被执行。
而我们上文讨论的后置通知,只要是被增强的方法运行结束,不管有没有抛出异常,都会被执行。

示例代码:

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

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

@Component
@Aspect
public class MyAdvice {

@Pointcut("execution(String com.kakawanyifan.dao.BookDao.select())")
public void pt(){}

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

}

异常后通知

异常后通知,@AfterThrowing
故名思义,一定要抛出异常了,才会被执行;如果原始方式不抛出异常,直接try-catch了,也不会被执行。

示例代码:

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

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

@Component
@Aspect
public class MyAdvice {

@Pointcut("execution(String com.kakawanyifan.dao.BookDao.select())")
public void pt(){}

@AfterThrowing("pt()")
public void afterReturning() {
System.out.println("afterThrowing advice ...");
}

}
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 com.kakawanyifan.dao.impl;

import com.kakawanyifan.dao.BookDao;
import org.springframework.stereotype.Repository;

import java.time.Instant;
import java.util.UUID;

@Repository
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println(Instant.now());
System.out.println("save");
}

public void delete() {
System.out.println("delete");
}

public void update() {
System.out.println("update");
}

public String select() {
System.out.println("select");
String s = "abc";
return s.substring(2,4);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.kakawanyifan;

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

@Component
@Aspect
public class MyAdvice {

@Pointcut("execution(String com.kakawanyifan.dao.BookDao.select())")
public void pt(){}

@AfterThrowing("pt()")
public void afterReturning() {
System.out.println("afterThrowing advice ...");
}

}

运行结果:

1
2
3
4
5
6
7
8
9
10
select
afterThrowing advice ...
Exception in thread "main" java.lang.StringIndexOutOfBoundsException: String index out of range: 4
at java.lang.String.substring(String.java:1963)
at com.kakawanyifan.dao.impl.BookDaoImpl.select(BookDaoImpl.java:27)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

【部分运行结果略】

获取数据

获取的数据有3类:

  1. 参数(入参)
  2. 返回值(出参)
  3. 异常

在上文,我们说通知有5类。

这5类通知和3类数据的瓜系如下:

  • 获取参数(入参),所有的通知类型都可以获取参数
    • 前置、后置、返回后和抛出异常后,利用JoinPoint获取参数。
    • 环绕利用ProceedingJoinPoint获取参数。
  • 获取返回值(出参),前置和抛出异常后,没有返回值;后置可有可无;主要关注:返回后、环绕。
  • 获取异常,前置和返回后,不会获取异常,后置可有可无;主要关注:抛出异常后、环绕。

获取参数

JoinPoint

前置、后置、返回后和抛出异常后,利用JoinPoint获取参数。

示例代码:

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 com.kakawanyifan;

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

import java.util.Arrays;

@Component
@Aspect
public class MyAdvice {

@Pointcut("execution(void com.kakawanyifan.dao.BookDao.select(..))")
public void pt(){}

@Before("pt()")
public void before(JoinPoint joinPoint) {
System.out.println("before");
Object[] args = joinPoint.getArgs();
System.out.println(Arrays.toString(args));
}

}

示例代码:

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
package com.kakawanyifan.dao.impl;

import com.kakawanyifan.dao.BookDao;
import org.springframework.stereotype.Repository;

import java.time.Instant;
import java.util.UUID;

@Repository
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println(Instant.now());
System.out.println("save");
}

public void delete() {
System.out.println("delete");
}

public void update() {
System.out.println("update");
}

public void select(String id) {
System.out.println("select");
System.out.println(id + ":" + UUID.randomUUID().toString());
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.kakawanyifan;

import com.kakawanyifan.dao.BookDao;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

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

运行结果:

1
2
3
4
before
[1]
select
1:9efd8992-fd18-4146-9fed-e22dda9ddb68

解释说明:因为一个方法的参数的个数是不固定的,所以使用数组更通配一些。

特别的,如果我们想在获取参数后,还对参数进行统一的处理呢?那就需要环绕通知了。

ProceedingJoinPoint

环绕利用ProceedingJoinPoint获取参数。
需要注意的是,proceedingJoinPoint.proceed()有两个方法,一个是无参的,一个是含参的。

在下面的例子中,利用的是含参的。示例代码:

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
package com.kakawanyifan;

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

import java.util.Arrays;

@Component
@Aspect
public class MyAdvice {

@Pointcut("execution(String com.kakawanyifan.dao.BookDao.select(..))")
public void pt(){}

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

}
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
package com.kakawanyifan.dao.impl;

import com.kakawanyifan.dao.BookDao;
import org.springframework.stereotype.Repository;

import java.time.Instant;
import java.util.UUID;

@Repository
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println(Instant.now());
System.out.println("save");
}

public void delete() {
System.out.println("delete");
}

public void update() {
System.out.println("update");
}

public String select(String id) {
System.out.println("select");
return id + ":" + UUID.randomUUID().toString();
}

}

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.kakawanyifan;

import com.kakawanyifan.dao.BookDao;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

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.select("1"));
}
}

运行结果:

1
2
3
4
5
before
[1]
[abc]
select
abc:1238c08f-e6e7-4307-9a4f-5c6556cc8cad

获取返回值

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

环绕通知获取返回值

关于环绕通知获取返回值,我们在上文都已经讨论过了。
我们不仅可以获取返回值,还可以对返回值进行修改。
这里不再赘述。

返回后通知获取返回值

返回后通知获取返回值,需要在通知方法中新增一个参数。并且:

  1. 该参数的名称需要和注解中配置的一致。
  2. 在同时有"JoinPoint"类型的参数的时候,必须在"JoinPoint"之后。
  3. 至于参数的数据类型,则没有要求。

示例代码:

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 com.kakawanyifan;

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

import java.time.Instant;
import java.util.Arrays;

@Component
@Aspect
public class MyAdvice {

@Pointcut("execution(String com.kakawanyifan.dao.BookDao.select(..))")
public void pt(){}

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

}
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.kakawanyifan;

import com.kakawanyifan.dao.BookDao;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

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

运行结果:

1
2
3
4
5
select
afterReturning
[1]
afterReturning : 1:da7dd56d-4cf6-4dcd-9dd4-5d9cab2016c8
return : 1:da7dd56d-4cf6-4dcd-9dd4-5d9cab2016c8

获取异常

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

环绕通知获取异常

之前用环绕通知的时候,我们会抛出异常,现在改为用catch获取异常。

示例代码:

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 com.kakawanyifan;

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;

import java.time.Instant;

@Component
@Aspect
public class MyAdvice {

@Pointcut("execution(String com.kakawanyifan.dao.BookDao.select(..))")
public void pt(){}

@Around("pt()")
public String around(ProceedingJoinPoint proceedingJoinPoint) {
Object[] args = proceedingJoinPoint.getArgs();
Object ret = "";
try {
ret = proceedingJoinPoint.proceed(args);
} catch (Throwable e) {
e.printStackTrace();
}

String str = ret.toString().split(":")[0] + ":" + Instant.now();

return str;
}

}

抛出异常后通知获取异常

需要利用一个参数获取通知,且该参数的名字需要和注解中配置的一样。

示例代码:

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

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

@Component
@Aspect
public class MyAdvice {

@Pointcut("execution(String com.kakawanyifan.dao.BookDao.select(..))")
public void pt(){}

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

}

事务

简介

Spring为了管理事务,提供了一个平台事务管理器PlatformTransactionManager,其部分源代码如下:

1
2
3
4
5
6
7
8
9
10
11
package org.springframework.transaction;

import org.springframework.lang.Nullable;

public interface PlatformTransactionManager extends TransactionManager {
TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;

void commit(TransactionStatus status) throws TransactionException;

void rollback(TransactionStatus status) throws TransactionException;
}

其中:

  • commit是用来提交事务的
  • rollback是用来回滚事务的

PlatformTransactionManager只是一个接口,Spring还为其提供了一个具体的实现DataSourceTransactionManager,部分源代码如下:

1
2
3
4
5
6
7
8
9
10

package org.springframework.jdbc.datasource;

【部分代码略】

public class DataSourceTransactionManager extends AbstractPlatformTransactionManager implements ResourceTransactionManager, InitializingBean {

【部分代码略】

}

正如其名字所描述的,我们只需要给它一个DataSource对象,它就可以帮我们在业务层管理事务。
当然,因为其内部采用的是JDBC的事务,所以还需要我们在持久层采用的是JDBC相关的技术。
而Mybatis内部采用的就是JDBC的事务。

我们直接通过例子,来讨论转账。

环境准备

创建表

1
2
3
4
5
6
7
8
9
10
11
12
create database spring_db character set utf8;
use spring_db;
create table account
(
id int primary key auto_increment,
name varchar(35),
money double
);
insert into account
values (1, 'Tom', 1000);
insert into account
values (2, 'Jerry', 1000);

pom.xml

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

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

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.12</version>
</dependency>

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.10</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.23</version>
</dependency>

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.7</version>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.22.RELEASE</version>
<scope>test</scope>
</dependency>

根据表创建模型类

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

import java.io.Serializable;

public class Account implements Serializable {

private Integer id;
private String name;
private Double money;

【Getter和Setter代码略】

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

创建Dao接口

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

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

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

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

创建Service接口和实现类

1
2
3
4
5
6
7
8
9
10
11
package com.kakawanyifan.service;

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

import com.kakawanyifan.dao.AccountDao;
import com.kakawanyifan.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class AccountServiceImpl implements AccountService {

@Autowired
AccountDao accountDao;

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

jdbc.properties

1
2
3
jdbc.url=jdbc:mysql://127.0.0.1:3306/spring_db
jdbc.username=root
jdbc.password=MySQL@2022

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
29
package com.kakawanyifan.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource;

import javax.sql.DataSource;

@PropertySource("classpath:jdbc.properties")
public class JdbcConfig {

@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.setUrl(url);
ds.setUsername(userName);
ds.setPassword(password);
return ds;
}

}

MybatisConfig配置类

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
package com.kakawanyifan.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 sqlSessionFactory(DataSource dataSource){
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
//设置数据源
sqlSessionFactoryBean.setDataSource(dataSource);
return sqlSessionFactoryBean;
}

//定义bean,返回MapperScannerConfigurer对象
@Bean
public MapperScannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
mapperScannerConfigurer.setBasePackage("com.kakawanyifan.dao");
return mapperScannerConfigurer;
}

}

SpringConfig配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.kakawanyifan.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;

@Configuration
@ComponentScan("com.kakawanyifan")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
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
package com.kakawanyifan.service;

import com.kakawanyifan.config.SpringConfig;
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 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("Tom","Jerry",100D);
}
}

步骤

在需要被事务管理的方法上添加注解@Transactional

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

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

建议写在接口方法上。

示例代码:

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

import com.kakawanyifan.dao.AccountDao;
import com.kakawanyifan.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class AccountServiceImpl implements AccountService {

@Autowired
AccountDao accountDao;

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

在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
29
30
31
32
33
34
35
36
37
38
39
package com.kakawanyifan.config;

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

import javax.sql.DataSource;

@PropertySource("classpath:jdbc.properties")
public class JdbcConfig {

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

}
  • 重点关注public PlatformTransactionManager transactionManager(DataSource dataSource)方法,该方法配置事务管理器。

开启事务

在SpringConfig的配置类中添加注解@EnableTransactionManagement

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.kakawanyifan.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("com.kakawanyifan")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
@EnableTransactionManagement
public class SpringConfig {

}

运行

最后,我们可以修改AccountServiceImpltransfer方法,使其报错。示例代码:

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

然后,重新运行test方法,运行结果:

1
2
3
4
5
6
7
8
9
java.lang.ArithmeticException: / by zero

at com.kakawanyifan.service.impl.AccountServiceImpl.transfer(AccountServiceImpl.java:16)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)

【部分运行结果略】

再观察一下数据库中的account表,数据没有被修改,说明事务生效了。

角色

有两个角色:

  1. 事务管理员
  2. 事务协调员

以刚刚的转账为例。

1
2
3
accountDao.outMoney(out,money);
int i = 1/0;
accountDao.inMoney(in,money);

在没有开启Spring事务时:

  • outMoney,会开启一个事务T1。
  • inMoney,会开启一个事务T2。
  • 而整个transfer方法没有事务。如果在运行过程中如果没有抛出异常,则T1和T2都正常提交,数据正确;如果在两个方法中间抛出异常,T1因为执行成功提交事务,T2因为抛异常不会被执行;就会导致数据出现错误。

开启Spring的事务管理后:

  • transfer方法上有一个@Transactional注解,因此,在该方法上会有一个事务T。
  • outMoney方法的事务T1会加入到transfer的事务T中。
  • inMoney方法的事务T2也会加入到transfer的事务T中。
  • 即,在同一个事务中。如果当业务层中出现异常,整个事务就会回滚,从而保证数据的准确性。

对于,发起事务方,我们称之为事务管理员,在Spring中通常是业务层开启事务的方法。
对于,加入事务方,我们称之为事务协调员,在Spring中通常指数据层方法,也可能是业务层方法。

配置

@Transactional注解上,可以进行参数配置。

  • readOnly
    • true,只读事务,通常查询设为true
    • false,读写事务,通常增删改设为false
  • timeout:设置超时时间,单位秒。
    • 在指定的时间之内事务没有提交成功就自动回滚。
    • -1表示不超时。
  • rollbackFor:当出现指定异常进行事务回滚,值为异常的类(XXX.class)。
  • noRollbackFor:当出现指定异常不进行事务回滚,值为异常的类(XXX.class)。
  • rollbackForClassName等同于rollbackFor,只不过属性值为异常的类全名字符串。
  • noRollbackForClassName等同于noRollbackFor,只不过属性值为异常的类全名字符串。
  • isolation事务的隔离级别,有5个枚举值:
    • DEFAULT:默认隔离级别,会采用数据库的隔离级别。
    • READ_UNCOMMITTED:读未提交。
    • READ_COMMITTED:读已提交。
    • REPEATABLE_READ:重复读取。
    • SERIALIZABLE:串行化。

(关于事务的隔离级别,可以参考我们在《MySQL从入门到实践:6.事务》的讨论。)

我们重点讨论一下rollbackFor等和事务会滚相关的四个属性。
首先,需要强调的是,不是所有的异常都会导致事务会滚,比如下面的代码就不会回滚。
示例代码:

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

import com.kakawanyifan.dao.AccountDao;
import com.kakawanyifan.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;

@Service
public class AccountServiceImpl implements AccountService {

@Autowired
AccountDao accountDao;

public void transfer(String out, String in, Double money) throws IOException {
accountDao.outMoney(out,money);
if (true){
throw new IOException();
}
accountDao.inMoney(in,money);
}
}

因为,Spring的事务只会对Error异常RuntimeException异常及其子类进行事务会滚,其他的异常类型是不会回滚的。
在上例中的IOException不符合上述条件所以不回滚。

如果一定要让IOException也会滚,可以通过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
package com.kakawanyifan.service.impl;

import com.kakawanyifan.dao.AccountDao;
import com.kakawanyifan.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;

@Service
public class AccountServiceImpl implements AccountService {

@Autowired
AccountDao accountDao;

@Transactional(rollbackFor = IOException.class)
public void transfer(String out, String in, Double money) throws IOException {
accountDao.outMoney(out,money);
if (true){
throw new IOException();
}
accountDao.inMoney(in,money);
}
}

传播行为

环境准备

现在,假设我们不但需要转账,还需要同时在一张日志表中记录下来。
而且,无论转账是否成功,都需要在日志表中记录。

一、创建日志表

1
2
3
4
5
6
create table log
(
id int primary key auto_increment,
info varchar(255),
createDate datetime
)

二、添加LogDao接口

1
2
3
4
5
6
7
8
9
10
package com.kakawanyifan.dao;

import org.apache.ibatis.annotations.Insert;

public interface LogDao {

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

}

三、LogService接口与实现类

1
2
3
4
5
6
7
package com.kakawanyifan.service;

public interface LogService {

void log(String out, String in, Double money);

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

import com.kakawanyifan.dao.LogDao;
import com.kakawanyifan.service.LogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class LogServiceImpl implements LogService {

@Autowired
LogDao logDao;

@Override
public void log(String out, String in, Double money) {
logDao.log("trans:" + money + " from:" + out + " to:" + in);
}

}

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

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

import com.kakawanyifan.dao.LogDao;
import com.kakawanyifan.service.LogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class LogServiceImpl implements LogService {

@Autowired
LogDao logDao;

@Override
public void log(String out, String in, Double money) {
logDao.log("trans:" + money + " from:" + out + " to:" + in);
}

}

运行
在转账业务之间出现了异常,account表确实回滚,但是log表未添加数据,这不符合我们说的无论转账是否成功,都要记录。

那么,为什么会失败呢?
因为日志的记录与转账操作隶属同一个事务,同成功同失败。
那么,怎么解决呢?
这就涉及到事务传播行为了。

propagation

事务传播行为:事务协调员对事务管理员所携带事务的处理态度。
配置事务传播行为,需要利用propagation属性。

我们为log方法添加@Transactional注解,并配置propagation属性为REQUIRES_NEW。示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.kakawanyifan.service.impl;

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

@Service
public class LogServiceImpl implements LogService {

@Autowired
LogDao logDao;

@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void log(String out, String in, Double money) {
logDao.log("trans:" + money + " from:" + out + " to:" + in);
}

}

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

propagation的可选值有:

  • REQUIRED(默认)
    • 事务管理员开启事务T时,事务协调员加入T。
    • 事务管理员无操作时,事务协调员新建事务T2。
  • REQUIRES_NEW
    • 事务管理员开启事务T时,事务协调员依然新建事务T2。
    • 事务管理员无操作时,事务协调员新建事务T2。
  • SUPPORTS
    • 事务管理员开启事务T时,事务协调员加入T。
    • 事务管理员无操作时,事务协调员无操作。
  • NOT_SUPPORTED
    • 事务管理员开启事务T时,事务协调员无操作。
    • 事务管理员无操作时,事务协调员无操作。
  • MANDATORY
    • 事务管理员开启事务T时,事务协调员加入T。
    • 事务管理员无操作时,事务协调员报错。
  • NEVER
    • 事务管理员开启事务T时,事务协调员报错。
    • 事务管理员无操作时,事务协调员无操作。
  • NESTED
    • 设置savePoint,一旦事务回滚,事务将回滚到savePoint处。
文章作者: Kaka Wan Yifan
文章链接: https://kakawanyifan.com/10816
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Kaka Wan Yifan

留言板