面试记录 2026-09-28
题目:介绍一下Spring的AOP
难度:difficulty/medium
标签:spring/core, general/knowledge, difficulty/medium
评分:⭐⭐⭐☆☆(3/5)
我的回答
Spring AOP,AOP 就是基于代理的切面编程增强。它的核心逻辑就是在创建对象的时候,创建的是一个代理对象。在你的对象的前后,对应标注的方法,增加了对应的代理方法实现。在调用你的方法前,先调用代理内的前置方法、后置方法或者环绕方法、前后环绕方法,最终实现对你原有的方法的增强。我们常规用来使用的 AOP 会很多,比如健全,大量方法的健全使用一个注解,然后在健全,在进入方法前提前进行校验健全就可以了。另外就是日志,调用的使用环绕的 AOP 切面,在调用前调用后记录对应的日志。这是 AOP 的常规使用方式,另外还有一些打点可能会用到 AOP。
点评
优点:
- 抓住了 AOP 的本质:基于代理的增强
- 提到通知类型(前置、后置、环绕)
- 实战场景列举到位:参数校验、日志、打点监控
不足:
- AOP 核心概念没展开:连接点(JoinPoint)、切点(Pointcut)、通知(Advice)、切面(Aspect)、目标对象(Target)、引入(Introduction)、织入(Weaving)
- Spring AOP 默认代理机制没讲:JDK 动态代理(基于接口)vs CGLIB(基于子类),Spring 5+ 默认策略是有接口用 JDK、无接口用 CGLIB,Spring Boot 2.x 默认强制 CGLIB(spring.aop.proxy-target-class=true)
- 切点表达式没提:@Pointcut("execution( com.x.service..*(..))")、@within、@annotation、@args 等
- 切面失效场景没提:这是最容易踩坑的地方
- 织入方式没提:Spring AOP 是运行时织入的代理模式,不是真正的字节码织入(AspectJ 才是)
补充:
核心概念:
- JoinPoint:代码执行中可以插入增强逻辑的点(Spring AOP 只支持方法执行)
- Pointcut:定义在哪些连接点应用增强(切点表达式)
- Advice:在连接点做什么(前置、后置、返回后、异常、环绕)
- Aspect:切点 + 通知 = 切面
- Introduction:让目标类实现一个原本没实现的接口
- Weaving:把切面织入到连接点,Spring 是运行时织入(动态代理)
通知类型:
- @Before / @After / @AfterReturning / @AfterThrowing / @Around
AOP 五大失效场景:
- 类内部自调用:this.aMethod() 不走代理 → 事务失效、日志不生效
- 方法 private / final / static:代理无法覆写
- 方法非 public:Spring AOP 基于代理,非 public 方法可能被忽略
- 目标类未注册为 Spring Bean:无代理对象
- 切面 bean 没被 Spring 扫描:切点无匹配对象
规避自调用失效方案:
- 从其他 Bean 中调用
- AopContext.currentProxy() 获取当前代理对象
- @Autowired @Lazy 自注入 self 字段
- 拆成两个 Bean
完整讲解
第一部分:知识重点剖析
核心考点定位:考察对 AOP 设计思想、Spring AOP 实现机制以及工程实践中失效场景的深度理解。
知识点拆解:
- AOP 思想本源:
- 关注点分离(Separation of Concerns):把横切逻辑(日志、事务、权限、监控)从业务代码中剥离
- 横切关注点(Cross-cutting Concern):分散在多个方法中但功能类似的逻辑
- 相比继承(编译期强耦合)和直接编码(散乱),AOP 是运行时的动态代理增强
- 七大核心概念:
- JoinPoint(连接点):程序执行中能插入增强的点,Spring AOP 只支持方法执行
- Pointcut(切点):定义哪些连接点需要被增强,通过表达式匹配
- Advice(通知):在连接点做什么,五种:Before / After / AfterReturning / AfterThrowing / Around
- Aspect(切面):切点 + 通知 = 切面,通常是一个 @Component 类
- Target(目标对象):被代理的原始对象
- Introduction(引入):让目标类在运行时"实现"一个原本没实现的接口
- Weaving(织入):把切面织入到连接点,编译期 / 类加载期 / 运行时
- Spring AOP 实现机制:
- Spring AOP = 基于动态代理的运行时织入
- 底层是 BeanPostProcessor(AbstractAutoProxyCreator)在 Bean 初始化后(postProcessAfterInitialization)判断是否需要代理,需要就包装成代理对象
- 代理选型:
- JDK 动态代理:基于接口,目标类必须有接口
- CGLIB:基于继承,生成目标类的子类覆写方法
- Spring 5+ 默认:目标类有接口用 JDK,无接口用 CGLIB
- Spring Boot 2.x 默认:
spring.aop.proxy-target-class=true 强制 CGLIB(避免自调用时的 this 类型不一致)
- 切点表达式(Pointcut Expression):
execution(modifiers? ret-type? name(param) throws?):最常用,匹配方法执行
- 例:
execution( com.x.service..*(..)) 匹配所有 service 包下的方法
@within:匹配被特定注解标注的类中的所有方法
@annotation:匹配被特定注解标注的方法
@args:匹配参数带有特定注解的方法
this() / target():匹配代理对象 / 目标对象的类型
- 支持
&& || ! 组合
- 织入方式对比:
- 编译期织入(AspectJ CTW):编译时修改字节码,性能最好但依赖 AspectJ 编译器
- 类加载期织入(AspectJ LTW):JVM 启动时通过 agent 拦截,性能次之
- 运行时织入(Spring AOP):动态代理,性能稍差但零侵入
- Spring AOP 不是真正的字节码织入,只做方法级切点,无法切字段访问、构造器、私有方法
- AOP 五大失效场景(工程必看):
- 类内部自调用:this.aMethod() 直接调原方法,绕过代理 → 事务失效、日志不生效
- 方法 private / final / static:代理无法覆写 final/static,CGLIB 不能拦截 private
- 非 public 方法:JDK 动态代理只支持 public 接口方法
- 目标类未注册为 Spring Bean:无代理对象
- 切面 bean 没被 Spring 扫描:切点无匹配对象
- 自调用失效的四种规避方案:
- 从其他 Bean 中调用(最干净)
AopContext.currentProxy() 获取当前代理对象(需 @EnableAspectJAutoProxy(exposeProxy=true))
@Autowired @Lazy private MyService self; 自注入,然后 self.xxx()
- 拆成两个 Bean(架构最佳)
第二部分:考察方向与延伸递进
考察方向:
- 深度:Spring AOP 与 AspectJ 的差异、代理选型策略
- 广度:切点表达式语法、失效场景的工程排查
- 应用:日志切面、事务切面、限流切面、脱敏切面
延伸递进面试题:
- Spring AOP 和 AspectJ 有什么本质区别?为什么 Spring 不用 AspectJ 做织入?
- 解答要点:Spring AOP 是运行时动态代理,只支持方法级切点,性能略差但零依赖;AspectJ 是编译期/类加载期真正的字节码织入,能切字段、构造器、静态初始化,功能完整但复杂度高。Spring 选择了"够用即可"的工程折中
- Spring Boot 2 为什么默认强制 CGLIB?有什么坑?
- 解答要点:解决 JDK 代理下 this 指向代理对象而非目标类导致的类型不一致问题;坑点是 final 类 / final 方法无法代理(CGLIB 不能继承),会启动报错;解决办法是避免 final,或用 @SpringBootApplication 排除强制 CGLIB
- 如何写一个"脱敏"切面,在方法返回前把敏感字段打码?
- 解答要点:用 @Around 环绕通知,调用 proceed() 拿到返回值,反射遍历字段或基于注解(@Sensitive)匹配敏感字段,用 StringUtils.mask 处理后重新 set 回对象;注意返回类型可能是基本类型 / 集合 / null 的判断
第三部分:面试者参考回答(300-600 字口语化)
面试官您好,Spring AOP 本质上是基于动态代理的切面编程增强。它的核心思想是关注点分离——把日志、事务、权限、监控这些横切关注点从业务代码中剥离出来,通过代理对象在方法执行前后插入增强逻辑。
核心概念有七个:JoinPoint(连接点)是程序执行中能插入增强的点,Spring AOP 只支持方法执行;Pointcut(切点)定义哪些连接点需要被增强,通常用 @Pointcut 注解配表达式;Advice(通知)是在连接点做什么,有五种:Before、After、AfterReturning、AfterThrowing、Around,其中 Around 最灵活,能拿到 ProceedingJoinPoint 控制是否执行原方法;Aspect(切面)是切点 + 通知的组合,通常是一个 @Component 类;Target是被代理的原始对象;Introduction让目标类运行时实现新接口;Weaving(织入)是把切面织入连接点。
Spring AOP 的实现机制:底层通过 BeanPostProcessor(AbstractAutoProxyCreator)在 Bean 初始化后判断是否需要代理,需要就包装成代理对象。代理选型有两种:JDK 动态代理基于接口,目标类必须有接口;CGLIB基于继承生成子类覆写方法。Spring 5+ 默认策略是目标类有接口用 JDK、无接口用 CGLIB;Spring Boot 2.x 更进一步,通过 spring.aop.proxy-target-class=true 强制使用 CGLIB,主要是为了解决 JDK 代理下 this 指向代理对象而非目标类导致的类型不一致问题。
切点表达式主要有几种:execution( com.x.service..*(..)) 匹配所有 service 包下的方法(最常用)、@within 匹配被特定注解标注的类、@annotation 匹配被特定注解标注的方法、@args 匹配参数带注解的方法,可以用 && / || / ! 组合。
工程中最容易踩的坑是切面失效,主要是五种场景:
- 类内部自调用:this.aMethod() 直接调原方法绕过代理,导致事务、日志、脱敏切面全部失效。规避方案是从其他 Bean 调用、用 AopContext.currentProxy()、@Lazy 自注入 self 字段、或拆成两个 Bean
- 方法 private / final / static:代理无法覆写 final 和 static,CGLIB 不能拦截 private
- 方法非 public:JDK 动态代理只支持 public 接口方法
- 目标类不是 Spring Bean:无代理对象
- 切面 bean 没被扫描:切点无匹配
实际场景中我用 AOP 主要做三类事:日志切面用 @Around 记录方法入参、耗时、返回值;权限校验切面用 @Before 校验方法上的自定义 @RequirePermission 注解;脱敏切面用 @Around 遍历返回值对象,把标了 @Sensitive 的字段打码。
需要澄清的一点:Spring AOP 不是真正的字节码织入,它只支持方法级切点,无法切字段访问、构造器、私有方法。如果需要这些能力,得用 AspectJ 的编译期或类加载期织入。