Spring框架详解(中篇)-AOP

3、AOP

3.1、场景引入

假设我们要写一个计算器功能,我们首先声明一个接口,里面包含了加减乘除四个抽象方法,然后再编写实现类,去实现这个方法。代码如下:

计算器接口:

1
2
3
4
5
6
7
8
9
10
public interface Calculator {
//加
int add(int i, int j);
//减
int sub(int i, int j);
//乘
int mul(int i, int j);
//除
int div(int i, int j);
}

实现类:

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
public class CalculatorImpl implements Calculator {
@Override
public int add(int i, int j) {
int result = i + j;
System.out.println("方法内部 result = " + result);
return result;
}

@Override
public int sub(int i, int j) {
int result = i - j;
System.out.println("方法内部 result = " + result);
return result;
}

@Override
public int mul(int i, int j) {
int result = i * j;
System.out.println("方法内部 result = " + result);
return result;
}

@Override
public int div(int i, int j) {
int result = i / j;
System.out.println("方法内部 result = " + result);
return result;
}
}

此时,计算器功能已经开发完毕,代码并不复杂,但是假如后期有这么一个需求:我们需要在每次计算之前打印参数日志,并在计算之后打印计算结果的日志。

为了实现这个功能,在没有学习新技术时,我们就需要修改源代码,在每个方法计算之前和计算之后加入日志功能,比如下面这样:

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
public class CalculatorImpl implements Calculator {
@Override
public int add(int i, int j) {
System.out.println("【日志】add 方法开始了,参数是:" + i + "," + j);
int result = i + j;
System.out.println("方法内部 result = " + result);
System.out.println("【日志】add 方法结束了,结果是:" + result);
return result;
}

@Override
public int sub(int i, int j) {
System.out.println("【日志】sub 方法开始了,参数是:" + i + "," + j);
int result = i - j;
System.out.println("方法内部 result = " + result);
System.out.println("【日志】sub 方法结束了,结果是:" + result);
return result;
}

@Override
public int mul(int i, int j) {
System.out.println("【日志】mul 方法开始了,参数是:" + i + "," + j);
int result = i * j;
System.out.println("方法内部 result = " + result);
System.out.println("【日志】mul 方法结束了,结果是:" + result);
return result;
}

@Override
public int div(int i, int j) {
System.out.println("【日志】div 方法开始了,参数是:" + i + "," + j);
int result = i / j;
System.out.println("方法内部 result = " + result);
System.out.println("【日志】div 方法结束了,结果是:" + result);
return result;
}
}

3.2、提出问题

① 现有代码缺陷

  1. 日志功能并非核心业务,编写代码过程中对核心业务功能有干扰,导致开发核心业务功能时容易分散精力。
  2. 附加功能分散在各个业务功能方法中,不利于统一维护。
  3. 附加代码都是重复代码,编写浪费时间。

②解决思路

  • 解决这两个问题的核心就是:解耦。即把附加功能从业务功能代码中抽取出来。

③困难

  • 要抽取的代码在方法内部,没有办法进行统一的封装。所以需要引入新的技术。

3.3、代理模式

二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。

① 静态代理

创建静态代理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class CalculatorStaticProxy implements Calculator{
// 将被代理的目标对象声明为成员变量
private Calculator target;

public CalculatorStaticProxy(Calculator target) {
this.target = target;
}

@Override
public int add(int i, int j) {
// 附加功能由代理类中的代理方法来实现
System.out.println("【日志】add 方法开始了,参数是:" + i + "," + j);
// 通过目标对象来实现核心业务逻辑
int addResult = target.add(i, j);
System.out.println("【日志】add 方法结束了,结果是:" + addResult);
return addResult;
}
......
......
}

静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。就拿日志功能来说,将来如果其他地方也需要附加日志,那还得再声明更多个静态代理类,那就产生了大量重复的代码,日志功能还是分散的,没有统一管理。

提出进一步的需求:将日志功能集中到一个代理类中,将来有任何日志需求,都通过这一个代理类来实现。这就需要使用动态代理技术了。

② 动态代理

动态代理有两种:

  1. jdk动态代理,要求必须有接口,最终生成的代理类和目标类实现相同的接口,生成在com.sun.proxy包下,类名为$proxy2。
  2. cglib动态代理,最终生成的代理类会继承目标类,并且和目标类在相同包下。

