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,面向对象编程,也是一种编程范式。)

Spring对AOP的实现方式,就是利用动态代理。
在运行期间,Spring通过动态代理技术动态的生成代理对象,代理对象方法执行时进行增强功能的介入,从而完成功能的增强。

在Python中,也有一种方式,可以在不改动原始代码的基础上为其进行功能增强,装饰器,其基于的是闭包。
具体可以参考《基于Python的后端开发入门:3.拷贝、类型注解、闭包和一些常用的包》

动态代理技术

常用的动态代理技术有两种:

  • JDK代理:基于接口的动态代理技术(必须要有接口)
  • cglib代理:基于父类的动态代理技术(可以没有接口)
    cglib是为目标对象,动态的生成子对象。注意,这不是继承,是动态的生成子对象。

动态代理

JDK的动态代理

目标类接口:

1
2
3
4
5
package com.kakawanyifan.proxy.jdk;

public interface TargetInterface {
public void method();
}

目标类:

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

public class TargetInterfaceImpl implements TargetInterface{
@Override
public void method() {
System.out.println("method");
}
}

动态代理
示例代码:

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.proxy.jdk;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class ProxyApp {
public static void main(String[] args) {
//创建目标对象
TargetInterfaceImpl targetInterfaceImpl = new TargetInterfaceImpl();
TargetInterface proxy = (TargetInterface) Proxy.newProxyInstance(
// 目标对象类,加载器
targetInterfaceImpl.getClass().getClassLoader(),
// 目标对象相同的接口字节码对象数组
targetInterfaceImpl.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object invoke = method.invoke(targetInterfaceImpl, args);
return invoke;
}
}
);

proxy.method();
}
}

运行结果:

1
method

但这好像不太对啊?
光有代理,没有增强啊。

我们新建一个类Advice

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

public class Advice {
public void before(){
System.out.println("before method");
}

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

然后在ProxyApp中的Object invoke = method.invoke(targetInterfaceImpl, args);的前后分别执行advice.before();advice.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
26
27
28
29
30
31
32
package com.kakawanyifan.proxy.jdk;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class ProxyApp {
public static void main(String[] args) {
//创建目标对象
TargetInterfaceImpl targetInterfaceImpl = new TargetInterfaceImpl();

Advice advice = new Advice();

TargetInterface proxy = (TargetInterface) Proxy.newProxyInstance(
// 目标对象类,加载起
targetInterfaceImpl.getClass().getClassLoader(),
// 目标对象相同的接口字节码对象数组
targetInterfaceImpl.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
advice.before();
Object invoke = method.invoke(targetInterfaceImpl, args);
advice.after();
return invoke;
}
}
);

proxy.method();
}
}

运行结果:

1
2
3
before method
method
after method

cglib的动态代理

第一步,导包?
如果我们已经导过Spring的包的话,不需要额外再导入cglib的包,因为Spring集成了cglib的包。

示例代码:

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.proxy.cglib;

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class ProxyApp {
public static void main(String[] args) {
Advice advice = new Advice();
// 创建目标对象
TargetInterfaceImpl TargetInterfaceImpl = new TargetInterfaceImpl();
// 创建增强器
Enhancer enhancer = new Enhancer();
// 设置父类
enhancer.setSuperclass(TargetInterfaceImpl.class);
// 设置回调
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects,MethodProxy methodProxy) throws Throwable {
advice.before();
Object invoke = method.invoke(TargetInterfaceImpl, objects);
advice.after();
return invoke;
}
});

TargetInterfaceImpl proxy = (TargetInterfaceImpl) enhancer.create(); //创建代理对象

proxy.method();
}
}

运行结果:

1
2
3
before method
method
after method

Spring中的AOP

那么,Spring采用的是哪种方式呢?cglib?Spring中都有cglib的包。

在Spring中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式。

开始

概述

Spring对动态代理的代码进行了封装,封装后我们只需要关注的部分代码的编写,并通过注解或配置的方式,对指定目标的方法进行增强。

相关概念如下:

  • Target(目标对象):代理的目标对象。
  • Proxy(代理):一个类被AOP织入增强后,就产生一个代理类。
  • Joinpoint(连接点):连接点是指那些被拦截到的点。在Spring中,这些点指的是方法,因为Spring只支持方法类型的连接点。
  • Pointcut(切入点):切入点是指我们要对哪些Joinpoint进行拦截的定义
  • Advice(通知/增强):通知是指拦截到Joinpoint之后所要做的事情就是通知
  • Aspect(切面):是切入点和通知(引介)的结合
  • Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程。

环境准备

假设,现在存在一个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完成了对原始方法的功能增强。

XML方式

需要在applicationContext.xml添加如下的三行,导入aop命名空间。

  • xmlns:aop="http://www.springframework.org/schema/aop"
  • http://www.springframework.org/schema/aop
  • http://www.springframework.org/schema/aop/spring-aop.xsd

并在applicationContext.xml中配置织入关系。

applicationContext.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">

<bean id="myAdvice" class="com.kakawanyifan.MyAdvice"></bean>
<bean id="bookDao" class="com.kakawanyifan.dao.impl.BookDaoImpl"></bean>

