Spring6 - AOP
# 1. 场景模拟
搭建子模块:Spring6-aop
# 声明计算器接口
这是定义基本算术操作的接口。包括加、减、乘、除四种操作。
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);
}
2
3
4
5
6
# 创建计算器实现类
这个类实现了Calculator
接口,提供了基本算术操作的实现。它专注于核心的算术逻辑。
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;
}
}
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
# 创建带日志功能的计算器实现类
此类同样实现了Calculator
接口,但添加了日志记录功能,展示了在每个方法开始和结束时的参数和结果。
public class CalculatorLogImpl 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;
}
}
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
# 提出问题
针对带日志功能的实现类,我们发现有如下缺陷:
- 对核心业务功能有干扰,导致程序员在开发核心业务功能时分散了精力
- 附加功能分散在各个业务功能方法中,不利于统一维护
解决思路
解决这两个问题,核心就是:解耦。我们需要把附加功能从业务功能代码中抽取出来。
困难
解决问题的困难:要抽取的代码在方法内部,靠以前把子类中的重复代码抽取到父类的方式没法解决。所以需要引入新的技术。
# 2. 代理模式
# 概念
介绍
二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类 间接 调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来—— 解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。
使用代理后:
生活中的代理
- 广告商找大明星拍广告需要经过经纪人
- 合作伙伴找大老板谈合作要约见面时间需要经过秘书
- 房产中介是买卖双方的代理
相关术语
- 代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。
- 目标:被代理「套用」了非核心逻辑代码的类、对象、方法。
# 静态代理
创建静态代理类:这个类实现了Calculator
接口并包含一个对目标对象的引用。代理类中的每个方法都会在执行目标对象的相应方法前后添加日志记录的功能。
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;
}
// 此处省略 sub, mul, div 等其他方法的实现,它们也将包含类似的日志功能
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
静态代理的局限性
静态代理实现了基本的功能解耦,但存在以下问题:
- 重复代码:对于每一种不同的代理需求(例如日志、性能监控等),都需要创建一个新的代理类。这导致大量重复的代码。
- 扩展性差:如果接口增加新的方法,静态代理类也需要添加相应的实现,这降低了代码的可维护性。
- 灵活性低:静态代理类的逻辑是硬编码的,对于不同的方法可能需要不同的处理逻辑,这使得静态代理的适应性受限。
进一步的需求:使用动态代理
为了解决静态代理的局限性,我们可以考虑使用动态代理技术。动态代理允许在运行时动态地创建代理对象,并动态地将调用委派给目标对象,同时在委派之前或之后执行附加的操作(如日志记录)。这样,我们可以:
- 减少重复代码:不需要为每一种代理功能编写单独的代理类。
- 提高灵活性:可以根据需要动态地为不同的方法或对象添加不同的代理行为。
- 简化维护:当接口变更时,无需修改代理类,动态代理可以自动适应新的方法。
动态代理主要有两种实现方式:
- JDK动态代理:使用
java.lang.reflect.Proxy
类,要求目标对象必须实现一个或多个接口。 - CGLIB动态代理:使用字节码增强技术生成目标类的子类,适用于没有实现接口的类。
# JDK动态代理实现
生产代理对象的工厂类: 此工厂类用于生成动态代理对象。它接受任何目标对象,并为其创建一个代理,该代理可以拦截所有方法调用,以实现横切关注点,例如日志记录。
public class ProxyFactory {
// 目标对象,代理类将为此对象添加额外的功能(如日志)
private Object target;
// 构造器,注入目标对象
public ProxyFactory(Object target) {
this.target = target;
}
public Object getProxy(){
// 获取目标对象的类加载器,用于加载代理类
ClassLoader classLoader = target.getClass().getClassLoader();
// 获取目标对象实现的接口,代理类将实现这些接口
Class<?>[] interfaces = target.getClass().getInterfaces();
// 创建调用处理器,控制对目标方法的访问
InvocationHandler invocationHandler = new InvocationHandler() {
@Override
/**
* proxy:代理对象
* method:代理对象需要实现的方法,即其中需要重写的方法
* args:method所对应方法的参数
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
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;
}
};
// 使用Proxy类的静态方法newProxyInstance创建代理对象
/**
* newProxyInstance():创建一个代理实例
* 其中有三个参数:
* 1、classLoader:加载动态生成的代理类的类加载器
* 2、interfaces:目标对象实现的所有接口的class对象所组成的数组
* 3、invocationHandler:设置代理对象实现目标对象方法的过程,即代理类中如何重写接口中的抽象方法
*/
return Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
}
}
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
# JDK动态代理的测试
这个测试演示了如何使用ProxyFactory
来创建一个Calculator
的代理对象,该对象在执行除法操作时添加了日志记录功能。
@Test
public void testDynamicProxy(){
// 创建ProxyFactory实例,传入一个CalculatorLogImpl对象作为目标对象
ProxyFactory factory = new ProxyFactory(new CalculatorLogImpl());
// 获取动态生成的代理对象,转型为Calculator接口
Calculator proxy = (Calculator) factory.getProxy();
// 调用代理对象的div方法,由于存在动态代理,将自动进行日志记录
proxy.div(1,0); // 故意引发除以零的异常来观察日志输出
// proxy.div(1,1); // 正常情况
}
2
3
4
5
6
7
8
9
10
# 如何确定自己的AOP是采用jdk还是CGLIB实现的动态代理
在Spring框架中,AOP的底层可以通过JDK动态代理或者CGLIB来实现。选择哪种实现方式主要取决于目标对象是否实现了接口以及Spring配置。下面是如何确定Spring AOP使用的是JDK动态代理还是CGLIB的几个关键因素:
# 1. 接口的实现
- JDK动态代理:如果被代理的目标对象实现了至少一个接口,Spring默认使用JDK动态代理。JDK代理主要依赖于接口,代理类会实现目标对象的所有接口,并在调用时通过反射机制调用实现。
- CGLIB:如果目标对象没有实现任何接口,Spring会回退使用CGLIB生成代理。CGLIB通过扩展目标类的方式来创建代理对象,不需要目标对象实现接口。
# 2. 明确指定代理方式
你可以在Spring的配置中显式指定使用哪种代理方式:
- 通过Java配置:可以使用
@EnableAspectJAutoProxy(proxyTargetClass = true)
来强制使用CGLIB代理,即使目标对象实现了接口。如果设置为false
或不设置proxyTargetClass
属性(默认为false
),Spring将优先使用JDK动态代理。 - 通过XML配置:可以在XML配置中使用
<aop:config proxy-target-class="true">
来启用CGLIB代理。
# 3. 运行时检查
在运行时,你可以通过检查代理对象的类型来确定使用的是哪种代理技术:
// 获取代理对象的Class信息
Class<?> proxyClass = proxy.getClass();
// 检查代理类是否是JDK动态代理
boolean isJdkProxy = Proxy.isProxyClass(proxyClass);
// 检查代理类是否是CGLIB代理
boolean isCglibProxy = proxyClass.getName().contains("CGLIB");
System.out.println("Is JDK dynamic proxy: " + isJdkProxy);
System.out.println("Is CGLIB proxy: " + isCglibProxy);
2
3
4
5
6
7
8
9
10
11
# 4. 检查代理实例
还可以通过分析代理实例来确定代理的类型。如果代理实例实现了org.springframework.aop.framework.AopProxy
接口,你可以通过其API来判断是使用了JDK代理还是CGLIB代理。
通过以上方法,你可以明确判断出在Spring AOP中,代理是通过JDK动态代理实现的还是通过CGLIB实现的。这可以帮助进行更精确的性能优化和问题诊断。
# 3. AOP 概念及相关术语
想要知道Spring AOP,就得先了解AOP
AOP是面向切面编程,是一种思想,是对某一类事情的集中处理,其核心思想是将那些与业务逻辑无关,但是被多处业务逻辑模块共享的代码(比如日志管理,权限检查,事务管理等)抽取出来,通过预编译方式和运行期动态代理实现程序功能的统一维护的方式。这样,开发者可以将更多的精力放在处理核心业务逻辑上。
# 为什么要使用AOP?
对一些功能统一,使用较多,我们就可以考虑使用AOP思想进行统一处理,如登录校验,使得我们不用在每一处需要做登录校验的地方进行相同逻辑的代码实现了(下面详细解释)
- 在一个应用程序中,可能有很多操作都需要在执行前验证用户是否已经登录。如果不使用AOP,你可能需要在每个需要验证的方法中都写一段校验代码。这不仅使得代码重复,而且如果以后需要更改验证逻辑,就需要去修改每一个方法中的代码。
- 但是,如果使用了AOP,你就可以将校验逻辑抽取出来定义成一个切面,然后通过配置,让这个切面在需要验证的方法执行前运行。这样,你就只需要在一个地方编写和维护验证逻辑,大大提高了代码的可维护性和可读性。
除了登录校验,AOP还可以用在这些地方:
- 统一日志记录
- 统一方法执行时间统计
- 统一返回格式
- 统一异常处理
- 事务开启和提交
# 横切关注点
分散在每个各个模块中解决同一样的问题,如用户验证、日志管理、事务处理、数据缓存都属于横切关注点。
从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。
这个概念不是语法层面的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点。
# 切面(类)
- 切面是一个封装横切关注点逻辑的模块。它由通知(Advice)和切点(Pointcut)组成。
- 通知定义了横切关注点的具体行为,即在何时(事件时机),何地(执行环境)以及如何执行。
- 切点则定义了通知应用的位置,即哪些连接点(Join point)应当被通知影响。
作用
简化代码:把方法中固定位置的重复的代码 抽取 出来,让被抽取的方法更专注于自己的核心功能,提高内聚性
代码增强:把特定的功能封装到切面类中,看哪里有需要,就往上套,被 套用 了切面逻辑的方法就被切面给增强了
# 连接点(能够插入切面的点)
程序在运行过程中能够插入切面的点。例如,方法调用、异常抛出等。Spring只支持方法级的连接点。一个类的所有方法前、后、抛出异常时等都是连接点。
我们可以把方法排成一排,每一个横切位置看成 x 轴方向,把方法从上到下执行的顺序看成y轴,x 轴和 y 轴的交叉点就是连接点。通俗说,就是 Spring 允许你使用通知的地方。
# 切点 (定义通知应该切入到哪些连接点上)
- 用于定义通知应该切入到哪些连接点上。不同的通知通常需要切入到不同的连接点上,这种精准的匹配是由切入点的正则表达式来定义的,比如
execution(* com.spring.service.impl.*.*(..))
。
# 通知 (具体要增强的功能实现)
增强,通俗说,就是你想要增强的功能,比如 安全,事务,日志等。
每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。
Spring 切⾯类中,可以在⽅法上使⽤以下注解,会设置⽅法为通知⽅法,在满⾜条件后会通知本⽅法进⾏调⽤
- 前置通知@Before:这个注解标注的方法会在目标方法(实际要执行的方法)被调用前执行
- 后置通知@After:这个注解标注的方法会在目标方法完成后执行,无论目标方法是否成功完成。
- 环绕通知@Around:这个注解标注的方法会在目标方法调用前后都执行,可以自行决定何时执行目标方法。
- 异常通知@AfterThrowing:这个注解标注的方法会在目标方法抛出异常后执行。
- 方法返回通知@AfterReturning:这个注解标注的方法会在目标方法成功返回后执行
# 4. 基于注解的 AOP
- 动态代理分为 JDK 动态代理和 cglib 动态代理
- 当目标类有接口的情况使用 JDK 动态代理和 cglib 动态代理,没有接口时只能使用 cglib 动态代理
- JDK 动态代理动态生成的代理类会在
com.sun.proxy
包下,类名为$proxy1
,和目标类实现相同的接口 - cglib 动态代理动态生成的代理类会和目标在在相同的包下,会继承目标类
- 动态代理(InvocationHandler):JDK 原生的实现方式,需要被代理的目标类必须实现接口。因为这个技术要求 代理对象和目标对象实现同样的接口(兄弟两个拜把子模式)
- cglib:通过 继承被代理的目标类(认干爹模式)实现代理,所以不需要目标类实现接口
- AspectJ:是 AOP 思想的一种实现。本质上是静态代理,将代理逻辑「织入」被代理的目标类编译得到的字节码文件,所以最终效果是动态的。weaver 就是织入器。Spring 只是借用了 AspectJ 中的注解
# 1. 引入依赖
为了使用Spring AOP,我们需要在pom.xml
文件中添加几个关键的依赖项。以下是必需的依赖,包括Spring的上下文管理、AOP支持、单元测试库以及日志管理。
<dependencies>
<!-- Spring context 依赖,提供Spring的基础功能 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.2</version>
</dependency>
<!-- Spring AOP依赖,提供面向切面编程的支持 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>6.0.2</version>
</dependency>
<!-- Spring aspects 依赖,提供与AspectJ的集成 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.0.2</version>
</dependency>
<!-- JUnit 5 测试框架 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.3.1</version>
</dependency>
<!-- Log4j2 依赖,提供日志记录功能 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.19.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>2.19.0</version>
</dependency>
</dependencies>
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
对于使用Spring Boot的项目,可以通过添加spring-boot-starter-aop
依赖来简化配置,这个starter包含了AOP的所有必要依赖。
添加 SpringBoot AOP 框架支持.➡️中央仓库链接 (opens new window)
<!-- Spring Boot AOP starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.7.11</version>
</dependency>
2
3
4
5
6
# 2. 定义目标资源
定义一个计算器接口Calculator
和一个实现这个接口的类CalculatorImpl
。实现类用@Component
注解标记,以确保Spring可以通过组件扫描自动注册它。
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);
}
@Component
public class CalculatorImpl implements Calculator {
@Override
public int add(int i, int j) {
int result = i + j;
System.out.println("方法内部 result = " + result);
return result;
}
// 省略其他方法以节省空间,但实现相似...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 3. 创建切面类并配置
定义一个切面类LogAspect
,使用@Aspect
和@Component
注解,确保它既被视为一个切面也被Spring容器管理。在切面类中定义各种类型的通知,以便在目标方法执行的不同阶段插入日志记录代码。
@Aspect
@Component
public class LogAspect {
@Before(value = "execution(* com.springaop.springaopdemo.*.*.*(..))")
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args);
}
@After(value = "execution(* com.springaop.springaopdemo.*.*.*(..))")
public void afterMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->后置通知,方法名:"+methodName);
}
@AfterReturning(value = "execution(* com.springaop.springaopdemo.*.*.*(..))", returning = "result")
public void afterReturningMethod(JoinPoint joinPoint, Object result){
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->返回通知,方法名:"+methodName+",结果:"+result);
}
@AfterThrowing(value = "execution(* com.springaop.springaopdemo.*.*.*(..))", throwing = "ex")
public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex){
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->异常通知,方法名:"+methodName+",异常:"+ex);
}
@Around(value = "execution(* com.springaop.springaopdemo.*.*.*(..))")
public Object aroundMethod(ProceedingJoinPoint joinPoint){
Object result = null;
try {
System.out.println("环绕通知-->目标对象方法执行之前");
result = joinPoint.proceed();
System.out.println("环绕通知-->目标对象方法返回值之后");
} catch (Throwable throwable) {
System.out.println("环绕通知-->目标对象方法出现异常时");
} finally {
System.out.println("环绕通知-->目标对象方法执行完毕");
}
return result;
}
}
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
ProceedingJoinPoint
是 AOP(面向切面编程)中与环绕通知(@Around
)相关的一个特殊对象,它是 JoinPoint
的一个扩展,用于环绕通知中。它允许控制何时执行目标方法,以及如何处理方法的返回值或异常。这个对象主要用于 Spring AOP 中,为切面提供了强大的控制能力。
proceed()
:- 这是最核心的方法,用于执行目标方法。它实际上允许你决定是否继续执行连接点(即被拦截的方法)或直接返回自己的返回值或抛出异常。
- 它可以直接调用,
joinPoint.proceed()
,这意味着执行原方法。 - 它也可以传递新的参数,
joinPoint.proceed(new Object[]{arg1, arg2, ...})
,这将使用提供的参数而非原始参数调用方法。
getArgs()
:- 返回方法执行时的参数数组。可以用这些参数信息进行日志记录、修改参数值或进行验证。
getSignature()
:- 返回被调用的方法的签名信息(如方法名、返回类型和所属类),这对于日志记录或权限验证等方面特别有用。
getTarget()
:- 返回目标对象(包含被调用的方法的对象)。这允许你访问目标对象的属性和其他方法。
getThis()
:- 返回代理对象本身,这与
getTarget()
方法返回的对象可能不同,尤其是当代理对象与目标对象不是同一个时(例如使用JDK代理时)。
- 返回代理对象本身,这与
# 4. 配置Spring XML
在Spring的XML配置文件中,启用组件扫描和AOP的自动代理,确保所有标记有@Aspect
注解的类都被视为切面,并为所有合适的Bean生成代理。
<?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
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<context:component-scan base-package="com.springaop.springaopdemo.service"></context:component-scan>
<aop:aspectj-autoproxy />
</Beans>
2
3
4
5
6
7
8
9
10
11
12
13
14
注意
如果是在Spring Boot项目中使用AOP,不需要使用Spring XML配置文件来设置AOP或其他组件。Spring Boot支持全自动配置,通常只需要添加适当的依赖并通过注解进行配置即可。这样可以大大简化配置过程,让你能够更快速地开发应用。
# 5. 执行测试
创建一个测试类CalculatorTest
,通过Spring容器获取计算器Bean,并执行add
方法来验证切面是否正确工作。
Spring写法
@SpringBootTest
public class CalculatorTest {
private Logger logger = LoggerFactory.getLogger(CalculatorTest.class);
@Test
public void testAdd(){
ApplicationContext ac = new ClassPathXmlApplicationContext("Beans.xml");
Calculator calculator = ac.getBean(Calculator.class);
int add = calculator.add(1, 1);
logger.info("执行成功:"+add);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
SpringBoot写法
@SpringBootTest
public class CalculatorTest {
@Resource
private CalculatorImpl calculator;
private Logger logger = LoggerFactory.getLogger(CalculatorTest.class);
@Test
public void testAdd(){
int add = calculator.add(1, 1);
logger.info("执行成功:"+add);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 6. 各种通知的类型和作用
- 前置通知 (@Before)
- 在目标方法执行之前执行,无论目标方法最后是否成功完成。
- 用于执行如安全检查、日志记录等准备工作。
- 返回通知 (@AfterReturning)
- 在目标方法成功执行之后执行,可以访问到方法的返回值。
- 用于执行如修改返回值、统计方法执行性能等操作。
- 异常通知 (@AfterThrowing)
- 在目标方法抛出异常后执行,可以访问到异常对象,可用于处理错误管理逻辑。
- 用于回滚操作、异常报告等。
- 后置通知 (@After)
- 在目标方法执行之后执行,无论方法最终是否成功,类似于finally块。
- 用于释放资源、清理数据等。
- 环绕通知 (@Around)
- 环绕通知可以在调用目标方法之前和之后执行自定义行为,还可以决定是否调用目标方法。
- 允许控制方法调用,是最强大的通知类型。
通知的执行顺序,对于Spring版本5.3.x以前和5.3.x以后,通知的执行顺序略有不同:
- Spring 5.3.x 以前:
- 前置通知
- 目标操作
- 后置通知
- 返回通知或异常通知
- Spring 5.3.x 以后:
- 前置通知
- 目标操作
- 返回通知或异常通知
- 后置通知
# 7. 切入点表达式语法
作用
Aspect 语法中的通配符
- 星号 (*):
- 表示匹配任意数量的字符,可以用在方法返回类型、包名、类名或方法名上。
- 例子:
execution(* set*(..))
匹配所有以"set"开头的方法。
- 两点 (..):
- 在方法参数中使用,表示匹配任意数量和类型的参数。
- 在包名中使用时,必须与星号 (*) 一起使用,表示匹配指定包及其所有子包。
- 例子:
execution(* com.example..*.*(..))
匹配com.example
包及其所有子包中的所有方法。
- 加号 (+):
- 用于类型匹配,表示匹配指定类及其所有子类。
- 例子:
execution(* com.example.Car+.*(..))
匹配Car
类及其所有子类的所有方法。
AspectJ 语法(Spring AOP 切点的匹配方法)
切点表达式由切点函数组成,其中 execution() 是最常⽤的切点函数,⽤来匹配⽅法,语法为:
execution(<修饰符><返回类型><包.类.⽅法(参数)><异常>)
其中修饰符和异常可以省略,下面是具体含义:
- 修饰符(一般省略):public(公共方法),*(任意)
- 返回类型(不能省略):void,String,int,*(任意)
- 包:com.demo(固定包),com.*(com包下所有),com.demo..(com.demo包下所有子包含自己)
- 类:Test(固定类),Test*(以之开头),test(以之结尾),(任意)
- 方法名(不能省略):addUser(固定方法),add*(以add开头),add(以add结尾),(任意)
- 参数:(),(int),(int,String),(..)任意参数
- 异常(可省略,一般不写)
示例表达式
匹配特定包下的所有方法:
execution(* com.example.service.*.*(..))
1匹配任意公共方法:
execution(public * *(..))
1匹配特定包及其子包下的所有方法:
execution(* com.example..*.*(..))
1匹配所有返回整型的方法:
execution(int *..*.*(..))
1匹配特定类中以'save'开头的方法:
execution(* com.example.service.MyService.save*(..))
1
# 8. 复用切入点表达式
在Spring AOP中,切入点表达式定义了在何处应用通知(advice),但重复编写相同的切入点表达式会使代码变得冗长且难以维护。为了解决这个问题,Spring AOP允许我们通过@Pointcut
注解声明一个切入点,这样可以在一个或多个通知中重用这个切入点。下面是如何声明和使用切入点的详细说明。
声明切入点
你可以在任何@Aspect
注解的类中声明切入点。使用@Pointcut
注解来指定要匹配的方法执行表达式。
// 声明一个名为pointCut的切入点
@Pointcut("execution(* com.springaop.springaopdemo.*.*(..))")
public void pointCut() {
// 这个方法的内容实际上不重要,它的作用主要是作为切入点表达式的载体
}
2
3
4
5
在同一个切面中使用切入点
在同一个切面中,可以直接通过方法名引用切入点。
// 使用声明的切入点来定义一个前置通知
@Before("pointCut()")
public void beforeMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
// 输出日志信息,包括方法名和参数
System.out.println("Logger-->前置通知,方法名:" + methodName + ",参数:" + args);
}
2
3
4
5
6
7
8
在不同切面中使用切入点
如果你想在不同的切面中使用已声明的切入点,你需要使用全路径来引用它,这包括包名、类名和切入点方法名。
// 在另一个切面中引用已声明的切入点
@Before("cn.youngkbt.aop.CommonPointCut.pointCut()")
public void beforeMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
// 和前面的例子一样,输出方法名和参数
System.out.println("Logger-->前置通知,方法名:" + methodName + ",参数:" + args);
}
2
3
4
5
6
7
8
总结
通过使用@Pointcut
注解,我们可以把特定的切入点表达式抽象化并集中管理,这有几个好处:
- 减少重复:避免在多个通知中重复相同的切入点表达式。
- 提高可维护性:当切入点表达式需要更改时,你只需更改切入点声明,而不用修改每一个使用这一表达式的通知。
- 增强可读性:通过为切入点提供有意义的名称,可以使得切面的配置更易于理解。
# 9. 获取连接点的相关信息
在Spring AOP中,通知(Advice)是围绕连接点(Join Point)执行的动作,它可以在方法执行前、后、返回或抛出异常时进行一些操作。获取连接点的相关信息对于日志记录、事务管理、安全检查等功能至关重要。下面总结了如何在Spring AOP中获取连接点的信息以及如何使用各种类型的通知。
# 9.1 获取连接点信息
连接点信息通过注入JoinPoint
或ProceedingJoinPoint
(仅限于环绕通知)到通知方法中来获取。这些类提供了关于当前执行方法的详细信息。
示例:前置通知中获取方法信息
在前置通知中,可以通过JoinPoint
获取方法的名称和参数。
@Before("execution(* com.springaop.springaopdemo.*.*.*(..))")
public void logMethodCall(JoinPoint joinPoint) {
// 获取方法参数
Object[] args = joinPoint.getArgs();
// 获取方法签名
Signature signature = joinPoint.getSignature();
// 获取目标对象
Object target = joinPoint.getTarget();
// 获取代理对象
Object proxy = joinPoint.getThis();
// 记录日志
System.out.println("Calling method: " + signature.toLongString());
System.out.println("Arguments: " + Arrays.toString(args));
System.out.println("Target object: " + target.getClass().getName());
System.out.println("Proxy class: " + proxy.getClass().getName());
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
JoinPoint
是一个核心接口,在 Spring AOP 和 AspectJ 中用于表示程序执行的某个特定位置。它主要用于获取有关当前执行点的详细信息,如方法调用或异常抛出的位置。这个接口在编写切面时非常有用,因为它允许切面逻辑访问到正在被通知(advice)的方法的详细信息,例如方法名称、参数、被执行的对象等。
JoinPoint 的主要方法和使用:
getArgs()
:- 返回一个包含传递给方法的参数的对象数组。这可以用来记录参数值或者进行参数校验。
getSignature()
:- 返回一个
Signature
对象,提供了对正在被调用的方法的签名的访问。它可以用来获取方法名称、返回类型和所在类。 toShortString()
,toLongString()
,toString()
是Signature
的方法,提供了不同格式的签名表示。
- 返回一个
getTarget()
:- 返回目标对象,即包含连接点的方法的对象。这可以用于访问目标对象的属性或其他方法。
getThis()
:- 返回代理对象,即正在执行的对象。这可能与
getTarget()
返回的对象相同或不同,依赖于代理的类型和配置。
- 返回代理对象,即正在执行的对象。这可能与
toString()
:- 提供了连接点的字符串表示,通常包括方法签名和调用位置。
# 9.2 获取目标方法的返回值和异常
返回通知
在返回通知中,可以通过指定@AfterReturning
注解的returning
属性来接收方法的返回值。
@AfterReturning(value = "execution(* com.springaop.springaopdemo.*.*.*(..))", returning = "result")
public void afterReturningMethod(JoinPoint joinPoint, Object result) {
// 方法名称
String methodName = joinPoint.getSignature().getName();
// 日志记录方法返回值
System.out.println("Logger-->返回通知,方法名:" + methodName + ",结果:" + result);
}
2
3
4
5
6
7
异常通知
使用@AfterThrowing
注解可以捕获方法抛出的异常。通过throwing
属性将异常信息绑定到通知方法的参数。
@AfterThrowing(value = "execution(* com.springaop.springaopdemo.*.*.*(..))", throwing = "ex")
public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex) {
// 获取方法名
String methodName = joinPoint.getSignature().getName();
// 日志记录异常信息
System.out.println("Logger-->异常通知,方法名:" + methodName + ",异常:" + ex);
}
2
3
4
5
6
7
环绕通知的使用
环绕通知是Spring AOP中最强大的通知类型,因为它可以完全控制方法的执行过程。
@Around("execution(* com.springaop.springaopdemo.*.*.*(..))")
public Object aroundMethod(ProceedingJoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
Object result = null;
try {
// 方法执行前的逻辑
System.out.println("环绕通知-->目标对象方法执行之前");
// 执行目标方法
result = joinPoint.proceed();
// 方法返回后的逻辑
System.out.println("环绕通知-->目标对象方法返回值之后");
} catch (Throwable throwable) {
throwable.printStackTrace();
System.out.println("环绕通知-->目标对象方法出现异常时");
} finally {
System.out.println("环绕通知-->目标对象方法执行完毕");
}
return result;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
这里的ProceedingJoinPoint
是JoinPoint
的子类,它允许控制何时执行目标方法。proceed()
方法是关键,它实际上触发了目标方法的执行。
# 10. 切面的优先级
在Spring AOP中,当多个切面同时作用于同一个连接点(如一个方法)时,切面的执行顺序变得非常重要。Spring提供了一种方式来控制这些切面的执行顺序,确保它们按照预定的方式相互嵌套或者执行。这通过使用@Order
注解来实现,它帮助定义切面的优先级。
切面的执行顺序和嵌套
切面的执行顺序决定了它们是如何相互嵌套的。例如,对于两个切面A和B:
- 如果A的优先级高于B,A的前置通知会先于B的前置通知执行,而B的后置通知会在A的后置通知之前执行。这意味着A切面在B切面的外层执行。
- 相反,如果A的优先级低于B,A的前置通知将在B的前置通知之后执行,而B的后置通知将在A的后置通知之后执行。这表明A切面在B切面的内层执行。
使用@Order
注解控制优先级
@Order
注解可以应用于任何被@Aspect
注解标记的切面类,来指定这个切面相对于其他切面的优先级。@Order
注解接受一个整数值,数值越小,优先级越高。
假设我们有两个切面,一个是日志记录切面(LoggingAspect),另一个是事务处理切面(TransactionAspect):
@Aspect
@Order(1) // 优先级高,数值小
@Component
public class LoggingAspect {
// 切面的实现代码
}
@Aspect
@Order(2) // 优先级低,数值大
@Component
public class TransactionAspect {
// 切面的实现代码
}
2
3
4
5
6
7
8
9
10
11
12
13
在这个例子中,LoggingAspect
的优先级高于TransactionAspect
,这意味着日志记录的操作会包裹事务处理的操作,即日志记录的前置通知会在事务处理的前置通知之前执行,而日志记录的后置通知将在事务处理的后置通知之后执行。
优先级的具体影响
- 前置通知:高优先级切面的前置通知先执行。
- 后置通知:低优先级切面的后置通知先执行。
- 返回通知和异常通知:这些通知的执行顺序也会受到优先级的影响,类似于后置通知。
通过合理地设置切面的优先级,可以有效地控制多个切面对同一连接点的影响顺序,确保程序的执行逻辑符合预期,这对于保证程序行为的正确性和可预测性至关重要。
# 5. 基于XML的AOP
在Spring框架中,基于XML的AOP配置提供了一种灵活的方法来定义切面、切入点和通知,而不使用注解。这种方式特别适合于需要将配置与业务逻辑代码分离的场景。下面详细介绍如何在Spring AOP中使用XML配置来实现切面编程。
# 准备工作
在开始之前,你需要确保项目中已经包含了Spring AOP的依赖,并且项目配置了Spring的类路径扫描,以确保能够找到相关的组件。
# 基于XML的AOP配置详解
在XML文件中,你可以通过<aop:config>
元素来配置AOP的相关设置。以下是具体的配置项和它们的功能:
<!-- 开启组件扫描,让Spring自动检测并注册项目中的Bean -->
<context:component-scan base-package="com.springaop.springaopdemo.aop.xml"></context:component-scan>
<!-- AOP配置开始 -->
<aop:config>
<!-- 配置切面类,ref属性指向Spring管理的Bean -->
<aop:aspect ref="loggerAspect">
<!-- 定义切入点,id为该切入点的唯一标识,expression定义匹配的方法 -->
<aop:pointcut id="pointCut"
expression="execution(* com.springaop.springaopdemo.aop.xml.CalculatorImpl.*(..))"/>
<!-- 前置通知:在目标方法执行之前执行 -->
<aop:before method="beforeMethod" pointcut-ref="pointCut"></aop:before>
<!-- 后置通知:在目标方法执行后执行,无论方法是否成功完成 -->
<aop:after method="afterMethod" pointcut-ref="pointCut"></aop:after>
<!-- 返回通知:在目标方法成功执行后执行,可以访问方法的返回值 -->
<aop:after-returning method="afterReturningMethod" returning="result" pointcut-ref="pointCut"></aop:after-returning>
<!-- 异常通知:在目标方法抛出异常时执行 -->
<aop:after-throwing method="afterThrowingMethod" throwing="ex" pointcut-ref="pointCut"></aop:after-throwing>
<!-- 环绕通知:围绕目标方法执行,可以在方法执行前后添加自定义行为 -->
<aop:around method="aroundMethod" pointcut-ref="pointCut"></aop:around>
</aop:aspect>
</aop:config>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 组件解释
<context:component-scan>
: 这个标签用于指定Spring在初始化时应该扫描哪些包,以便发现和注册Bean。<aop:config>
: 这是所有AOP配置的根元素。<aop:aspect>
: 定义一个切面,ref
属性引用一个定义在Spring容器中的bean。<aop:pointcut>
: 定义一个切入点,用于指定切面应当应用的方法。<aop:before>
,<aop:after>
,<aop:after-returning>
,<aop:after-throwing>
,<aop:around>
: 定义不同类型的通知,并关联到前面定义的切入点。
总结
使用XML配置AOP的主要优点是集中管理和配置灵活性。通过这种方式,开发者可以在不修改源代码的情况下,通过更改配置文件来调整切面逻辑。这种分离也使得代码更易于理解和维护,特别是在大型项目中,将业务逻辑与横切关注点(如日志和安全)明确分离。
# 6. Spring AOP 的实现原理
由于Spring AOP 的实现建在动态代理基础上的, Spring 对 AOP 的支持局限于方法级别的拦截.
动态代理呢就是当调用者调用目标对象的时候,它不会与目标对象接触,而是由代理类进行调用
# 织入(Weaving)
将切面应用到目标对象从而创建一个新的代理对象的过程。可以在编译期织入,也可以在运行期织入,Spring采用后者。
- 编译期间:切面在类编译时被织入
- 类加载期间:切面在类被加载到jvm时被织入
- 运行期:切面在程序运行的某一时刻被织入,在织入切面时,AOP容器会为目标对象动态创建一个代理对象,Spring AOP就是以这种方式织入切面的
# 目标对象(Target)
- 目标对象是那些将被AOP切面逻辑所影响的对象。这些对象通常专注于业务本身的逻辑。在AOP的架构中,目标对象本身不包含任何关于如何处理横切关注点的代码;这些逻辑被抽离出来,由AOP框架管理。
# 代理对象(Proxy)
- 当AOP框架在目标对象上应用切面时,它会创建一个代理对象。这个代理对象包装了目标对象,并在执行目标对象的方法时,根据切面的配置,自动执行相应的前置通知、后置通知、环绕通知等。
- 代理对象对客户端是透明的,也就是说,从客户端的角度看,它们调用的还是目标对象的方法。实际上,他们是通过代理对象来调用这些方法的,代理对象在调用前后执行额外的横切逻辑。
- 在Spring中,代理对象的实现可以通过JDK动态代理(针对接口)或者CGLIB代理(针对类)来完成。选择哪种类型的代理通常取决于目标对象的类型(是否实现接口等)。
Spring AOP支持JDK Proxy和CGLIB方式实现动态代理,这两种方式都是在程序运行期,动态的将切面织入字节码形成代理对象
- JDK 动态代理:当被代理的目标对象实现了至少一个接口时,Spring AOP 默认使用 JDK 动态代理。在这种模式下,Spring AOP 会为目标对象实现的接口创建一个代理对象,这个代理对象会拦截所有接口方法的调用。这种方式不需要额外的库支持,因为它使用了 Java 核心库中的
java.lang.reflect.Proxy
类。 - CGLIB 代理:当被代理的目标对象没有实现任何接口时,Spring AOP 会回退到使用 CGLIB 来创建代理对象。CGLIB(Code Generation Library)是一个第三方代码生成库,它通过在运行时动态生成被代理对象的子类来实现代理。与 JDK 动态代理相比,CGLIB 能够代理没有实现接口的类。
# JDK动态代理与CGLIB的区别:
综上所述,JDK Proxy 和 CGLIB 都有自己的优缺点和适用场景。如果目标对象实
- JDK动态代理要求被代理的类必须实现接口,因此它只能代理接口中定义的方法,当你通过代理对象调用接口中的方法时,这个调用会被转发到
InvocationHandler
的的invoke
方法,然后通常会通过反射来调用被代理对象的原始方法. CGLIB动态代理不要求被代理的类实现接口,而是通过继承被代理类。 - JDK动态代理在生成代理对象的速度上更快,因为JDK动态代理直接使用了Java自带的API,而CGLIB则需要通过字节码技术动态生成新的类。
- CGLIB 无法代理
final
类和final
方法,因为CGLIB通过继承目标类来创建子类,而Java语言规定final
类不能被继承,final
方法不能被重写。而JDK动态代理可以代理任意类的方法。
综上所述,JDK Proxy 和 CGLIB 都有自己的优缺点和适用场景。如果目标对象实现了接口并且需要代理的方法较少,则建议使用 JDK Proxy;如果目标对象没有实现接口或需要代理的方法较多,则建议使用 CGLIB。