Spring AOP 详解
目标:把 Spring AOP “是什么 / 怎么跑 / 怎么写 / 坑在哪” 一口气讲透。
你看完至少能做到:写得出常用切面、看得懂事务为什么生效/失效、排得了 AOP 的线上坑。
1. AOP 到底在解决什么问题?
AOP(Aspect Oriented Programming,面向切面编程)本质上是在做一件事:
把“横切关注点”从业务代码里抽出来,统一管理。
横切关注点 = 不是业务核心,但几乎每个业务点都会用到的东西,例如:
- 日志埋点(入参/出参/耗时/traceId)
- 监控指标(QPS、RT、错误率)
- 权限校验(鉴权/验签/数据权限)
- 事务(@Transactional)
- 幂等、防重、限流、熔断
- 审计(谁在什么时候改了什么)
- 异常统一转换
如果不用 AOP,你会在每个方法里 copy 一堆 try/catch、日志、耗时统计 —— 业务很快变成“面条代码”。
2. Spring AOP 的关键概念(面试必问)
记住这一组词就够了:连接点、切点、通知、切面、织入、代理
2.1 Join Point(连接点)
可以被拦截的位置。
在 Spring AOP 里基本就是:方法执行(method execution)。
2.2 Pointcut(切点)
你到底要拦截哪些连接点。
也就是一条规则/表达式:execution(...)、@annotation(...) 等。
2.3 Advice(通知)
拦截到之后要干什么:
@Before:方法执行前@After:方法执行后(无论是否异常)@AfterReturning:正常返回后@AfterThrowing:抛异常后@Around:最强(可控制是否执行目标方法、可改返回值、可吞异常)
2.4 Aspect(切面)
切点 + 通知 + 其它配置(如 order),组合成一个“模块”。
2.5 Weaving(织入)
把切面应用到目标对象上,形成最终执行链。
Spring AOP 是在 运行时 通过代理织入(不是编译期)。
2.6 Proxy(代理)
Spring AOP 的实现方式:用代理对象替代原对象,外部调用进来时先走代理,再走目标方法。
3. Spring AOP vs AspectJ(别混了)
- Spring AOP
- 运行时代理(JDK 动态代理 / CGLIB)
- 只支持方法级别(Method Execution)切入
- 轻量,够用,和 Spring 容器集成非常好
- AspectJ
- 编译期/类加载期织入(更“强”)
- 能拦截字段、构造器、静态块等更多 join point
- 需要额外 weaving 配置,复杂度更高
现实里:大多数业务系统 Spring AOP 足够;需要“拦截非方法执行”才考虑 AspectJ weaving。
4. Spring AOP 是怎么跑起来的(核心原理)
4.1 代理创建发生在什么时候?
Bean 创建完成后,Spring 会用一堆 BeanPostProcessor 进行后置处理。
AOP 的关键是:AutoProxyCreator(一类 BeanPostProcessor)。
流程大概是:
- Spring 创建 bean(实例化、属性注入、初始化)
- 进入 BeanPostProcessor:
postProcessBeforeInitialization- 初始化(init-method / @PostConstruct)
postProcessAfterInitialization
- AOP 的 AutoProxyCreator 在
AfterInitialization阶段判断:- 这个 bean 是否命中某个 Advisor(切点 + 通知)
- 命中就创建代理对象(Proxy)
- 用代理替换原 bean 放进容器
所以你最终从容器拿到的很可能不是原类,而是代理类。
4.2 Advisor / Interceptor / MethodInvocation(执行链)
Spring 会把通知包装成一串拦截器(Interceptor),形成调用链:
代理入口 -> 拦截器链(通知) -> 目标方法
@Around 本质就是拦截器链里的一环,并且能决定是否调用 proceed() 继续链条。
5. JDK 动态代理 vs CGLIB(怎么选?)
5.1 JDK 动态代理
- 基于接口:目标类必须实现接口
- 生成一个实现相同接口的代理类
- 优点:不需要子类化,较轻
- 缺点:没接口就用不了(除非强制用 CGLIB)
5.2 CGLIB
- 基于继承:给目标类生成子类代理
- 优点:不需要接口
- 缺点:
- final 类 / final 方法不能被代理(不能重写)
- 代理类生成相对更重(但现在 JVM 里通常没啥大问题)
5.3 Spring 默认策略(常见结论)
- 有接口 → 默认 JDK 代理
- 没接口 → 用 CGLIB
- 你也可以强制 CGLIB:
@EnableAspectJAutoProxy(proxyTargetClass = true)- 或
spring.aop.proxy-target-class=true
6. 注解式 AOP(@AspectJ 风格)怎么写
最推荐用法:
@Aspect + @Around/@Before。
6.1 最常用切面模板(日志 + 耗时)
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
@Order(10) // 数字越小,优先级越高(越先执行)
public class LogAspect {
@Pointcut("execution(* com.example..service..*(..))")
public void serviceMethods() {}
@Around("serviceMethods()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
String method = pjp.getSignature().toShortString();
try {
Object result = pjp.proceed();
long cost = System.currentTimeMillis() - start;
log.info("[AOP] {} ok, cost={}ms", method, cost);
return result;
} catch (Throwable ex) {
long cost = System.currentTimeMillis() - start;
log.warn("[AOP] {} fail, cost={}ms, ex={}", method, cost, ex.toString());
throw ex;
}
}
}
6.2 让切点更精准:按注解拦截(推荐)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuditLog {
String value() default "";
}
@Aspect
@Component
public class AuditAspect {
@Pointcut("@annotation(auditLog)")
public void auditPointcut(AuditLog auditLog) {}
@Around(value = "auditPointcut(auditLog)", argNames = "pjp,auditLog")
public Object around(ProceedingJoinPoint pjp, AuditLog auditLog) throws Throwable {
// auditLog.value() 就是注解参数
return pjp.proceed();
}
}
这种方式比 execution(..) 更稳(改包名、改方法名不容易误伤/漏拦)。
7. Pointcut 表达式速查(能背就背)
最常用:
execution+@annotation,其它知道即可。
7.1 execution(方法签名匹配)
格式(别死背,理解就行):
execution(访问修饰符 返回值 包名.类名.方法名(参数))
例子:
execution(* com.example.service.UserService.*(..)) // UserService 所有方法
execution(public * com.example..service..*(..)) // service 包及子包所有 public 方法
execution(* *..*Service.save*(..)) // 所有以 save 开头的方法
execution(* *(java.lang.String, ..)) // 第一个参数是 String 的方法
7.2 within(按类/包匹配)
within(com.example..service..*) // 某个包下所有类的方法
within(com.example.service.UserService) // 某个类
7.3 this / target(按代理类型/目标类型)
this(xxx):代理对象类型匹配target(xxx):目标对象类型匹配
7.4 args(按参数类型)
args(java.lang.Long, ..) // 第一个参数是 Long
7.5 @annotation / @within / @target
@annotation(注解):方法上有这个注解@within(注解):类上有这个注解@target(注解):目标类上有这个注解(和 @within 类似但语义略有区别)
实战建议:能用
@annotation就优先用它。
8. 通知执行顺序(常被问)
以一个 @Around 和其它通知同时存在为例,典型顺序是:
@Around(进入)@Before- 目标方法执行
@AfterReturning(若正常返回)或@AfterThrowing(若抛异常)@After@Around(退出)
多个切面之间通过 @Order 控制优先级:数字越小越靠外层(越先进入、越后退出)。
9. Spring AOP 的经典坑(尤其事务)
9.1 自调用(self-invocation)导致 AOP 失效(第一大坑)
@Service
public class OrderService {
public void outer() {
innerTx(); // 直接 this.innerTx(),不会走代理
}
@Transactional
public void innerTx() { ... }
}
原因:outer() 和 innerTx() 在同一个对象里,调用是 this 调用,绕过代理。
解决套路(按推荐程度):
- 把 innerTx 放到另一个 Bean,通过注入调用(最干净)
- 开启暴露代理:
@EnableAspectJAutoProxy(exposeProxy = true)然后:
(能用但不优雅)((OrderService) AopContext.currentProxy()).innerTx(); - 用 AspectJ weaving(重,但能拦截内部调用)
9.2 final / private 方法拦不到
- Spring AOP 主要拦截 public 方法(尤其事务)
- CGLIB 也无法覆盖 final 方法
- private 方法本来就不是外部调用点,代理也很难切进去
9.3 没被 Spring 管理的对象,AOP 不生效
你自己 new 出来的对象,容器没机会给你换成代理。
9.4 切点写太宽导致“全站被拦截”,性能/日志爆炸
execution(* com..*(..)) 这种一不小心就炸。
建议:
- 优先按注解
- 或者限定在 service 包 + public + 业务前缀
10. 怎么确认一个 Bean 到底有没有被代理?(排障必备)
10.1 看类名
Object bean = ctx.getBean(UserService.class);
System.out.println(bean.getClass());
- JDK 代理:
com.sun.proxy.$Proxyxx - CGLIB:
UserService$$EnhancerBySpringCGLIB$$...
10.2 用 AopUtils
import org.springframework.aop.support.AopUtils;
boolean isAop = AopUtils.isAopProxy(bean);
boolean isCglib = AopUtils.isCglibProxy(bean);
boolean isJdk = AopUtils.isJdkDynamicProxy(bean);
10.3 开日志(快速看到 Advisor 匹配)
把这些包的日志级别调到 DEBUG/TRACE:
org.springframework.aoporg.springframework.aop.frameworkorg.springframework.aop.aspectj
11. AOP 在真实项目里最常见的 5 个落地
- 统一日志/链路追踪:接口入参/出参 + traceId + 耗时
- 统一异常转换:把底层异常转成业务错误码
- 权限/数据权限:基于注解 + SpEL 做规则表达
- 幂等/防重:@Idempotent(Redis/DB 唯一键)配合 AOP
- 事务:@Transactional 本质也是 AOP(Advisor + 拦截器)
12. 一句话总结(背这句就够了)
Spring AOP 用 BeanPostProcessor 在 Bean 初始化后创建 代理对象,通过 切点 匹配目标方法,把 通知 组装成拦截器链,实现对方法调用的增强;默认优先 JDK 动态代理(有接口),否则用 CGLIB,最常见的坑是 自调用绕过代理。
13. 额外补充:面试高频问答快闪
-
Q:@Transactional 为什么是 AOP?
A:事务拦截器本质就是一个 MethodInterceptor,匹配到方法就开启/提交/回滚事务。 -
Q:为什么同类内部调用事务不生效?
A:绕过代理(this 调用),没走拦截器链。 -
Q:AOP 能拦截 static 方法吗?
A:Spring AOP 基本不行(代理是对象级别),AspectJ weaving 才可能做到更多 join point。 -
Q:@Order 数字越大越先执行?
A:不是。数字越小越优先,越靠外层。
1159

被折叠的 条评论
为什么被折叠?



