什么是 AOP,如何理解面向切面编程

AOP, Aspect Oriented Programming ,即面向切面编程

通过预编译方法和运行期间动态代理的方式实现,在不修改源代码的方式下,给程序动态统一添加额外功能的一种技术。

  • AOP是对面向对象编程OOP的一个补充。
  • 它的目的是将复杂的需求分解为不同的切面,将散布在系统中的公共功能集中解决。
  • 它的实际含义是在运行时将代码切入到类的指定方法、指定位置上,将不同方法的同一个位置抽象为一个切面对象,并对该对象进行编程。

img

AOP 的目的是为了解耦 其次是简化开发,AOP 是 Spring 的核心 面向切面编程

他是一套规范,通过预编译方式和运行期间动态代理实现程序的统一维护

核心概念 就是 将分散在各个业务逻辑代码中的相同的代码通过横向切割的方式抽取到一个独立的模块中

img

AOP的优点

  • 降低模块之间的耦合度
  • 使系统更容易扩展
  • 更好的代码复用
  • 非业务代码更加集中,不分散,便于统一管理
  • 业务代码更加简洁纯粹,不掺杂其他的代码的影响

AOP中出现的一些概念

  • 切面:横切关注点,从每个方法中抽取的非核心业务,被模块化的抽象对象,横切关注点的模块化,比如下标提到的日志组件。可以认为是通知、引入和切入点的组合;在Spring中可以使用Schema和@AspectJ方式进行组织实现;在AOP中表示为在哪干和干什么集合
  • 通知:切面对象完成的工作(非业务代码),在连接点上执行的行为,通知提供了在AOP中需要在切入点所选择的连接点处进行扩展现有行为的手段;包括前置通知(before advice)、后置通知(after advice)、环绕通知(around advice),在Spring中通过代理模式实现AOP,并通过拦截器模式以环绕连接点的拦截器链织入通知;在AOP中表示为干什么;
  • 目标:被通知的对象(即被横切的对象),需要被织入横切关注点的对象,即该对象是切入点选择的对象,需要被通知的对象,从而也可称为被通知对象;由于Spring AOP 通过代理模式实现,从而这个对象永远是被代理对象,在AOP中表示为对谁干
  • 代理:切面、通知、目标混合之后的对象。封装通知方法的类
  • AOP代理:AOP框架使用代理模式创建的对象,从而实现在连接点处插入通知(即应用切面),就是通过代理来对目标对象应用切面。在Spring中,AOP代理可以用JDK动态代理或CGLIB代理实现,而通过拦截器模型应用切面。在AOP中表示为怎么实现的一种典型方式
  • 连接点:通知要插入业务代码的具体位置,表示需要在程序中插入横切关注点的扩展点,连接点可能是类初始化、方法执行、方法调用、字段调用或处理异常等等,Spring只支持方法执行连接点,在AOP中表示为spring允许你使用通知的地方;(如Spring实现中的JoinPoint)
  • 切入点:选择一组相关连接点的模式,即可以认为连接点的集合,Spring支持perl5正则表达式和AspectJ切入点模式,Spring默认使用AspectJ语法,在AOP中表示为在哪里干的集合
  • 织入:把切面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。在AOP中表示为怎么实现的
  • 引入:也称为内部类型声明,为已有的类添加额外新的字段或方法,Spring允许引入新的接口(必须对应一个实现)到所有被代理对象(目标对象), 在AOP中表示为干什么(引入什么)
img

AOP 的核心是连接点

连接点是我们需要关注的程序拓展点

可能是类初始化 方法执行 方法调用 字段调用 异常处理等

Spring 支持的连接点是方法执行点

切入点是一系列连接点的集合,Spring默认使用AspectJ语法,在AOP中抽象表示为可以进行操作的集合

之后就是通知

通知就是我们在连接点上执行的行为

连接点 切入点 通知组合在一起 就是一个切面

把切面映入到其他应用程序或者对象上,创建一个被通知的对象,这些就是织入,Spring 在运行时完成织入 ,在 AOP 中表示为怎么实现的,实现方式

通知类型:

  1. 前置通知(Before advice):

    在被代理的目标方法之前执行的通知,但这个通知不能阻止连接点之前的执行流程(除非它抛出一个异常)。

  2. 后置通知(After returning advice):

    在被代理的目标方法后执行的通知:例如,一个方法没有抛出任何异常,正常返回。

  3. 异常通知(After throwing advice):

    在被代理的目标方法抛出异常退出时执行的通知。

  4. 最终通知(After (finally) advice):

    当被代理的目标方法最终说明的时候执行的通知(不论是正常返回还是异常退出)。

  5. 环绕通知(Around Advice):

    包围一个连接点的通知,如方法调用。这是最强大的一种通知类型。环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它自己的返回值或抛出异常来结束执行。