下面用jdk动态代理做演示。

创建生产代理对象的工厂类:

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
public class ProxyFactory {

private Object target;

public ProxyFactory(Object target) {
this.target = target;
}

public Object getProxy() {
/*
* newProxyInstance():创建一个代理实例
* 其中有三个参数:
* 1、ClassLoader loader:指定加载动态生成的代理类的类加载器
* 2、Class[] interfaces:目标对象实现的所有接口的class对象所组成的数组
* 3、InvocationHandler h:设置代理对象实现目标对象方法的过程,即代理类中如何重写接口中的抽象方法
*/
ClassLoader classLoader = this.getClass().getClassLoader();
Class<?>[] interfaces = target.getClass().getInterfaces();
InvocationHandler invocationHandler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
/*
* proxy:代理对象
* method:代理对象需要实现的方法,即其中需要重写的方法
* args:method所对应方法的参数
*/
Object result = null;
try {
System.out.println("【动态代理】【日志】" + method.getName() + ",参数:" + Arrays.toString(args));
result = method.invoke(target, args);
System.out.println("【动态代理】【日志】" + method.getName() + ",结果:" + result);
} catch (Exception e) {
e.printStackTrace();
System.out.println("【动态代理】【日志】" + method.getName() + ",异常:" + e.getMessage());
} finally {
System.out.println("【动态代理】【日志】" + method.getName() + ",方法执行完毕");
}
return result;
}
};
//返回动态生成的代理对象
return Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
}
}

测试

1
2
3
4
5
6
@Test
public void test(){
ProxyFactory proxyFactory = new ProxyFactory(new CalculatorImpl());
Calculator proxy = (Calculator) proxyFactory.getProxy();
proxy.add(1, 1);
}

输出:
【动态代理】【日志】add,参数:[1, 1]
方法内部 result = 2
【动态代理】【日志】add,结果:2
【动态代理】【日志】add,方法执行完毕

3.4、AOP介绍

3.4.1、概念

AOP(Aspect Oriented Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程的一种补充和完善,它以通过预编译方式和运行期动态代理方式实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术。

3.4.2、术语

① 横切关注点:横切关注点就是从目标对象的核心业务中抽取出的非核心代码,有多少非核心业务就有多少横切关注点。

② 切面:我们需要把横切关注点封装到一个类中,这个类就叫做切面,也可以把封装通知方法的类叫做切面。

③ 通知:切面类中封装的每一个横切关注点都是一个通知,一般都需要写一个方法来实现,这样的方法就叫通知方法。

通俗来说,站在目标对象的角度,这些非核心代码叫做横切关注点;站在切面类的角度,每一个横切关注点所封装的方法都是一个通知。

  • 前置通知:在被代理的目标方法执行
  • 返回通知:在被代理的目标方法成功结束后执行
  • 异常通知:在被代理的目标方法异常结束后执行
  • 后置通知:在被代理的目标方法最终结束后执行
  • 环绕通知:使用try…catch…finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置

④ 目标:被代理的目标对象,也就是要进行功能增强的对象。

⑤ 代理:为目标对象所创建出来的代理对象;在AOP中,该对象不需要手动创建,是由AOP所封装的动态代理模式自动创建出来的。

⑥ 连接点:也就是抽取横切关注点的位置。这也是一个纯逻辑概念,不是语法定义的。比如:

1
2
3
4
5
6
public int add(int i, int j) {
//横切关注点1:方法开始。---->连接点1
int result = i + j;
//横切关注点2:方法结束。---->连接点2
return result;
}

⑦ 切入点:定位连接点的方式,通过切入点我们可以找到连接点。

每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)。如果把连接点看作数据库中的记录,那么切入点就是查询记录的 SQL 语句。

以上概念可以总结为一句话:AOP就是我们抽取横切关注点,封装到切面中,它就是一个通知,再通过切入点定位到连接点,那么就可以在不改变目标对象代码同时,把切面中的通知,通过切入点表达式套到连接点上,从而实现功能的增强。

3.4.3、作用

  1. 简化代码:把方法中固定位置的重复的代码抽取出来,让被抽取的方法更专注于自己的核心功能,提高内聚性。
  2. 代码增强:把特定的功能封装到切面类中,看哪里有需要,就往上套,被套用了切面逻辑的方法就被切面给增强了。

