Java 框架 Spring AOP 详细分析介绍
一、AOP 核心概念:解决何种问题?
在软件开发中,像日志记录、事务管理、安全校验这样的功能,会散布在许多业务模块中。这些功能被称为横切关注点 (Cross-Cutting Concerns)。
- 传统 OOP 的困境:会导致“代码纠缠”和“代码分散”,使得核心业务逻辑变得不清晰,难以维护。
public void transfer(Account from, Account to, double amount) { // 1. 开始事务 (分散的横切逻辑) beginTransaction(); try { // 2. 核心业务逻辑 from.debit(amount); to.credit(amount); // 3. 提交事务 (分散的横切逻辑) commitTransaction(); } catch (Exception e) { // 4. 回滚事务 (分散的横切逻辑) rollbackTransaction(); // 5. 记录日志 (分散的横切逻辑) log.error("Transfer failed", e); throw e; } }
AOP (Aspect-Oriented Programming) 的出现,就是为了解决这一问题。它允许我们将这些横切关注点模块化,然后通过声明的方式定义它们应该应用在程序的哪些地方。最终,由 AOP 框架在运行时或编译时,自动将这些模块化的逻辑织入 (Weave) 到指定的位置。
核心术语:
- Aspect (切面):横切关注点的模块化。它是一个类,上面标注了
@Aspect注解。它包含了 Advice 和 Pointcut。 - Join Point (连接点):程序执行过程中一个明确的点,如方法调用、异常抛出、字段修改等。Spring AOP 仅支持方法执行 (Method Execution) 这一种连接点。
- Advice (通知):切面在特定连接点采取的动作。它定义了“做什么”和“何时做”。
- Pointcut (切入点):一个匹配连接点的表达式。它定义了“在哪里”应用通知。这是 AOP 的核心,决定了通知应该织入到哪个或哪些方法上。
- Weaving (织入):将切面应用到目标对象,从而创建代理对象的过程。Spring AOP 在运行时完成织入。
- Target Object (目标对象):被一个或多个切面所通知的对象。也就是被织入横切逻辑的核心业务对象。
- AOP Proxy (AOP 代理):由 AOP 框架创建的对象,它是目标对象的增强版本。在 Spring AOP 中,代理可以是 JDK 动态代理或 CGLIB 代理。
二、Spring AOP 的实现原理:动态代理
Spring AOP 的本质是在 IoC 容器管理 Bean 生命周期的过程中,通过动态代理技术,在合适的时机(BeanPostProcessor)为 Bean 创建代理对象。
2.1 两种代理机制
-
JDK Dynamic Proxy (基于接口的代理)
- 机制:通过
java.lang.reflect.Proxy和InvocationHandler实现。 - 条件:目标类实现了至少一个接口。
- 特点:代理对象会实现目标类所实现的所有接口。外部调用通过接口进行,代理对象拦截所有接口方法调用。
- 性能:生成代理类较快。
- 机制:通过
-
CGLIB Proxy (基于子类的代理)
- 机制:通过操作字节码,生成目标类的一个子类,并重写其中的方法。
- 条件:目标类没有实现任何接口,或者配置了强制使用 CGLIB (
proxy-target-class="true")。 - 特点:代理对象是目标类的子类。它不能代理
final类或final方法。 - 性能:生成的代理类在运行时调用更快,但生成过程稍慢。
Spring 的默认策略:如果目标对象实现了接口,则使用 JDK 动态代理;否则使用 CGLIB。但可以通过 @EnableAspectJAutoProxy(proxyTargetClass = true) 强制使用 CGLIB。
2.2 核心源码:代理的创建时机
代理对象的创建发生在 Bean 生命周期的初始化后阶段,由 BeanPostProcessor 完成。
核心类是 AnnotationAwareAspectJAutoProxyCreator,它实现了 BeanPostProcessor 接口。
// 在 AbstractAutowireCapableBeanFactory.initializeBean() 中调用
@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
if (bean != null) {
// 1. 为每个 Bean 构建一个 key (通常是 beanName)
Object cacheKey = getCacheKey(bean.getClass(), beanName);
if (this.earlyProxyReferences.remove(cacheKey) != bean) {
// 2. ★ 核心方法:如果需要,则包装 Bean (创建代理)
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
// ... (检查是否已经处理过、是否是基础设施Bean等)
// 1. ★★★ 获取适用于此 Bean 的 Advisor (通知器) ★★★
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
// 2. ★★★ 创建代理对象 ★★★
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
return proxy;
}
// 如果不需要代理,返回原始 Bean
return bean;
}
getAdvicesAndAdvisorsForBean():这个方法会扫描所有@Aspect注解的类(即切面),解析其中的@Pointcut和@Before等注解,并使用 Pointcut 表达式去匹配当前 Bean 的所有方法。如果匹配成功,则返回对应的Advisor(封装了 Advice 和 Pointcut)链。createProxy():根据配置和目标对象的情况,选择使用 JDK 动态代理或 CGLIB 来创建最终的代理对象。
2.3 代理对象的执行流程
当一个方法在代理对象上被调用时,会触发一个拦截器链(MethodInterceptor)。Spring AOP 将不同的 Advice 类型(@Before, @After等)都封装成对应的 MethodInterceptor。
其核心拦截与执行流程,特别是责任链模式的应用,可以通过以下流程图清晰地展现:
MethodInvocation.proceed() 是关键。它维护着一个当前拦截器的索引计数器。每次调用 proceed(),计数器递增,并调用链中的下一个拦截器。当所有拦截器都执行完后,最终会调用目标方法本身 (invokeJoinpoint())。然后,结果(或异常)会沿着调用链反向返回,因此 @Around 通知可以在方法调用后执行逻辑。
三、Advice 类型详解
Spring AOP 支持 5 种类型的通知,它们定义了“何时”执行切面代码。
| 注解 | 时机 | 说明 |
|---|---|---|
@Before | 在目标方法执行之前执行。 | 适用于权限校验、日志记录等。 |
@AfterReturning | 在目标方法成功完成之后执行。 | 可以访问方法的返回值。 |
@AfterThrowing | 在目标方法抛出异常之后执行。 | 可以访问抛出的异常。 |
@After (@Finally) | 在目标方法完成之后执行,无论结果是成功还是异常。 | 类似于 finally 块,用于释放资源等。 |
@Around | 环绕目标方法执行。 | 功能最强大的通知。它可以控制是否执行目标方法,何时执行,甚至可以修改参数和返回值。它必须接收一个 ProceedingJoinPoint 参数,并调用其 proceed() 方法来执行目标方法。 |
@Around 通知示例:
@Around("execution(* com.example.service.*.*(..))")
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
// 1. 目标方法执行前的逻辑
long start = System.currentTimeMillis();
Object[] args = pjp.getArgs();
// 可以修改参数...
try {
// 2. 决定是否执行目标方法 (可以完全跳过)
Object result = pjp.proceed(args); // 继续执行链,调用目标方法
// 3. 目标方法成功执行后的逻辑 (可以修改返回值)
long end = System.currentTimeMillis();
logger.info("Method executed in " + (end - start) + "ms");
return result;
} catch (Exception e) {
// 4. 目标方法抛出异常后的逻辑
logger.error("Method threw an exception", e);
throw e; // 可以重新抛出原异常,或抛出新的异常
}
}
四、Pointcut 表达式与指示器
Pointcut 用于定义通知应该织入的位置。Spring AOP 使用了 AspectJ 的切点表达式语言。
4.1 execution 指示器
这是最常用的主指示器,用于匹配方法执行连接点。
语法:execution([modifiers] return-type [declaring-type].method-name(param-list) [throws-pattern])
*:匹配任意字符(但只能匹配一个部分)..:匹配任意字符,可用于包名和参数列表(匹配多个部分)+:匹配指定类型及其子类型(仅用于声明类型)
示例:
execution(* com.example.service.*.*(..)):匹配com.example.service包下任何类的任何方法。execution(* com.example.service.UserService.*(..)):匹配UserService接口的所有方法。execution(* save*(..)):匹配所有以save开头的方法。execution(* com.example.service..*.*(..)):匹配com.example.service包及其所有子包下任何类的任何方法。
4.2 within 指示器
限制匹配到特定类型内的连接点。
within(com.example.service.*):匹配com.example.service包下的所有方法。within(com.example.service..*):匹配com.example.service包及其子包下的所有方法。
4.3 args 指示器
限制匹配到参数符合指定类型的方法。
@Around("execution(* *..find*(..)) && args(id,..)"):匹配任何find开头且第一个参数为id的方法,并可以在通知中访问该参数。
4.4 @annotation 指示器
匹配带有指定注解的方法。
@Around("@annotation(com.example.annotation.MyLog)"):匹配任何被@MyLog注解标记的方法。
4.5 组合使用
可以使用 && (and), || (or), ! (not) 来组合切点表达式。
@Before("execution(* com.example.service.*.*(..)) && @annotation(org.springframework.transaction.annotation.Transactional)"):匹配 service 包下所有被 @Transactional 注解的方法。
五、Spring AOP 的局限性
- 仅支持方法级别的织入:无法织入字段、构造器、静态初始化块等连接点。
- 仅适用于 Spring 容器管理的 Bean:只能对由 Spring IoC 容器初始化的 Bean 进行代理。
- 自调用问题:同一个类中的一个方法调用另一个方法,被调用的方法上的 AOP 通知不会生效。因为自调用是通过
this引用(即目标对象本身)进行的,而不是通过代理对象。public class UserService { public void a() { this.b(); // `this` 是真实对象,不是代理,因此 b() 的 AOP 通知不会生效。 } @Transactional public void b() { // save to DB } } - 性能考量:虽然现代 JVM 对动态代理优化得很好,但大量使用 AOP 还是会带来轻微的性能开销。
六、最佳实践与总结
- 选择合适的通知类型:优先使用最不强大的通知类型能满足需求。例如,如果只是记录日志,用
@Before或@AfterReturning而不是@Around。 - 保持切面轻量:切面中的逻辑应该简单高效,避免耗时的操作,因为它们会影响所有被织入的方法。
- 精确的切入点表达式:尽量编写精确的表达式,避免过于宽泛的匹配(如
execution(* *(..))),以免意外织入不需要的方法,影响性能和预期行为。 - 处理自调用:如果需要在同一对象内进行有 AOP 通知的方法调用,可以通过
AopContext.currentProxy()获取当前代理对象,然后通过它来调用方法(需要配置exposeProxy = true)。 - 理解代理机制:清楚你的 Bean 是被 JDK 代理还是 CGLIB 代理,这在某些场景下(如类型转换、获取类信息)可能会有影响。
总结:Spring AOP 是一个强大而实用的框架,它通过动态代理和 BeanPostProcessor 扩展机制,将 AspectJ 的切面编程模型无缝集成到了 Spring IoC 容器中。它有效地将横切关注点模块化,是实现声明式事务(@Transactional)、安全、日志等功能的基石。理解其原理和局限性,有助于我们更好地在项目中应用它。对于更复杂的需求(如构造器、字段织入),则需要使用完整的 AspectJ。
本文来自博客园,作者:NeoLshu,转载请注明原文链接:https://www.cnblogs.com/neolshu/p/19120884

浙公网安备 33010602011771号