环绕通知是最常用的通知类型。和AspectJ一样,Spring提供所有类型的通知,推荐你使用尽可能简单的通知类型来实现需要的功能。例如,如果你只是需要一个方法的返回值来更新缓存,最好使用后置通知而不是环绕通知,尽管环绕通知也能完成同样的事情。用最合适的通知类型可以使得编程模型变得简单,并且能够避免很多潜在的错误。比如,你不需要在JoinPoint上调用用于环绕通知的proceed()方法,就不会有调用的问题。

Spring AOP和 AspectJ 是什么关系

AspectJ 是一个更加强大的 AOP 框架 是一个 AOP 标准

如果只是简单的业务 可以使用 AOP

AOP 一个重要的原则就是无侵入性

AspectJ 重要的是 一般在编译期进行 即静态织入

在这个期间使用AspectJ的acj编译器(类似javac)把aspect类编译成class字节码后,在java目标类编译时织入,即先编译aspect类再编译目标类。

动态织入的方式是在运行时动态将要增强的代码织入到目标类中,这样往往是通过动态代理技术完成的,如Java JDK的动态代理(Proxy,底层通过反射实现)或者CGLIB的动态代理(底层通过继承实现),Spring AOP采用的就是基于运行时增强的代理技术。 img

Spring AOP更易用,AspectJ更强大

Java中的代理模式实现AOP

创建一个计算器接口,定义四个方法

1
2
3
4
5
6
7
8
package edu.software.ergoutree.spring6aop.example;

public interface Calculator {
public int add(int a, int b);
public int subtract(int a, int b);
public int multiply(int a, int b);
public int divide(int a, int b);
}

创建一个带日志输出的实现类

高耦合的写法,每次打印日志都要手动完成

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