3.5、基于注解的AOP

  • JDK动态代理(InvocationHandler):JDK原生的实现方式,需要被代理的目标类必须实现接口。因为这个技术要求代理对象和目标对象实现同样的接口
  • cglib:通过继承被代理的目标类实现代理,所以不需要目标类实现接口。
  • AspectJ:本质上是静态代理,将代理逻辑“织入”被代理的目标类编译得到的字节码文件,所以最终效果是动态的。weaver就是织入器。Spring只是借用了AspectJ中的注解。

3.5.1、准备工作

在IOC所需依赖基础上再加入下面依赖即可:

1
2
3
4
5
6
<!-- spring-aspects会帮我们传递过来aspectjweaver -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.20</version>
</dependency>

创建接口

1
2
3
4
5
6
7
8
9
10
public interface Calculator {
//加
int add(int i, int j);
//减
int sub(int i, int j);
//乘
int mul(int i, int j);
//除
int div(int i, int j);
}

创建实现类

注意:切面类和目标类都要交给IOC容器管理

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
@Component //交给IOC容器管理
public class CalculatorImpl implements Calculator {
@Override
public int add(int i, int j) {
int result = i + j;
System.out.println("方法内部 result = " + result);
return result;
}

@Override
public int sub(int i, int j) {
int result = i - j;
System.out.println("方法内部 result = " + result);
return result;
}

@Override
public int mul(int i, int j) {
int result = i * j;
System.out.println("方法内部 result = " + result);
return result;
}

@Override
public int div(int i, int j) {
int result = i / j;
System.out.println("方法内部 result = " + result);
return result;
}
}

在Spring配置中开启注解扫描

1
<context:component-scan base-package="com.dyz"/>

3.5.2、创建切面类

  1. 在切面类中,需要使用指定的注解来标识通知方法

  2. 切入点表达式:设置在通知注解的value属性中。

    • execution(public int com.dyz.CalculatorImpl.add(int, int)):表示只作用于add()方法。
    • * com.dyz.CalculatorImpl.*(..)):表示作用于CalculatorImpl类的所有方法。
    • * com.dyz.*.*(..)):表示作用于该包下所有类的所有方法。
      • 第一个*表示任意访问修饰符和返回值类型;
      • 第二个*表示包下任何一个类;
      • 第三个*表示类中任意方法;
      • ..表示任意参数;
  3. 获取连接点的信息:在通知方法的参数位置,设置JoinPoint类型的参数,就可以获取连接点对应方法的信息。

    • joinPoint.getSignature():用于获取连接点对应方法的签名信息。
    • joinPoint.getArgs():用于获取连接点对应方法的参数列表。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Aspect //@Aspect标注这个类是一个切面类
@Component //添加到IOC容器
public class LogAspect {
/**
* 前置通知方法
*/
@Before("execution(* com.dyz.CalculatorImpl.*(..))")
public void beforeMethod(JoinPoint joinPoint) {
//用于获取连接点对应方法的签名信息
Signature signature = joinPoint.getSignature();
//用于获取连接点对应方法的参数列表
Object[] args = joinPoint.getArgs();
System.out.println("【AOP】【日志】" + signature.getName() + ",参数:" + Arrays.toString(args));
}
}

在Spring配置文件中开启基于注解的AOP功能

1
2
<!--开启基于注解的AOP功能-->
<aop:aspectj-autoproxy />

测试

1
2
3
4
5
6
7
@Test
public void test() {
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("applicationContext.xml");
Calculator calculator = applicationContext.getBean(Calculator.class);
calculator.add(1, 1);
}

输出:
【AOP】【日志】add,参数:[1, 1]
方法内部 result = 2

3.5.3、各种通知注解

  • 前置通知:使用**@Before注解标识,在被代理的目标方法前**执行
  • 返回通知:使用**@AfterReturning注解标识,在被代理的目标方法成功结束**后执行,有异常不执行。
  • 异常通知:使用**@AfterThrowing注解标识,在被代理的目标方法异常结束**后执行,有异常才执行。
  • 后置通知:使用**@After注解标识,在被代理的目标方法最终结束**后执行
  • 环绕通知:使用**@Around注解标识,使用try…catch…finally结构围绕整个被代理的目标方法**,包括上面四种通知对应的所有位置

