面试知识库
极高 进阶

Spring-AOP原理#

一句话答案#

AOP 通过动态代理在不修改业务代码的情况下织入横切逻辑(事务、日志、权限)。Spring Framework 默认对有接口的类用 JDK 动态代理、对无接口的类用 CGLIB;Spring Boot 2.x+ 默认全用 CGLIB(可通过 spring.aop.proxy-target-class=false 切回 JDK 代理)。

核心要点

AOP 核心概念#

术语含义示例
切面(Aspect)横切关注点的模块化@Aspect 注解的类
连接点(JoinPoint)程序执行的某个点方法调用、异常抛出
切入点(Pointcut)匹配连接点的表达式execution(* com.xxx.service.*.*(..))
通知(Advice)在切入点执行的动作@Before / @After / @Around
织入(Weaving)将切面应用到目标对象的过程Spring 在运行时通过代理织入

五种通知类型#

@Around 环绕通知(最强大,可控制是否执行目标方法)
  ├── @Before 前置通知(方法执行前)
  │     ↓
  │   目标方法执行
  │     ↓
  ├── @AfterReturning 返回通知(正常返回后)
  ├── @AfterThrowing 异常通知(抛异常后)
  └── @After 最终通知(无论正常/异常都执行,类似 finally)
plaintext

两种代理方式#

维度JDK 动态代理CGLIB
原理基于接口,生成实现同接口的代理类基于继承,生成目标类的子类
要求目标类必须实现接口目标类不能是 final
性能调用时通过反射,稍慢通过 FastClass 直接调用,稍快
Spring 默认Spring Framework 默认(有接口时)Spring Boot 2.x+ 默认全用 CGLIB

Spring Boot 为什么改默认为 CGLIB?

  • 避免类型转换问题:JDK 代理生成的是接口类型,注入时用实现类类型会报错
  • 统一行为,减少开发者困惑

代理生成时机#

Spring 容器启动
  → Bean 实例化
  → 属性注入
  → BeanPostProcessor 的 postProcessAfterInitialization
  → AbstractAutoProxyCreator 检查是否需要代理
    ├── 有匹配的 Advisor → 创建代理对象
    └── 无匹配 → 返回原始 Bean
plaintext

关键类: AnnotationAwareAspectJAutoProxyCreator(继承自 AbstractAutoProxyCreator),在 Bean 初始化完成后判断是否需要代理。

深挖:拦截器责任链如何驱动通知顺序#

多个 Advice 怎么串成一条链:

  • 每个 @Before/@After/@Around 等通知,在底层都被适配成一个 MethodInterceptor(如 MethodBeforeAdviceInterceptorAspectJAroundAdvice)。
  • 代理被调用时,Spring 把匹配该方法的所有拦截器排成一个 List<MethodInterceptor>,封装进 ReflectiveMethodInvocation

proceed() 递归驱动整条链(这是执行顺序的底层原因):

ReflectiveMethodInvocation.proceed():
  if (拦截器都走完) → 反射调用目标方法 method.invoke(target)
  else → 取下一个拦截器,调 interceptor.invoke(this)
         拦截器内部再调 mi.proceed() → 进入下一层
plaintext
  • 每个拦截器在调用 proceed() 之前的代码 = 前置逻辑,之后的代码 = 后置逻辑。
  • 正是这种「调用前/调用后包裹 + proceed() 递归下钻」的结构,决定了 @Around 包在最外、@Before 先于目标方法、@After/@AfterReturning 在返回时执行——顺序不是写死的 if-else,而是责任链递归天然形成的洋葱模型

面试常追的一句话: @Around 里如果不调 joinPoint.proceed(),目标方法和它内层的所有通知都不会执行——因为链断了。

深挖:Spring AOP vs AspectJ 的根因对比#

维度Spring AOPAspectJ
织入方式运行时动态代理编译期(CTW)/ 加载期(LTW)字节码织入
能切什么只能切 Spring 容器管理的 Bean 的方法构造器、字段、静态方法、任意类(含非 Bean)
自调用失效(绕过代理)生效(字节码直接改了方法本身)
依赖无需特殊编译需 ajc 编译器或 LTW agent

根因: Spring AOP 基于代理,增强逻辑写在「代理对象」里,所以只有通过代理对象调用、且是被 Spring 管理的 Bean 方法才能被拦截——这就是自调用失效、private/构造器切不到的本质。AspectJ 直接把横切逻辑织入字节码本身,方法体被真正改写,因此不受代理边界限制,能切构造器、字段访问、final 方法乃至非 Spring 管理的对象。

AOP 在 Spring 中的典型应用#

  • @Transactional:事务管理(最常见)
  • @Cacheable:缓存
  • @Async:异步执行
  • @PreAuthorize:权限控制(Spring Security)
  • 自定义注解:日志、限流、幂等
面试回答(2分钟版)

Spring AOP 的实现原理是动态代理。在 Bean 初始化完成后,BeanPostProcessor(具体是 AbstractAutoProxyCreator)检查是否有匹配的切面,有的话就创建代理对象替换原始 Bean。代理方式有两种:JDK 动态代理基于接口、CGLIB 基于继承生成子类。注意区分两个层面的默认行为:Spring Framework 默认有接口用 JDK 代理、无接口用 CGLIB;Spring Boot 2.x 之后通过自动配置将默认改为全用 CGLIB,主要是为了避免 JDK 代理生成接口类型导致注入实现类时报错的问题。AOP 在 Spring 中最典型的应用是 @Transactional,事务管理就是通过 AOP 在方法前后开启/提交/回滚事务实现的。

追问与易错

追问方向:

  • “AOP 和拦截器有什么区别?”→ AOP 基于代理作用于 Bean 方法,拦截器基于 Servlet 作用于 HTTP 请求
  • “@Transactional 为什么自调用失效?”→ 自调用不经过代理对象,直接调 this.method()
  • “CGLIB 为什么不能代理 final 方法?”→ CGLIB 通过子类继承,final 方法不能被重写

易错点:

  • ❌ “AOP 是编译时织入”——Spring AOP 是运行时通过代理织入,AspectJ 才是编译时
  • ❌ “JDK 代理比 CGLIB 快”——JDK 代理通过反射调用,CGLIB 通过 FastClass 直接调用,CGLIB 调用更快
  • ❌ “Spring AOP 能拦截 private 方法”——代理只能拦截 public 方法