Java AOP及在SpringBoot项目中实践
AOP概念
面向切面编程(AOP,Aspect-Oriented Programming)是一种编程范式,旨在通过将横切关注点(如日志记录、事务管理、安全性等)从业务逻辑中分离出来,提高代码的模块化和可维护性。AOP 的核心思想是将这些横切关注点封装为“切面”,并在程序运行时动态地将它们织入到目标对象的方法中。
一、Java AOP 的实现原理
1.1 AOP 的几个概念名词
这几个名词初接触AOP时,会比较抽象,没必要刻意去记。不妨先忽略他们,用个例子实践后,再回头看这些概念。(包括学习其他知识点时,上来一堆概念,我们也可以这样,先拿例子了解后,再去回头看这些抽象的概念总结。)
- 切面(Aspect):切面是横切关注点的模块化,通常包含通知(Advice)和切点(Pointcut)。
- 通知(Advice):通知定义了切面的具体行为,包括在何时(如方法执行前、后或异常抛出时)执行切面代码。
- 切点(Pointcut):切点定义了在哪些连接点(Join Point)上应用通知。连接点可以是方法调用、异常抛出等。
- 织入(Weaving):织入是将切面应用到目标对象的过程,可以在编译时、类加载时或运行时进行。
1.2 Java AOP 的实现方式
Java AOP 的实现主要依赖于动态代理技术。Spring AOP 提供了两种代理方式:
- JDK 动态代理:基于接口的代理,要求目标对象实现至少一个接口。Spring 会为接口生成代理对象,并在代理对象中织入切面逻辑。
- CGLIB 动态代理:基于子类的代理,适用于没有实现接口的类。Spring 会为目标类生成一个子类,并在子类中织入切面逻辑。
1.3 Spring AOP 的工作流程
- 定义切面:通过
@Aspect
注解定义切面类,并在其中定义通知和切点。往后看例子。 - 定义通知:使用
@Before
、@After
、@Around
注解定义通知类型。通常使用@Around
就可以 - 定义切点:使用
@Pointcut
注解定义切点表达式,指定在哪些方法上应用通知。 - 织入切面:Spring 容器在启动时,会根据切面配置动态生成代理对象,并将切面逻辑织入到目标对象的方法中。
二、SpringBoot AOP 简单例子
2.1 背景
假设我们需要对所有 Service 层的方法进行日志记录,记录方法的执行时间、参数和返回值。
2.2 实现步骤
2.2.1 添加依赖
首先,在 pom.xml
中添加 Spring AOP 依赖(这里直接引用starter依赖):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.2.2 定义切面类
创建一个切面类 LoggingAspect
,用于记录方法执行的日志:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
/**
* Pointcut表达式 指定在哪些方法上应用通知
**/
@Pointcut("execution(* com.soda.demo.service.*.*(..))")
public void serviceLayer() {}
/**
* Around 引用 Pointcut修饰的方法,在这些方法前后执行
**/
@Around("serviceLayer()")
public Object logMethodExecution(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
logger.info("Entering method: {} with arguments: {}", methodName, args);
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
logger.info("Exiting method: {} with result: {}. Execution time: {} ms", methodName, result, endTime - startTime);
return result;
}
}
2.2.3 测试切面
在 Service 层中定义一个简单的服务类 UserService
:
import org.springframework.stereotype.Service;
@Service
public class UserService {
public String getUserById(Long id) {
return "User " + id;
}
}
测试类访问这个service,可以看到控制台输出了类似以下的日志:
Entering method: getUserById with arguments: [1]
Exiting method: getUserById with result: User 1. Execution time: 2 ms
三、ProceedingJoinPoint的实践
ProceedingJoinPoint
(通常简写为 pjp) 是一个重要的接口,它代表了连接点(join point),即程序执行过程中的某个特定点(如方法调用)。通过它可以获取代理类、目标类、修饰的方法、方法返回类型等信息。
1. 通过 JoinPoint 获取方法签名
@Aspect
@Component
public class MyAspect {
@Around("@annotation(com.soda.MyAnnotation)")
public Object aroundAnnotatedMethod(ProceedingJoinPoint pjp) throws Throwable {
// 获取方法签名
MethodSignature signature = (MethodSignature) pjp.getSignature();
// 获取返回类型
Class<?> returnType = signature.getReturnType();
System.out.println("方法返回类型: " + returnType.getName());
// 方法信息
Method method = signature.getMethod();
// 获取方法上的注解
MyAnnotation myAnnotation = method.getAnnotation(MyAnnotation.class);
String annotationValue = myAnnotation.value();
// 方法名
String methodName = method.getName();
// 参数类型
Class<?>[] parameterTypes = method.getParameterTypes();
// 执行原方法
Object result = pjp.proceed();
// 根据返回类型包装结果
if (returnType == void.class) {
return null;
} else if (returnType == String.class) {
return "Result: " + result;
} else {
return new CommonResult<>(true, "Success", result);
}
}
}
2. 获取泛型返回类型
如果需要获取泛型返回类型: 获取返回类型时,如果是泛型,需要考虑类型擦除问题
@Around("@annotation(com.soda.MyAnnotation)")
public Object aroundAnnotatedMethod(ProceedingJoinPoint pjp) throws Throwable {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
// 获取泛型返回类型
Type genericReturnType = method.getGenericReturnType();
if (genericReturnType instanceof ParameterizedType) {
ParameterizedType type = (ParameterizedType) genericReturnType;
Type[] actualTypeArguments = type.getActualTypeArguments();
// 处理泛型参数类型
}
return pjp.proceed();
}
3. getThis() vs getTarget()
getThis()
- 返回当前执行对象的代理对象
- 在 Spring AOP 中,这通常是 JDK 动态代理或 CGLIB 代理的实例
- 如果目标对象实现了接口,则返回 JDK 动态代理
- 如果目标对象没有实现接口,则返回 CGLIB 代理
getTarget()
- 返回被代理的目标对象(原始对象)
- 这是实际执行业务逻辑的原始对象
- 在 Spring AOP 中,这是你实际编写的服务类或组件类的实例
示例说明
@Aspect
@Component
public class MyAspect {
@Around("execution(* com.soda.service.*.*(..))")
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
// 获取代理对象
Object proxy = pjp.getThis();
// 返回的是原始业务对象
Object target = pjp.getTarget();
System.out.println("Proxy class: " + proxy.getClass());
System.out.println("Target class: " + target.getClass());
return pjp.proceed();
}
}
四、 AOP 的优缺点
优点
- 模块化:AOP 将横切关注点从业务逻辑中分离出来,使得代码更加模块化和易于维护。
- 非侵入性:AOP 通过代理机制实现,对原有代码没有侵入性,无需修改原有代码即可添加新功能。
缺点
- 性能开销:AOP 通过动态代理实现,会引入一定的性能开销,尤其是在方法调用频繁的场景下。
- 调试困难:由于切面逻辑是动态织入的,调试时可能会增加复杂性,尤其是在多个切面交织的情况下。读代码时可能不知道进了哪些切面逻辑。
性能开销的体现
AOP 所谓的“性能损耗”并非指代理对象创建时的开销(单例 Bean 的代理只创建一次),而是指每次调用被增强方法时的运行时开销。这种开销源于动态代理的工作机制,但多数场景可忽略,极端场景需注意。
一、动态代理的“每次调用”都要经过额外流程
Spring AOP 默认基于动态代理(JDK 代理或 CGLIB 代理)实现,每次调用被增强的方法时,都会触发代理对象的“拦截-增强-执行目标方法”流程,这会带来额外的性能消耗:
-
JDK 动态代理的开销
JDK 代理通过接口生成代理类,调用方法时需经过:InvocationHandler.invoke()
方法的反射调用(即使内部优化过,仍比直接调用原生方法慢);- 代理类需解析切入点匹配(判断当前方法是否需要增强);
- 执行切面逻辑(如
@Before
、@After
等通知); - 最终通过反射调用目标对象的方法。
这些步骤在每次方法调用时都会执行。
-
CGLIB 代理的开销
CGLIB 通过继承目标类生成代理类,调用方法时需经过:- 代理类重写的方法(比 JDK 代理少一层反射,但仍需执行拦截逻辑);
MethodInterceptor.intercept()
方法的回调;- 同样的切入点匹配和切面逻辑执行;
- 最终调用目标类的原始方法。
虽然 CGLIB 避免了 JDK 代理的反射调用,但继承机制带来的方法调用链路(子类→父类)和拦截器回调,仍比直接调用原生方法慢。
二、与 AspectJ 对比:静态织入 vs 动态增强
性能差异的核心原因是 Spring AOP 是动态增强,而 AspectJ 是静态织入:
- AspectJ:在编译期或类加载期就将切面逻辑“嵌入”目标类的字节码中,生成的是“增强后的目标类”。运行时调用方法时,就像调用原生方法一样,没有额外的代理层或拦截流程,几乎无性能损耗。
- Spring AOP:切面逻辑并未嵌入目标类字节码,而是通过代理对象“动态附加”。每次调用都需要代理对象在中间“转发”,并执行切面逻辑,这必然带来额外的 CPU 时间和内存开销(尤其是高频调用的方法)。
总结
以上就是AOP相关的一些介绍,在项目中实践起来吧~