各种通知的执行顺序:

  • Spring版本5.3.x以前:
  • 前置通知
  • 目标操作
  • 后置通知
  • 返回通知或异常通知
  • Spring版本5.3.x以后:
  • 前置通知
  • 目标操作
  • 返回通知或异常通知
  • 后置通知

3.5.4、切入点表达式语法

  • 用*号代替“权限修饰符”和“返回值”部分表示“权限修饰符”和“返回值”不限
  • 在包名的部分,一个“*”号只能代表包的层次结构中的一层,表示这一层是任意的。
    • 例如:*.Hello匹配com.Hello,不匹配com.dyz.Hello
  • 在包名的部分,使用“*..”表示包名任意、包的层次深度任意
  • 在类名的部分,类名部分整体用*号代替,表示类名任意
  • 在类名的部分,可以使用*号代替类名的一部分
    • 例如:*Service匹配所有名称以Service结尾的类或接口
  • 在方法名部分,可以使用*号表示方法名任意
  • 在方法名部分,可以使用*号代替方法名的一部分
    • 例如:*Operation匹配所有方法名以Operation结尾的方法
  • 在方法参数列表部分,使用(..)表示参数列表任意
  • 在方法参数列表部分,使用(int,..)表示参数列表以一个int类型的参数开头
  • 在方法参数列表部分,基本数据类型和对应的包装类型是不一样的
  • 切入点表达式中使用 int 和实际方法中 Integer 是不匹配的
  • 在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符
    • 例如:execution(public int ..Service.*(.., int)) 正确
    • 例如:execution(* int ..Service.*(.., int)) 错误

3.5.5、切入点表达式复用

我们可以只设置一次切入点表达式,然后可以直接使用准备好的切入点表达式

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
@Aspect
@Component
public class LogAspect {

//一个通用的切入点表达式
@Pointcut("execution(* com.dyz.CalculatorImpl.*(..))")
public void pointCut() {
}

//前置通知
@Before("pointCut()")
public void beforeMethod(JoinPoint joinPoint) {
//用于获取连接点对应方法的签名信息
Signature signature = joinPoint.getSignature();
//用于获取连接点对应方法的参数列表
Object[] args = joinPoint.getArgs();
System.out.println("【AOP】【日志】" + signature.getName() + ",参数:" + Arrays.toString(args));
}

//后置通知
@After("pointCut()")
public void afterMethod(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
System.out.println("【AOP】【日志】" + signature.getName() + ",方法执行完毕");
}

//返回通知(returning属性用于接收目标对象方法的返回值)
@AfterReturning(value = "pointCut()", returning = "result")
public void afterReturningMethod(JoinPoint joinPoint, Object result) {
Signature signature = joinPoint.getSignature();
System.out.println("【AOP】【日志】" + signature.getName() + ",结果:" + result);
}

//异常通知(throwing属性用于接收目标对象方法的异常信息)
@AfterThrowing(value = "pointCut()", throwing = "ex")
public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex) {
Signature signature = joinPoint.getSignature();
System.out.println("【AOP】【日志】" + signature.getName() + ",异常:" + ex);
}
}

测试:

1
2
3
4
5
6
7
@Test
public void test() {
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("applicationContext.xml");
Calculator calculator = applicationContext.getBean(Calculator.class);
calculator.div(1, 1);
}

无异常状态输出:
【AOP】【日志】div,参数:[1, 1]
方法内部 result = 1
【AOP】【日志】div,结果:1
【AOP】【日志】div,方法执行完毕

有异常状态输出:
【AOP】【日志】div,参数:[1, 0]
【AOP】【日志】div,异常:java.lang.ArithmeticException: / by zero
【AOP】【日志】div,方法执行完毕

3.5.6、环绕通知

环绕通知相当于包含了前四种所有通知。

其中方法参数类型为ProceedingJoinPoint类型,相比于JoinPoint类型,它是一个可执行的连接点对象,它有个proceed()方法,表示目标对象方法的执行。