// 带日志的实现类
// 发现日志功能对核心功能没有影响,但是混合到一起了,既要关心核心业务逻辑,还要关注日志,维护困难
public class CalculatirLogImpl implements Calculator{
@Override
public int add(int a, int b) {
System.out.println("[日志] add 方法开始");
int result = a + b;
System.out.println("方法内部Result: " + result);
System.out.println("[日志] add 方法结束");
return result;

}
@Override
public int subtract(int a, int b) {
System.out.println("[日志] sub 方法开始");
int result = a - b;
System.out.println("方法内部Result: " + result);
System.out.println("[日志] sub 方法结束");
return result;
}
@Override
public int multiply(int a, int b) {
System.out.println("[日志] mul 方法开始");
int result = a * b;
System.out.println("方法内部Result: " + result);
System.out.println("[日志] mul 方法结束");
return result;
}
@Override
public int divide(int a, int b) {
System.out.println("[日志] div 方法开始");
int result = a / b;
System.out.println("方法内部Result: " + result);
System.out.println("[日志] div 方法结束");
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
38
39
40
41
42
43
44
45
46
47
48
package edu.software.ergoutree.spring6aop.example;

// 代理类
public class CalculatorProxy implements Calculator {
// 传递被代理的目标对象
private final Calculator target;
public CalculatorProxy(Calculator target) {
this.target = target;
}

@Override
public int add(int a, int b) {
System.out.println("代理: 开始执行add方法"); // 关于代理的日志仅仅是记录操作,不代表没有实现代理
// 调用目标对象的方法实现核心业务的实现
int result = target.add(a, b);
System.out.println("代理: add方法执行完成,结果=" + result);
return result;
}

@Override
public int subtract(int a, int b) {
System.out.println("代理: 开始执行subtract方法");
int result = target.subtract(a, b);
System.out.println("代理: subtract方法执行完成,结果=" + result);
return result;
}

@Override
public int multiply(int a, int b) {
System.out.println("代理: 开始执行multiply方法");
int result = target.multiply(a, b);
System.out.println("代理: multiply方法执行完成,结果=" + result);
return result;
}

@Override
public int divide(int a, int b) {
System.out.println("代理: 开始执行divide方法");
try {
int result = target.divide(a, b);
System.out.println("代理: divide方法执行完成,结果=" + result);
return result;
} catch (ArithmeticException e) {
System.out.println("代理: divide方法执行出错 - " + e.getMessage());
throw e;
}
}
}

测试类

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 edu.software.ergoutree.spring6aop.example;

public class Main {
public static void main(String[] args) {
// 创建目标对象
Calculator target = new CalculatorImpl();

// 创建代理对象,传入目标对象
Calculator proxy = new CalculatorProxy(target);

// 通过代理对象调用方法
System.out.println("10 + 5 = " + proxy.add(10, 5));
System.out.println("10 - 5 = " + proxy.subtract(10, 5));
System.out.println("10 * 5 = " + proxy.multiply(10, 5));
System.out.println("10 / 5 = " + proxy.divide(10, 5));

// 测试除法异常
try {
System.out.println("10 / 0 = " + proxy.divide(10, 0));
} catch (Exception e) {
System.out.println("捕获到异常: " + e.getMessage());
}
}
}

使用动态代理优化

静态代理没有实现对日志的统一管理,实际上并没有进行多少的优化

image-20250422204359251

定义计算器接口和实现类都不变,我们添加一个日志处理器,实现 InvocationHandler 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 日志处理器,实现 InvocationHandler 接口
class LogHandler implements InvocationHandler {
private final Object target;

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

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
System.out.println("[日志] " + methodName + " 方法开始");
Object result = method.invoke(target, args);
System.out.println("方法内部Result: " + result);
System.out.println("[日志] " + methodName + " 方法结束");
return result;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 测试类
public class DynamicProxyExample {
public static void main(String[] args) {
// 创建目标对象
Calculator calculator = new CalculatorImpl();
// 创建日志处理器
LogHandler logHandler = new LogHandler(calculator);
// 创建代理对象
Calculator proxyCalculator = (Calculator) Proxy.newProxyInstance(
calculator.getClass().getClassLoader(),
calculator.getClass().getInterfaces(),
logHandler
);

// 调用代理对象的方法
proxyCalculator.add(2, 3);
proxyCalculator.subtract(5, 2);
proxyCalculator.multiply(3, 4);
proxyCalculator.divide(10, 2);
}
}

在上述动态代理的例子中,我们通过 InvocationHandler 实现了日志功能的增强。然而,当项目变得复杂时,手动编写代理逻辑会变得繁琐,并且难以维护。这时候,Spring AOP(面向切面编程)就可以发挥作用了。

Spring AOP 是 Spring 框架的一个重要特性,它允许我们在不修改原有代码的情况下,对程序进行增强。Spring 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
42
43
44
45
package edu.software.ergoutree.spring6aop.example;

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

public class ProxyFactory {
// 目标对象
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}

// 返回代理对象
public Object getProxy() {
/*
Proxy.newProxyInstance() 方法有三个参数
第一个参数 ClassLoader:加载动态生成代理类的加载器
第二个参数 class<?>[] interfaces:目标对象实现的所有接口的class类型的数组
第三个参数 InvocationHandler 设置代理对象实现目标对象方法的过程
*/
ClassLoader classLoader = target.getClass().getClassLoader();
Class<?>[] interfaces = target.getClass().getInterfaces();
InvocationHandler invocationHandler = new InvocationHandler() {
// 第一个参数 代理对象
// 第二个参数 需要重写的目标对象的方法
// 第三个参数 method方法里面的参数
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 方法调用前的日志输出
System.out.println("[动态代理日志] 开始执行 " + method.getName() + " 方法,参数: " +
java.util.Arrays.toString(args));

// 调用目标的方法
Object result = method.invoke(target, args);

// 方法调用后的日志输出
System.out.println("[动态代理日志] " + method.getName() + " 方法执行完毕,结果: " + result);
System.out.println("----------------------------------");
return result;
}
};
return Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
}
}

测试

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

public class CalcutorTestProxy {
public static void main(String[] args) {
Calculator target = new CalculatorImpl();
// 创建代理工厂
ProxyFactory proxyFactory = new ProxyFactory(target);

// 获取代理对象
Calculator proxy = (Calculator) proxyFactory.getProxy();

// 通过代理对象调用方法
System.out.println("===== 测试加法 =====");
int addResult = proxy.add(10, 5);
System.out.println("主程序获取的结果: " + addResult);

System.out.println("\n===== 测试减法 =====");
int subtractResult = proxy.subtract(10, 5);
System.out.println("主程序获取的结果: " + subtractResult);

System.out.println("\n===== 测试乘法 =====");
int multiplyResult = proxy.multiply(10, 5);
System.out.println("主程序获取的结果: " + multiplyResult);

System.out.println("\n===== 测试除法 =====");
int divideResult = proxy.divide(10, 5);
System.out.println("主程序获取的结果: " + divideResult);

// 测试异常情况
try {
System.out.println("\n===== 测试除法异常 =====");
proxy.divide(10, 0);
} catch (Exception e) {
System.out.println("捕获到异常: " + e.getClass().getName());
}
}

}

基于注解的 AOP

技术说明

动态代理分类:JDK动态代理 和 cglib动态代理

有接口,使用 JDK 动态代理,生成接口实现类的代理对象,代理对象和目标对象都实现同样的接口

无接口,使用 cglib 动态代理,生成子类的代理对象,继承目标类

image-20250423163635376

动态代理分为JDK动态代理和glib动态代理

当目标类有接口的情况使用JDK动态代理和cglib动态代理, 没有接口时只能使用cgib动态代理

JDK动态代理动态生成的代理类会在com.sun.proxy包下,类名为$proxy1,和目标类实现相同的接口●cglib动态代理动态生成的代理类会和目标在在相同的包下,会继承目标类

动态代理(InvocationHandler) : JDK原生的实现方式,需要被代理的目标类必须实现接口。因为这个技术要求*“代理对象和目标对象实现同样的接口** (兄弟两个拜把子模式)。

cglib: 通过继承被代理的目标类(认干爹模式)实现代理,所以不需要目标类实现接口。

AspectJ:是AOP思想的一种实现。本质上是静态代理,将代理逻辑”织入”被代理的目标类编译得到的字节码文件,所以最终效果是动态的。weaver就是织入器。 Spring只是借用了 AspectJ中的注解。

使用注解配置AOP

Spring AOP允许使用基于注解的方式实现AOP,这样做可以简化Spring配置文件的臃肿代码。

image-20250423163517236

步骤:

