📌 PDF :大白话说Java面试题 --- 06_Spring篇
第10题:解释一下 Spring AOP 里面的几个名词
📚 回答:
- 核心考点 : Spring AOP 的名词体系是理解 AOP 的基石,大厂面试不会只问"有哪几个名词",而是深入考察 每个名词的源码级实现 (如
AspectJExpressionPointcut如何解析表达式)、五种通知类型的执行顺序与异常传播 、织入的三种时机与 Spring 的选择原因 、以及JoinPoint与ProceedingJoinPoint的区别。面试官真正想判断的是:你是否建立了从概念到源码的完整认知链路,能否在工程实践中准确运用这些概念设计和排查问题。
1. 切面(Aspect)
-
1.1 定义与本质
切面是横切关注点的模块化封装 ,包含一组**通知(Advice)和 切入点(Pointcut)**的集合。在 Spring 中,切面通过
@Aspect注解标识,由 Spring 容器管理。java@Aspect @Component public class LoggingAspect { // 切入点定义 @Pointcut("execution(* com.example.service.*.*(..))") public void serviceMethods() {} // 通知定义 @Before("serviceMethods()") public void logBefore(JoinPoint joinPoint) { System.out.println("[日志切面] 方法开始: " + joinPoint.getSignature().getName()); } } -
1.2 切面的生命周期
阶段 说明 对应源码 解析 Spring 扫描 @Aspect注解,解析切点表达式AspectJAutoProxyCreator注册 将切面 Bean 注册到 BeanFactoryBeanDefinitionRegistry代理创建 根据切点匹配结果,为目标 Bean 创建代理 AbstractAutoProxyCreator织入 将通知织入到代理对象的方法调用链 JdkDynamicAopProxy/CglibAopProxy -
1.3 多切面优先级控制
当多个切面同时匹配同一连接点时,通过
@Order注解或实现Ordered接口控制执行顺序:java@Aspect @Order(1) // 数值越小,优先级越高,越先执行 public class SecurityAspect { ... } @Aspect @Order(2) public class LogAspect { ... }执行顺序规则 :优先级高的切面的
@Before先执行,但@After后执行(类似栈结构)。
2. 连接点(Join Point)
-
2.1 定义与范围
连接点是程序执行过程中的某个特定点 ,是 AOP 可以插入逻辑的位置。在 Spring AOP 中,连接点仅限于方法调用(因为 Spring AOP 基于代理实现)。
AOP 框架 支持的连接点类型 Spring AOP 仅方法调用 AspectJ 方法调用、构造器调用、字段访问、异常处理等 -
2.2 JoinPoint 对象详解
JoinPoint是 Spring AOP 提供的运行时对象,封装了当前连接点的全部信息:java@Before("serviceMethods()") public void logBefore(JoinPoint joinPoint) { // 获取目标对象 Object target = joinPoint.getTarget(); // 获取代理对象 Object proxy = joinPoint.getThis(); // 获取方法签名 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); // 获取方法名 String methodName = signature.getName(); // 获取参数 Object[] args = joinPoint.getArgs(); // 获取参数类型 Class<?>[] paramTypes = signature.getParameterTypes(); }JoinPoint API 说明 getTarget()获取被代理的目标对象(原始对象) getThis()获取代理对象本身 getSignature()获取连接点的签名(方法签名) getArgs()获取方法参数数组 getKind()获取连接点类型(如 method-execution)toLongString()获取完整的方法描述 -
2.3 ProceedingJoinPoint------环绕通知专用
ProceedingJoinPoint是JoinPoint的子接口,仅用于@Around通知 ,增加了proceed()方法控制目标方法的执行:java@Around("serviceMethods()") public Object logAround(ProceedingJoinPoint pjp) throws Throwable { // 前置逻辑 long start = System.currentTimeMillis(); // 执行目标方法(关键!不调用则目标方法不会执行) Object result = pjp.proceed(); // 也可以修改参数:pjp.proceed(new Object[]{newArg1, newArg2}); // 后置逻辑 long cost = System.currentTimeMillis() - start; System.out.println("方法耗时: " + cost + "ms"); // 可以修改返回值 return result; }⚠️ 重要 :
@Around通知中必须调用proceed(),否则目标方法不会执行。这是最常见的环绕通知使用错误。
3. 切入点(Pointcut)
-
3.1 定义与本质
切入点是匹配连接点的表达式,定义了"在哪里"织入通知。Spring AOP 使用 AspectJ 的切点表达式语法。
-
3.2 切点表达式语法详解
execution(修饰符 返回类型 包名.类名.方法名(参数) 异常)组成部分 说明 示例 修饰符 可选,public/private/protected public返回类型 必填, *表示任意*、void、String包名 必填, .表示当前包,..表示任意子包com.service、com..service类名 必填, *表示任意UserService、*Service方法名 必填, *表示任意addUser、*参数 必填, ()无参、(..)任意、(*, String)指定(Long)、(String, ..)异常 可选 throws IOException常用表达式示例:
java@Pointcut("execution(public * com.example.service.*.*(..))") // service包下所有public方法 @Pointcut("execution(* com.example.service..*.*(..))") // service包及其子包下所有方法 @Pointcut("execution(* com.example.service.UserService.add*(..))") // UserService中以add开头的方法 @Pointcut("execution(* com.example.service.*Service.*(..))") // 以Service结尾的类中的所有方法 @Pointcut("execution(* com.example.service.*.*(Long, ..))") // 第一个参数为Long的方法 @Pointcut("execution(* com.example.service.*.*(..)) throws Exception") // 声明抛出Exception的方法 -
3.3 其他切点指示器
指示器 说明 示例 @annotation匹配带有指定注解的方法 @annotation(com.example.Log)within匹配指定类型内的方法 within(com.example.service.*)this匹配代理对象为指定类型的 this(com.example.service.UserService)target匹配目标对象为指定类型的 target(com.example.service.UserService)args匹配参数为指定类型的 args(java.lang.String)bean匹配指定 Bean 名称 bean(userService)@within匹配类上有指定注解的 @within(com.example.Service)@target匹配目标对象类上有指定注解的 @target(com.example.Service)@args匹配参数类上有指定注解的 @args(com.example.Valid)组合表达式:
java@Pointcut("execution(* com.example.service.*.*(..)) && @annotation(com.example.Log)") public void loggedServiceMethods() {} // 匹配service包下且带有@Log注解的方法 @Pointcut("execution(* com.example.service.*.*(..)) || execution(* com.example.controller.*.*(..))") public void serviceOrController() {} // 匹配service或controller包下的方法 -
3.4 切点解析的底层实现
Spring 使用
AspectJExpressionPointcut解析切点表达式,底层依赖 AspectJ 的PointcutParser:java// Spring 源码简化示意 public class AspectJExpressionPointcut implements Pointcut { private String expression; // 切点表达式字符串 @Override public boolean matches(Method method, Class<?> targetClass) { // 使用 AspectJ 的 PointcutExpression 进行匹配 PointcutExpression pcExpression = pointcutParser.parsePointcutExpression(expression); return pcExpression.matchesMethodExecution(method).alwaysMatches(); } }
4. 通知(Advice)
-
4.1 五种通知类型深度解析
通知类型 注解 执行时机 特点 典型场景 前置通知 @Before目标方法执行前 无法阻止方法执行 权限校验、参数校验 后置通知 @After目标方法执行后(无论是否异常) 类似 finally 资源释放、清理 返回通知 @AfterReturning目标方法正常返回后 可获取返回值 日志记录、缓存更新 异常通知 @AfterThrowing目标方法抛出异常后 可获取异常对象 异常告警、补偿逻辑 环绕通知 @Around目标方法执行前后 完全控制执行流程 事务管理、性能监控 -
4.2 通知执行顺序详解
单一切面内的执行顺序:
@Around 前半部分(proceed() 之前) ↓ @Before ↓ 【目标方法执行】 ↓ @AfterReturning(正常返回)或 @AfterThrowing(抛出异常) ↓ @After(无论是否异常,最终执行) ↓ @Around 后半部分(proceed() 之后)异常场景下的执行顺序:
@Around 前半部分 ↓ @Before ↓ 【目标方法执行 → 抛出异常】 ↓ @AfterThrowing(捕获异常) ↓ @After(仍然执行!) ↓ @Around 后半部分(异常会传播到此处,需处理)⚠️ 注意 :
@After类似于finally,无论是否异常都会执行。@Around中如果proceed()抛出异常,异常会传播到@Around的后半部分。 -
4.3 通知的参数绑定
Spring AOP 支持将连接点信息绑定到通知方法的参数:
java// 绑定返回值 @AfterReturning(pointcut = "serviceMethods()", returning = "result") public void logAfterReturning(Object result) { System.out.println("方法返回: " + result); } // 绑定异常 @AfterThrowing(pointcut = "serviceMethods()", throwing = "ex") public void logAfterThrowing(Exception ex) { System.out.println("方法异常: " + ex.getMessage()); } // 绑定注解 @Before("@annotation(log)") public void logWithAnnotation(Log log) { System.out.println("注解值: " + log.value()); } // 绑定参数 @Before("execution(* com.example.service.*.*(Long,..)) && args(id,..)") public void logWithParam(Long id) { System.out.println("参数ID: " + id); } -
4.4 通知的异常传播
通知类型 异常处理行为 @Before抛出异常会阻止目标方法执行 @AfterReturning抛出异常会覆盖目标方法的正常返回,最终向外抛出 @AfterThrowing抛出异常会覆盖原始异常,最终向外抛出 @After抛出异常会覆盖原始异常(如果存在),最终向外抛出 @Around完全控制异常,可捕获、可抛出、可转换
5. 织入(Weaving)
-
5.1 定义与三种织入时机
织入是将切面应用到目标对象的过程,即把通知逻辑插入到连接点的过程。AOP 规范定义了三种织入时机:
织入时机 英文 实现方式 特点 代表框架 编译期织入 Compile-time weaving 特殊的编译器在编译时修改源码 无运行时开销,需特殊编译器 AspectJ AJC 加载期织入 Load-time weaving 类加载器在加载类时修改字节码 无运行时开销,需特殊类加载器 AspectJ LTW 运行期织入 Runtime weaving 运行时动态生成代理对象 有代理开销,使用简单 Spring AOP -
5.2 Spring AOP 为什么选择运行期织入?
Spring AOP 选择运行期织入(动态代理)的原因:
- 无侵入性:无需修改编译器或类加载器,纯 Java 实现;
- 与 IoC 集成:利用 Spring 容器管理 Bean 的生命周期,在 Bean 初始化后创建代理;
- 使用简单:注解驱动,零配置即可使用;
- 满足大多数场景:企业应用中 95% 的 AOP 需求是方法拦截,运行期织入足够。
代价:每次方法调用都经过代理,有一定性能开销(但通常可忽略)。
-
5.3 Spring AOP 织入的源码流程
Bean 实例化 ↓ Bean 属性填充(依赖注入) ↓ Bean 初始化(@PostConstruct、InitializingBean) ↓ 【关键】AbstractAutoProxyCreator.postProcessAfterInitialization() ↓ 检查 Bean 是否匹配任何切点 ↓ 是 → 创建代理对象(JDK 或 CGLIB)替换原始 Bean ↓ 否 → 返回原始 Bean ↓ 代理对象注入到依赖方
6. 目标对象(Target)与代理(Proxy)
-
6.1 目标对象(Target)
被代理的原始对象,即业务逻辑的真正执行者。在 Spring AOP 中,目标对象通常是 Spring 容器管理的 Bean。
-
6.2 代理(Proxy)
AOP 框架生成的代理对象,客户端实际持有的引用。代理对象拦截方法调用,在适当位置执行通知逻辑。
代理类型 实现方式 适用场景 JDK 动态代理 实现目标接口 目标类实现了接口 CGLIB 代理 继承目标类 目标类未实现接口,或强制使用 -
6.3 目标对象 vs 代理对象的区别
java@Autowired private UserService userService; // 注入的是代理对象! public void test() { // userService 是代理对象(JDK 代理时类型为 $ProxyXX,CGLIB 时为 UserService$$EnhancerBySpringCGLIB) userService.addUser(); // 调用经过代理,触发切面 // 获取目标对象 if (AopUtils.isAopProxy(userService)) { UserService target = (UserService) AopProxyUtils.ultimateTargetClass(userService); } }
7. 引入(Introduction)
引入是 Spring AOP 的一个高级特性,允许为目标对象动态添加新的接口实现:
java
@DeclareParents(value = "com.example.service.*", defaultImpl = DefaultUsageTracked.class)
private UsageTracked mixin; // 为所有 Service 类动态添加 UsageTracked 接口实现
| 特性 | 说明 |
|---|---|
| 作用 | 动态为目标类添加新接口,无需修改源码 |
| 实现 | 通过代理对象实现多个接口 |
| 场景 | 为现有类添加监控、统计等功能 |
8. 面试官追问与高分回答模板
-
追问 1:"Spring AOP 中有哪些关键名词?分别是什么意思?"
低分回答:"有切面、连接点、切入点、通知、织入。切面是模块化的横切关注点,连接点是程序执行的特定点,切入点是匹配连接点的表达式,通知是切面执行的动作,织入是将切面应用到目标对象的过程。"(背诵式回答,没有深度)
高分回答:
"Spring AOP 的核心名词体系包括:
- 切面(Aspect) :横切关注点的模块化封装,包含通知和切入点。在 Spring 中通过
@Aspect注解标识,由@Order控制多切面优先级。 - 连接点(Join Point) :程序执行过程中的特定点,Spring AOP 中仅限于方法调用 。
JoinPoint对象封装了目标对象、代理对象、方法签名、参数等信息;ProceedingJoinPoint是其子接口,专用于@Around通知,提供proceed()控制目标方法执行。 - 切入点(Pointcut) :匹配连接点的表达式,使用 AspectJ 语法。核心指示器包括
execution(方法匹配)、@annotation(注解匹配)、within(类型匹配)、bean(Bean 名称匹配)等,支持&&、||、!组合。 - 通知(Advice) :切面在连接点执行的动作,共五种类型:
@Before:方法执行前@After:方法执行后(无论是否异常,类似 finally)@AfterReturning:方法正常返回后(可获取返回值)@AfterThrowing:方法抛出异常后(可获取异常)@Around:环绕方法执行,完全控制流程(必须调用proceed())
- 织入(Weaving) :将切面应用到目标对象的过程。Spring AOP 采用运行期织入 (动态代理),通过
AbstractAutoProxyCreator在 Bean 初始化后检查切点匹配,创建代理对象替换原始 Bean。 - 目标对象(Target):被代理的原始对象。
- 代理(Proxy):AOP 生成的代理对象,客户端实际持有的引用,负责拦截方法调用并执行通知链。
理解这些名词的关键是建立'在哪里(Pointcut)→ 做什么(Advice)→ 怎么做(Proxy/Weaving)'的完整链路。"
- 切面(Aspect) :横切关注点的模块化封装,包含通知和切入点。在 Spring 中通过
-
追问 2:"五种通知类型的执行顺序是什么?异常时怎么执行?"
高分回答:
"正常执行顺序:
@Around 前半 → @Before → 目标方法 → @AfterReturning → @After → @Around 后半异常执行顺序:
@Around 前半 → @Before → 目标方法(抛异常)→ @AfterThrowing → @After → @Around 后半(异常传播)关键要点:
@After类似finally,无论是否异常都会执行;@AfterReturning和@AfterThrowing互斥,只会执行其一;@Around中如果proceed()抛出异常,异常会传播到@Around的后半部分,可以在@Around中捕获并处理;- 多切面通过
@Order控制顺序,数值越小优先级越高,@Before先执行但@After后执行。"
-
追问 3:"JoinPoint 和 ProceedingJoinPoint 有什么区别?"
高分回答:
"
ProceedingJoinPoint是JoinPoint的子接口,核心区别:特性 JoinPoint ProceedingJoinPoint 使用场景 所有通知类型(除 @Around)仅 @Around通知控制目标方法 ❌ 无法控制 ✅ 通过 proceed()控制执行修改参数 ❌ ✅ proceed(Object[] args)修改返回值 ❌ ✅ 可替换 proceed()的返回值阻止执行 ❌ ✅ 不调用 proceed()即可阻止JoinPoint提供只读信息(目标对象、方法签名、参数等);ProceedingJoinPoint增加了proceed()方法,这是@Around通知能够'环绕'目标方法的核心。⚠️ 常见错误 :在
@Around中忘记调用proceed(),导致目标方法不执行。" -
*追问 4:"切点表达式 execution( com.service..(...)) 每个部分是什么意思?"**
高分回答:
"
execution(* com.service.*.*(..))的语法结构是:execution(修饰符 返回类型 包名.类名.方法名(参数) 异常)逐部分解析:
execution:切点指示器,按方法签名匹配;- 第一个
*:返回类型,表示任意返回类型; com.service:包名,精确匹配com.service包;- 第二个
*:类名,表示任意类; - 第三个
*:方法名,表示任意方法; (..):参数,表示任意参数(零个或多个)。
整体含义:匹配
com.service包下任意类的任意方法,不限返回类型和参数。常用通配符:
*:匹配一个任意字符(包名中用一个点,类名/方法名中匹配任意);..:匹配零个或多个任意字符(包名中表示任意子包,参数中表示任意参数);+:匹配当前类及其子类。"
-
追问 5:"Spring AOP 为什么只能在运行时织入?AspectJ 是怎么织入的?"
高分回答:
"Spring AOP 选择运行期织入(动态代理)的原因:
- 纯 Java 实现:无需修改编译器或 JVM,与 Spring IoC 无缝集成;
- 使用简单:注解驱动,零配置;
- 满足大多数需求:企业应用 95% 的 AOP 需求是方法拦截。
AspectJ 支持三种织入时机:
- 编译期织入:使用 AspectJ 编译器(AJC)在编译时修改源码字节码,无运行时开销;
- 加载期织入(LTW) :通过
-javaagent和特殊类加载器在类加载时修改字节码; - 运行期织入:与 Spring AOP 类似。
Spring AOP 的织入流程:Bean 初始化后 →
AbstractAutoProxyCreator.postProcessAfterInitialization()检查切点匹配 → 匹配则创建代理对象(JDK/CGLIB)→ 代理对象替换原始 Bean 注入到依赖方。代价:每次方法调用经过代理,有一定性能开销。但方法调用本身的 overhead 远大于代理开销,通常可忽略。"
-
追问 6:"@Around 通知中 proceed() 的作用是什么?不调用会怎样?"
高分回答:
"
proceed()是@Around通知的核心方法,作用是执行目标方法(或下一个通知)。在
@Around中,proceed()的调用位置决定了通知的'环绕'特性:java@Around("serviceMethods()") public Object around(ProceedingJoinPoint pjp) throws Throwable { // 前置逻辑(proceed() 之前) System.out.println("Before"); Object result = pjp.proceed(); // ← 执行目标方法 // 也可以传参:pjp.proceed(new Object[]{arg1, arg2}); // 后置逻辑(proceed() 之后) System.out.println("After"); return result; }不调用
proceed()的后果:- 目标方法不会执行;
- 如果这是事务切面,事务不会开启;
- 如果这是日志切面,只会记录'方法开始',不会记录'方法结束'。
高级用法:
- 修改参数:
pjp.proceed(new Object[]{modifiedArg}); - 修改返回值:在
proceed()后修改result; - 异常处理:用
try-catch包裹proceed(),捕获并转换异常。"
9. 方案选型速查表
| 业务场景 | 推荐通知类型 | 核心理由 |
|---|---|---|
| 权限校验 | @Before |
方法执行前拦截,失败直接拒绝 |
| 日志记录 | @Around |
精确计算耗时,记录入参和出参 |
| 事务管理 | @Around |
完全控制事务边界,可捕获异常回滚 |
| 缓存管理 | @Around |
先查缓存,无则执行并写入 |
| 异常告警 | @AfterThrowing |
仅异常时触发,避免正常流程干扰 |
| 资源释放 | @After |
类似 finally,确保一定执行 |
| 返回值处理 | @AfterReturning |
正常返回时处理,如数据脱敏 |
| 方法重试 | @Around |
捕获异常后循环重试 |
💡 面试官想要的满分总结:
Spring AOP 的七大核心名词构成了完整的 AOP 概念体系:
切面(Aspect) 是横切关注点的模块化封装,包含通知和切入点;连接点(Join Point) 是程序执行的特定点,Spring 中仅限方法调用;切入点(Pointcut) 通过 AspectJ 表达式定义"在哪里"织入;通知(Advice) 定义"做什么",共五种类型按
@Around→@Before→ 目标方法 →@AfterReturning/@AfterThrowing→@After的顺序执行;织入(Weaving) 是应用切面的过程,Spring 选择运行期动态代理实现;目标对象(Target) 是被代理的原始对象;代理(Proxy) 是客户端实际持有的代理对象。理解这些名词的关键是建立从"概念"到"源码"的链路:切点表达式由
AspectJExpressionPointcut解析,通知执行由ReflectiveMethodInvocation驱动,代理创建由DefaultAopProxyFactory根据目标类是否实现接口选择 JDK 或 CGLIB。生产中最容易踩的坑是
@Around中忘记调用proceed()导致目标方法不执行,以及混淆JoinPoint(只读)和ProceedingJoinPoint(可控)的适用场景。记住:@Around是最强大的通知类型,也是最容易出错的类型。
觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