<aop:config>
<!--引用myAdvice的Bean为切面对象-->
<aop:aspect ref="myAdvice">
<!--配置Target的method方法执行时要进行myAspect的before方法前置增强-->
<aop:before method="Method" pointcut="execution(void com.kakawanyifan.dao.BookDao.delete())"></aop:before>
</aop:aspect>
</aop:config>

</beans>

示例代码:

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.support.ClassPathXmlApplicationContext;

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

运行结果:

1
2
2022-10-27T07:35:24.774Z
save

机制

过程

我们讨论Spring的工作过程。

一、Spring容器启动,加载Bean

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

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

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

如果用的是配置文件,不是注解的话,整体方式差不多。只是从读取注解改成了读取配置文件。
另外,根据我们关于配置文件的例子,我们很容易理解"只有pt()会被读取,pt2nd()不会被读取。"。
因为在配置文件中,这个配置都不全,pointcut属性没有。

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. 抛出异常后通知

如果是配置方式的话,整体格式如下

1
<aop:通知类型 method="切面类中方法名" pointcut=“切点表达式"></aop:通知类型>

前置通知

  • 注解:@Before
  • 配置:<aop:before>

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

后置通知

  • 注解:@After
  • 配置:<aop: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
  • 配置:<aop: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
  • 配置:<aop:after-returning>

也有一些资料,称该通知方式为"后置通知"。

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

示例代码:

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
  • 配置:<aop:throwing>

也有一些资料,称该通知方式为"异常抛出通知"。

故名思义,一定要抛出异常了,才会被执行;如果原始方式不抛出异常,直接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)

【部分运行结果略】

切点配置方式

1
2
3
4
5
<aop:config>
<aop:aspect ref="切面类">
<aop:before method="通知方法名称" pointcut="切点表达式"></aop:before>
</aop:aspect>
</aop:config>

当多个增强的切点表达式相同时,可以将切点表达式进行抽取,在增强中使用pointcut-ref属性代替pointcut属性来引用抽取后的切点表达式。

1
2
3
4
5
6
7
<aop:config>
<!--引用myAspect的Bean为切面对象-->
<aop:aspect ref="myAdvice">
<aop:pointcut id="myPointcut" expression="execution(* com.kakawanyifan.*(..))"/>
<aop:before method="before" pointcut-ref="myPointcut"></aop:before>
</aop:aspect>
</aop:config>

获取数据

获取的数据有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提供了声明式事务,顾名思义就是采用声明的方式来处理事务。这里所说的声明,就是指在注解或配置的方式。

事务管理不侵入开发的组件。具体来说,业务逻辑对象就不会意识到正在事务管理之中,事实上也应该如此,因为事务管理是属于系统层面的服务,而不是业务逻辑的一部分,如果想要改变事务管理策划的话,也只需要在定义文件中重新配置即可。
在不需要事务管理的时候,只要在设定文件上修改一下,即可移去事务管理服务,无需改变代码重新编译,这样维护起来极其方便。

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只是一个接口,不同的Dao层技术则有不同的实现类,例如Dao层技术是JDBC时,具体的实现是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对象,它就可以帮我们在业务层管理事务。

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

环境准备

创建表

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

XML方式

我们以《15.Spring Framework [1/2]》的"整合MyBatis"的"XML方式"的例子为基础,讨论事务的"XML方式"。

导入tx命名空间

需要在applicationContext.xml添加如下的三行,导入aop命名空间。

  • xmlns:tx="http://www.springframework.org/schema/tx"
  • http://www.springframework.org/schema/tx
  • http://www.springframework.org/schema/tx/spring-tx.xsd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">

配置事务增强

1
2
3
4
5
6
7
8
9
10
11
<!--平台事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>

<!-- 事务增强配置 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>

配置事务AOP织入

1
2
3
4
5
<!-- 事务的AOP增强 -->
<aop:config>
<aop:pointcut id="myPointcut" expression="execution(* com.kakawanyifan.service.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="myPointcut"></aop:advisor>
</aop:config>

所以,还需要导入AOP相关的包。

1
2
3
4
5
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.9.1</version>
</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.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 java.io.IOException;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"classpath:applicationContext.xml"})
public class AccountServiceTest {
@Autowired
private AccountService accountService;

@Test
public void testTransfer() throws IOException {
accountService.transfer("Tom","Jerry",100D);
}
}

切点方法的事务参数

1
2
3
4
5
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>

其中,<tx:method>代表切点方法的事务参数的配置,例如:

1
2
3
4
5
6
<tx:method 
name="transfer"
isolation="REPEATABLE_READ"
propagation="REQUIRED"
timeout="-1"
read-only="false"/>
  • name:切点方法名称
  • isolation:事务的隔离级别
  • propogation:事务的传播行为
  • timeout:超时时间
  • read-only:是否只读
文章作者: Kaka Wan Yifan
文章链接: https://kakawanyifan.com/10816
版权声明: 本博客所有文章版权为文章作者所有,未经书面许可,任何机构和个人不得以任何形式转载、摘编或复制。

评论区