  1. 引入 AOP 相关依赖
  2. 创建目标资源
    • 接口
    • 实现类
  3. 创建切面类
    • 切入点
    • 通知类型

实例

所以先创建 LogAspect 切面类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
package edu.software.ergoutree.spring6aop.anoaop;

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

import java.util.Arrays;

// 切面类
@Component // IoC容器中进行管理
@Aspect // 表示是一个切面类
public class LogAspect {

// 定义切入点表达式,匹配Calculator接口的所有方法
@Pointcut("execution(* edu.software.ergoutree.spring6aop.anoaop.Calculator.*(..))")
public void calculatorPointcut() {}

// 设置切入点和通知类型
// 通知类型 前置 返回 异常 后置 环绕

// 前置 @Before(value="切入点表达式配置切入点")
// 单独写切入点表达式@Before(value="execution(public int edu.software.ergoutree.spring6aop.anoaop.CalculatorImpl.add(..))")
@Before("calculatorPointcut()")
public void beforeMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("Logger->前置通知,方法名" + methodName + " 参数 " + args);
}

// 后置通知(最终通知)
@After("calculatorPointcut()")
public void afterMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("Logger->后置通知,方法名" + methodName + " 参数 " + args);
}

// 返回通知
// 返回 @AfterReturning(value = "切入点表达式", returning = "增强的目标方法返回值(和方法中的Object一样)")
@AfterReturning(value = "calculatorPointcut()", returning = "result")
public void afterReturningMethod(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("Logger->返回通知,方法名" + methodName + " 参数 " + args + "目标方法的返回值" + result);
}


// 异常通知,目标方法出现了异常,这个通知会执行
// @AfterThrowing(value = "切入点表达式", throwing = "目标方法的异常信息")
@AfterThrowing(value = "calculatorPointcut()", throwing = "ex")
public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex) {
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("Logger->异常通知 方法 " + methodName + " 参数 " + args + " 执行抛出异常: " + ex.getMessage());
}

// 环绕通知
@Around("calculatorPointcut()")
public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();

System.out.println("Logger->环绕通知-前 方法 " + methodName + " 开始执行,参数: " + Arrays.toString(args));

try {
System.out.println("Logger->环绕通知-目标方法之前执行");
// 执行目标方法
Object result = joinPoint.proceed();
System.out.println("Logger->环绕通知-目标方法之后执行 方法 " + methodName + " 执行成功,结果: " + result);
return result;
} catch (Exception e) {
System.out.println("Logger->环绕通知-异常 方法 " + methodName + " 执行失败,异常: " + e.getMessage());
throw e;
} finally {
System.out.println("Logger->环绕通知-最终 方法 " + methodName + " 执行结束");
}
}

