前言
AOP(Aspect-Oriented Programming,面向切面编程)是 Spring 框架的两大核心基石(IoC + AOP)之一。它弥补了 OOP 在横切关注点(如日志、事务、权限、监控等)上的代码冗余问题。本文将深入 Spring AOP 底层机制、核心概念、通知进阶及实战细节,帮你构建完整的 AOP 知识体系。
1. AOP 底层原理(代理模式)
Spring AOP 本质上是 动态代理 的封装。它不修改源代码,而是在运行期通过代理对象增强目标方法。
Spring 底层采用两种代理策略,由 DefaultAopProxyFactory 根据目标类自动抉择:
| 代理方式 | 实现机制 | 适用条件 | 性能特点 |
|---|---|---|---|
| JDK 动态代理 | 实现 InvocationHandler,通过反射生成目标接口的代理类 | 目标类实现了至少一个接口 | 反射开销较大,随 JDK 版本升级不断优化(JDK 8+ 性能接近 CGLIB) |
| CGLIB 动态代理 | 基于 ASM 字节码框架,生成目标类的子类(继承机制),重写父类方法 | 目标类未实现接口,或强制使用 proxyTargetClass=true | 方法调用通过 FastClass 机制,性能较高,但代理类创建较慢 |
Spring Boot 2.x+ 默认策略:如果目标类有接口,优先使用 JDK 动态代理;若想强制使用 CGLIB,可在配置中设置
spring.aop.proxy-target-class=true(Spring Boot 中默认已为true以增强兼容性)。
核心执行流程:
调用方 -> 代理对象(Proxy) -> 拦截器链(MethodInterceptor) -> 目标方法(Target)
Spring AOP 并非 AspectJ 的编译时织入(字节码修改),而是运行期织入。
2. AOP 核心概念详解(务必厘清)
| 术语 | 英文 | 解释(Spring 语境) |
|---|---|---|
| 切面 | Aspect | 横切关注点的模块化,通常是一个带有 @Aspect 注解的类,内部包含通知和切入点。 |
| 连接点 | Join Point | 程序执行过程中能够被拦截的点。在 Spring AOP 中,仅指方法的执行(区别于 AspectJ 的字段访问、构造器等)。方法调用前后、抛出异常时都是连接点。 |
| 切入点 | Pointcut | 一组连接点的集合,通过表达式(如 execution)定义哪些方法需要被增强。换言之,切入点定义了“在哪里”执行通知。 |
| 通知 | Advice | 切面在特定连接点执行的具体动作,定义了“做什么”和“何时做”(方法前、后、环绕等)。 |
| 引入 | Introduction | 允许切面为目标类添加新方法或属性(不常用)。 |
| 织入 | Weaving | 将切面应用到目标对象并创建代理对象的过程。Spring AOP 在运行期完成织入。 |
| 目标对象 | Target Object | 被增强的原始业务对象。 |
3. 通知类型(Advice Types)及其详解
Spring 支持五种通知注解,对应不同的拦截时机:
| 注解 | 执行时机 | 适用场景 | 注意事项 |
|---|---|---|---|
@Before | 目标方法执行之前 | 权限校验、参数预处理、日志记录 | 若抛出异常,会阻止目标方法执行 |
@After | 目标方法执行之后(无论是否抛出异常,类似 finally) | 资源释放、清理缓存 | 无法获取返回值(若异常则无返回值) |
@AfterReturning | 目标方法正常返回后执行 | 对返回值进行二次处理、记录成功日志 | 可通过 returning 属性绑定返回值参数 |
@AfterThrowing | 目标方法抛出异常后执行 | 异常监控、发送告警、回滚补偿 | 可通过 throwing 属性绑定异常参数 |
@Around | 环绕通知,包裹整个目标方法,可自定义执行时机 | 性能监控(耗时统计)、重试机制、事务管理 | 必须接收 ProceedingJoinPoint,手动调用 proceed();否则目标不执行 |
代码示例
@Aspect
@Component
public class LogAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object aroundLog(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
Object result = pjp.proceed(); // 执行目标方法
long cost = System.currentTimeMillis() - start;
System.out.println("【Around】耗时: " + cost + "ms");
return result;
}
@Before("execution(* com.example.service.*.*(..))")
public void beforeLog(JoinPoint jp) {
System.out.println("【Before】方法即将执行: " + jp.getSignature().getName());
}
@After("execution(* com.example.service.*.*(..))")
public void afterLog() {
System.out.println("【After】方法执行完毕(finally逻辑)");
}
@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "retVal")
public void afterReturningLog(Object retVal) {
System.out.println("【AfterReturning】返回值: " + retVal);
}
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex")
public void afterThrowingLog(Exception ex) {
System.out.println("【AfterThrowing】异常: " + ex.getMessage());
}
}
4. 通知执行顺序(高频面试考点)
4.1 同一切面类中不同通知的顺序
在一个切面类中,如果多个通知匹配同一个连接点,其执行顺序遵循 “环绕前置 -> Before -> 目标方法 -> 环绕后置 -> After -> AfterReturning/AfterThrowing” 的嵌套逻辑。
具体时序:
@Around中proceed()之前的代码@Before- 目标方法执行
@Around中proceed()之后的代码@After(无论如何都执行)@AfterReturning(正常返回)或@AfterThrowing(抛出异常)
同一切面内通知执行顺序不依赖代码声明顺序,而是严格按照上述织入顺序。但为了可读性,建议按逻辑顺序排列。
4.2 不同切面类中同一通知的顺序(如多个 @Before)
当多个切面类同时作用于同一目标方法时,需要通过 @Order 注解(或实现 Ordered 接口)来控制执行顺序。
规则:
@Order数值越小,优先级越高。- 前置通知(@Before):优先级高的先执行。
- 后置通知(@After / @AfterReturning):优先级高的后执行(先进后出,类似于栈的嵌套)。
示例:
@Aspect
@Component
@Order(1) // 优先级高
public class TransactionAspect { ... }
@Aspect
@Component
@Order(2) // 优先级低
public class LogAspect { ... }
如果有 @Before 和 @Around,先执行高优先级切面的 @Around 前置部分,再执行该切面的 @Before,再进入低优先级切面的包裹,以此类推。整体遵循多层洋葱模型。
5. 切入点表达式(Pointcut Expressions)
切入点表达式用于精准定位需要织入的方法。Spring 支持多种指示器(PCD),最常用的是 execution 和 @annotation。
5.1 execution(基础且最强大)
语法格式:
execution(修饰符? 返回类型 声明类型? 方法名(参数) 异常?)
全量示例及通配符详解:
| 表达式示例 | 含义 |
|---|---|
execution(public * com.example.UserService.*(..)) | UserService 类中所有公共方法,任意返回类型,任意参数 |
execution(* com.example.service..*.*(..)) | service 包及其子包下所有类的所有方法 |
execution(* find*(..)) | 所有以 "find" 开头的方法,任意参数 |
execution(* save*(String, ..)) | 以 save 开头,第一个参数为 String,后面任意参数的方法 |
execution(* update*(*)) | 以 update 开头,且恰好一个参数(任意类型)的方法 |
参数匹配详解:
(..):零个或多个任意参数。(*):一个任意类型的参数。(String, Integer):必须恰好两个参数,且类型分别为 String 和 Integer。(String, ..):第一个参数为 String,后面可有可无任意参数。
5.2 @annotation(自定义注解驱动)
通过自定义注解来标记切点,实现更灵活、非侵入式的增强。这是企业级开发中最优雅的实践方式。
Step 1:定义自定义注解
@Target(ElementType.METHOD) // 仅作用于方法
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecution {
String module() default "";
boolean recordParams() default true;
}
Step 2:定义切面,匹配该注解
@Aspect
@Component
public class CustomAnnotationAspect {
// 匹配任何标注了 @LogExecution 的方法
@Pointcut("@annotation(logExecution)")
public void pointcut(LogExecution logExecution) {}
@Around("pointcut(logExecution)")
public Object around(ProceedingJoinPoint pjp, LogExecution logExecution) throws Throwable {
String module = logExecution.module();
System.out.println("模块: " + module + ", 开始执行...");
if (logExecution.recordParams()) {
Object[] args = pjp.getArgs();
System.out.println("参数: " + Arrays.toString(args));
}
return pjp.proceed();
}
}
Step 3:在业务方法上使用
@Service
public class UserService {
@LogExecution(module = "用户管理")
public void deleteById(Long id) {
// 业务逻辑
}
}
其他常用指示器:
@within(匹配标注了指定注解的类中的所有方法)、bean(userService)(匹配指定 Bean 名称)。
6. 连接点(JoinPoint)的深入使用
在通知方法中,我们可以通过参数获取连接点的上下文信息。这是排查问题和获取运行时元数据的关键。
JoinPoint:用于@Before、@After、@AfterReturning、@AfterThrowing。ProceedingJoinPoint:用于@Around(它是JoinPoint的子接口,增加了proceed()方法)。
常用 API:
@Before("execution(* com.example..*.*(..))")
public void inspect(JoinPoint jp) {
// 1. 获取方法签名
MethodSignature signature = (MethodSignature) jp.getSignature();
Method method = signature.getMethod(); // 获取 Method 对象(反射)
// 2. 获取方法名
String methodName = jp.getSignature().getName();
// 3. 获取目标对象(原始 Bean)
Object target = jp.getTarget();
// 4. 获取代理对象
Object proxy = jp.getThis();
// 5. 获取方法参数
Object[] args = jp.getArgs();
// 6. 获取声明的参数名称(需开启 -parameters 编译选项或使用 ASM)
String[] paramNames = signature.getParameterNames();
}
实战技巧:在
@Around中,如果需要在proceed()前后修改参数,可以修改args数组后传入pjp.proceed(args),实现在不修改原始代码的情况下动态修改入参。
7. 最佳实践与避坑指南
| 问题 | 解决方案 |
|---|---|
| 内部方法调用不生效 | AOP 基于代理,同类中 methodA() 调用 @Transactional 或 @Around 的 methodB() 不会走代理。解决:注入自身代理(@Autowired)或通过 AopContext.currentProxy()。 |
| 切面执行多次 | 检查是否配置了多个代理(如 Spring 代理 + AspectJ 自动代理),或切面类未被 @Component 扫描但又被重复定义了。 |
| @AfterReturning 拿不到返回值 | 检查 returning 属性名称是否与参数名完全一致。 |
| @AfterThrowing 捕获不到异常 | 检查 throwing 属性参数类型必须匹配异常类型(或为 Throwable),且该异常未被方法签名忽略。 |
| 切入点表达式过于宽泛 | 尽量缩小扫描范围(如限制在 service 包下),避免对框架内部类(如 BeanFactory)进行代理,导致性能下降或栈溢出。 |
| CGLIB 代理无法访问 final 方法 | 代理类无法重写 final 方法,目标方法若非 public 需注意代理限制。 |
8. 总结速查表
| 关注点 | 核心结论 |
|---|---|
| 底层原理 | 运行期动态代理(JDK Proxy / CGLIB),非编译时织入 |
| 核心三要素 | 切面(Aspect) = 切入点(Pointcut) + 通知(Advice) |
| 通知执行顺序(单切面) | Around前置 -> Before -> 目标 -> Around后置 -> After -> AfterReturning/AfterThrowing |
| 通知执行顺序(多切面) | @Order 值小者优先;Before 正序,After 逆序(嵌套执行) |
| 最常用切入点 | execution(* com.xxx.service..*.*(..)) |
| 企业级优雅切点 | @annotation(自定义注解),配合参数绑定,松耦合且直观 |
| 获取方法信息 | ProceedingJoinPoint(Around) / JoinPoint(其他) |
掌握以上内容,你不仅能熟练使用 Spring AOP 解决横切问题,还能在面试中清晰阐述其原理与底层细节。写 AOP 时多想想“代理对象 vs 目标对象”,多思考“何时使用注解 vs execution”,会让你的代码更加健壮优雅。
Comments NOTHING