环绕通知方法的返回值也必须和目标对象方法的返回值一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//环绕通知,相当于前四种通知
@Around("pointCut()")
public Object aroundMethod(ProceedingJoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
Object result = null;
try {
System.out.println("【AOP】【日志】" + methodName + ",参数:" + args);
//目标方法的执行,目标方法的返回值一定要返回给外界调用者
result = joinPoint.proceed();
System.out.println("【AOP】【日志】" + methodName + ",结果:" + result);
} catch (Throwable ex) {
ex.printStackTrace();
System.out.println("【AOP】【日志】" + methodName + ",异常:" + ex);
} finally {
System.out.println("【AOP】【日志】" + methodName + ",方法执行完毕");
}
return result;
}

3.5.7、切面的优先级

相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序

  • 优先级高的切面:外面
  • 优先级低的切面:里面

使用@Order注解可以控制切面的优先级:

  • @Order(较小的数):优先级高
  • @Order(较大的数):优先级低

创建另一个切面,并设置优先级大于LogAspect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component
@Aspect
@Order(1) //设置优先级为1,默认是Integer.MAX_VALUE,因此优先级高于LogAspect
public class ValidateAspect {

@Before("com.dyz.LogAspect.pointCut()")
public void beforeMethod(JoinPoint joinPoint) {
System.out.println("这是ValidateAspect------>前置通知");
}

@After("com.dyz.LogAspect.pointCut()")
public void afterMethod(JoinPoint joinPoint) {
System.out.println("这是ValidateAspect------>后置通知");
}

@AfterReturning("com.dyz.LogAspect.pointCut()")
public void afterReturningMethod(JoinPoint joinPoint) {
System.out.println("这是ValidateAspect------>返回通知");
}
}

输出:
这是ValidateAspect——>前置通知
【AOP】【日志】div,参数:[1, 1]
方法内部 result = 1
【AOP】【日志】div,结果:1
【AOP】【日志】div,方法执行完毕
这是ValidateAspect——>返回通知
这是ValidateAspect——>后置通知

3.6、基于xml的AOP

Spring的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
<?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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 扫描组件 -->
<context:component-scan base-package="com.dyz"/>

<aop:config>
<!-- 设置切入点 -->
<aop:pointcut id="pointCut" expression="execution(* com.dyz.CalculatorImpl.*(..))"/>
<!-- 将IOC容器的某个bean设置为切面 -->
<aop:aspect ref="logAspect">
<!-- 前置通知 -->
<aop:before method="beforeMethod" pointcut-ref="pointCut"/>
<!-- 后置通知 -->
<aop:after method="afterMethod" pointcut-ref="pointCut"/>
<!-- 返回通知 -->
<aop:after-returning method="afterReturningMethod" pointcut-ref="pointCut" returning="result"/>
<!-- 异常通知 -->
<aop:after-throwing method="afterThrowingMethod" pointcut-ref="pointCut" throwing="ex"/>
<!-- 环绕通知 -->
<aop:around method="aroundMethod" pointcut-ref="pointCut"/>
</aop:aspect>

<!-- 设置其他切面 -->
<aop:aspect ref="validateAspect">
......
......
</aop:aspect>

</aop:config>
</beans>

切面,删除所有关于AOP的注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Component
public class LogAspect {

public void beforeMethod(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
Object[] args = joinPoint.getArgs();
System.out.println("【AOP】【日志】" + signature.getName() + ",参数:" + Arrays.toString(args));
}

public void afterMethod(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
System.out.println("【AOP】【日志】" + signature.getName() + ",方法执行完毕");
}

public void afterReturningMethod(JoinPoint joinPoint, Object result) {
Signature signature = joinPoint.getSignature();
System.out.println("【AOP】【日志】" + signature.getName() + ",结果:" + result);
}

public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex) {
Signature signature = joinPoint.getSignature();
System.out.println("【AOP】【日志】" + signature.getName() + ",异常:" + ex);
}

public Object aroundMethod(ProceedingJoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
Object result = null;
try {
System.out.println("【AOP】【日志】" + methodName + ",参数:" + args);
result = joinPoint.proceed();
System.out.println("【AOP】【日志】" + methodName + ",结果:" + result);
} catch (Throwable ex) {
ex.printStackTrace();
System.out.println("【AOP】【日志】" + methodName + ",异常:" + ex);
} finally {
System.out.println("【AOP】【日志】" + methodName + ",方法执行完毕");
}
return result;
}
}