// 环绕通知另一个应用,计算执行时间
@Around("calculatorPointcut()")
public Object timingAround(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
try {
return joinPoint.proceed();
} finally {
long end = System.currentTimeMillis();
System.out.println("方法 " + joinPoint.getSignature().getName()
+ " 执行耗时: " + (end - start) + "ms");
}
}
}

关于切入点表达式

语法细节:

  • 号代替”权限修饰符”和“返回值”部分表示”权限修饰符”和“返回值”不限

  • 在包名的部分,一个*号只能代表包的层次结构中的一层,表示这一层是任意的。

  • 例如: *.Hello匹配com.Hello, 不匹配com.atguigu.Hello

  • 在包名的部分,使用 *..表示包名任意,包的层次深度任意

  • 在类名的部分,类名部分整体用 * 号代替,表示类名任意

  • 在类名的部分,可以使用 * 号代替类名的一部分

    • 例如: *Service匹配所有名称以Service结尾的类或接口
  • 在方法名部分,可以使用 * 号表示方法名任意

  • 在方法名部分,可以使用 * 号代替方法名的一部分

    • 例如: *Operation匹配所有方法名以Operation结尾的方法
  • 在方法参数列表部分,使用(..)表示参数列表任意

  • 在方法参数列表部分,使用(int,..)表示参数列表以一个int类型的参数开头

  • 在方法参数列表部分,基本数据类型和对应的包装类型是不一-样的

    • 切入点表达式中使用int和实际方法中Integer 是不匹配的
  • 在方法返回值部分,如果想要明确指定一个返回值类型, 那么必须同时写明楫限修饰符。

    • 例如: execution(public int ..Service,(..,int))正确
    • 例如: execution( int ..Service.*(..,int))错误

完整例子:

execution(访问修饰符 增强方法的返回类型 增强方法所在类全路径.方法名)

image-20250423165621177

关于 环绕通知

环绕通知的特点

  1. 功能最强大的通知类型:可以控制目标方法是否执行、何时执行、如何执行
  2. 唯一能控制方法执行的切入点:可以决定是否调用proceed()方法来执行目标方法
  3. 可以修改返回值:在方法执行前后对返回值进行处理
  4. 可以处理异常:捕获并处理目标方法抛出的异常

aroundMethod 的方法参数说明

需要注意的是,环绕通知必须接收一个类型为ProceedingJoinPoint的参数,返回值也必须是Object类型,且必须抛出异常

  • ProceedingJoinPoint:继承自JoinPoint,新增了proceed()方法
    • proceed():执行目标方法,返回目标方法的返回值
    • proceed(Object[] args):使用新参数执行目标方法

与其他通知的执行顺序

当环绕通知与其他通知共存时,执行顺序为:

  1. 环绕通知的前半部分
  2. 前置通知
  3. 目标方法
  4. 返回通知/异常通知
  5. 后置通知
  6. 环绕通知的后半部分

重用切入表达式

关于 @Pointcut

@Pointcut 是 Spring AOP 里的一个注解,其作用是定义一个切入点(Pointcut)

public void calculatorPointcut() {}

这是一个空方法,其作用是为切入点命名。通过这种方式,可以在其他通知注解(如 @Before@After 等)中引用这个切入点

创建 bean.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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="edu.software.ergoutree.spring6aop.anoaop">

</context:component-scan>

<!--开启Aspectj自动代理,为目标类自动生成代理-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>

当然也可以用注解类

1
2
3
4
5
6
7
8
9
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan("edu.software.ergoutree.spring6aop.anoaop")
@EnableAspectJAutoProxy // 启用AOP自动代理
public class AppConfig {
}

基于 XML 的AOP实现

因为Spring AOP的代理对象由IoC容器自动生成,所以开发者无须过多关注代理对象生成的过程,只需选择连接点、创建切面、定义切点并在XML文件中添加配置信息即可。

image-20250423174728711

配置切面

在Spring的配置文件中,配置切面使用的是<aop:aspect>元素,该元素会将一个已定义好的Spring Bean转换成切面Bean,因此,在使用<aop:aspect>元素之前,要在配置文件中先定义一个普通的Spring BeanSpring Bean定义完成后,通过<aop:aspect>元素的ref属性即可引用该 Bean。

配置<aop:aspect>元素时,通常会指定 id 和 ref 这两个属性 image-20250423174825779

配置切入点

在Spring的配置文件中,切入点是通过<aop:pointcut>元素来定义的,表示该切入点是全局切入点,它可被多个切面共享;当<aop:pointcut>元素作为<aop:aspect>元素的子元素时,表示该切入点只对当前切面有效。

在定义<aop:aspect>元素时,通常会指定 id 和 expression 这两个属性

属性名称 描述
id 用于指定切入点的唯一标识
expression 用于指定切入点关联的切入点表达式

配置通知

在Spring的配置文件中,使用<aop:aspect>元素配置了5种常用通知,如表所示,5种通知分别为前置通知、后置通知、环绕通知、返回通知和异常通知,<aop:aspect>元素的常用属性如表4所示。

image-20250423175046604

实例

文件结构

1
2
3
4
5
6
7
8
9
10
11
12
src/
├── main/
│ ├── java/
│ │ └── edu.software.ergoutree.spring6aop.xmlaop/
│ │ ├── Calculator.java # 接口
│ │ ├── CalculatorImpl.java # 实现类
│ │ └── LogAspect.java # 切面类
│ └── resources/
│ └── bean-aopxml.xml # XML配置
└── test/
└── java/
└── CalculatorAOPTest.java # 测试类

导入AspectJ框架相关JAR包的依赖,在pom.xml中添加的代码如下:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.1</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>

计算器的接口和实现类依旧用上面的那个

切面类,定义了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
// LogAspect.java
package edu.software.ergoutree.spring6aop.xmlaop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;

public class LogAspect {

// 前置通知
public void beforeAdvice(JoinPoint joinPoint) {
System.out.println("[前置] 方法名: " + joinPoint.getSignature().getName());
}

// 后置通知(无论是否异常都会执行)
public void afterAdvice(JoinPoint joinPoint) {
System.out.println("[后置] 方法执行完毕");
}

// 返回通知
public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
System.out.println("[返回] 结果: " + result);
}

// 异常通知
public void afterThrowingAdvice(JoinPoint joinPoint, Exception ex) {
System.out.println("[异常] 错误信息: " + ex.getMessage());
}

// 环绕通知
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("[环绕前] 参数: " + pjp.getArgs()[0] + ", " + pjp.getArgs()[1]);
Object result = pjp.proceed(); // 执行目标方法
System.out.println("[环绕后] 结果: " + result);
return result;
}
}

xml 配置 bean-aopxml.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
<?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="edu.software.ergoutree.spring6aop.xmlaop"/>

<!-- 定义切面Bean -->
<bean id="logAspect" class="edu.software.ergoutree.spring6aop.xmlaop.LogAspect"/>

<!-- AOP配置 -->
<aop:config>
<aop:aspect ref="logAspect">
<!-- 定义切入点表达式 -->
<aop:pointcut id="calculatorMethods"
expression="execution(* edu.software.ergoutree.spring6aop.xmlaop.Calculator.*(..))"/>

<!-- 五种通知绑定到切入点 -->
<aop:before method="beforeAdvice" pointcut-ref="calculatorMethods"/>
<aop:after method="afterAdvice" pointcut-ref="calculatorMethods"/>
<aop:after-returning method="afterReturningAdvice" pointcut-ref="calculatorMethods" returning="result"/>
<aop:after-throwing method="afterThrowingAdvice" pointcut-ref="calculatorMethods" throwing="ex"/>
<aop:around method="aroundAdvice" pointcut-ref="calculatorMethods"/>
</aop:aspect>
</aop:config>

<!-- 显式声明Calculator类型的Bean -->
<bean id="calculator" class="edu.software.ergoutree.spring6aop.xmlaop.CalculatorImpl"/>
</beans>

测试

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
package edu.software.ergoutree.spring6aop;

import edu.software.ergoutree.spring6aop.xmlaop.Calculator;
import edu.software.ergoutree.spring6aop.xmlaop.CalculatorImpl;
import org.junit.jupiter.api.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class CalculatroAOPTest {
@Test
public void Test1() {
try (ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("bean-aopxml.xml")) {

CalculatorImpl calculator = context.getBean("calculator", CalculatorImpl.class);

// 正常测试
System.out.println("=== 正常测试 ===");
calculator.add(5, 3);
calculator.divide(6, 2);

// 异常测试(除数为0)
System.out.println("\n=== 异常测试 ===");
try {
calculator.divide(6, 0);
} catch (ArithmeticException ignored) {}
}
}
}