【Spring】面试突击系列(四):Spring AOP 深度解析

Spring AOP 深度解析:从概念到源码,面试不再踩坑

Spring 面试必杀技系列 · 第四期 |
前三期:[IoC与DI] [自动配置] [Spring MVC]
技术栈:Spring Boot 2.7.x + JDK 11 + MySQL 8.x


开篇:你为什么需要 AOP?

想象一个场景:EduLearn 平台的每个 Service 方法,产品都要求「记录入参、出参和耗时」。最笨的办法是在每个方法里复制粘贴日志代码——80 个 Service 类,400+ 个方法,改一处日志格式就要全部改一遍。

这种散落在各处的相同逻辑就是「横切关注点」。AOP 解决的就是这个问题:把日志逻辑抽到一个地方,声明它「拦截哪些方法」,其余的交给框架。

一句话定义:AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,通过「切面」将横切关注点与业务逻辑分离,在不修改源码的情况下为程序动态添加功能。


一、AOP 核心概念速览

AOP 只有 5 个核心概念,理解它们的关系等于理解了 AOP。

在这里插入图片描述

1.1 JoinPoint(连接点)—— 所有可能的插入点

连接点是程序执行过程中「可以插入切面的位置」。在 Spring AOP 中,连接点就是方法执行(方法调用、异常抛出、字段修改等在 Spring AOP 中不被拦截)。

类比:一栋楼的每一层楼板都是可以打洞穿管的位置——但你不会每层都打。

1.2 Pointcut(切点)—— 筛选出真正需要增强的连接点

切点是一个谓词表达式,从无数连接点中筛选出真正需要增强的目标方法。

// 匹配 com.edulearn.service 包下所有方法
@Pointcut("execution(* com.edulearn.service..*.*(..))")
public void serviceLayer() {}

类比:只有卫生间和厨房的楼板需要打洞穿水管,你通过图纸(切点表达式)精确标出这些位置。

1.3 Advice(通知)—— 在切点处「做什么」

通知定义了在切点处具体执行的逻辑。Spring 提供 5 种通知类型:

通知类型执行时机能否阻止方法执行能否获取返回值能否修改返回值
@Before方法执行前
@AfterReturning方法正常返回后
@AfterThrowing方法抛出异常后
@After方法执行后(finally)
@Around环绕方法执行

1.4 Aspect(切面)—— 切点 + 通知的封装

切面 = 切点(Where)+ 通知(What)。在 Spring 中,就是一个标注了 @Aspect 的类。

@Aspect
@Component
public class LogAspect {
    // 切点: 定义拦截位置
    @Pointcut("execution(* com.edulearn.service..*.*(..))")
    public void serviceLayer() {}

    // 通知: 定义拦截后做什么
    @Before("serviceLayer()")
    public void logBefore(JoinPoint joinPoint) { /* 记录日志 */ }
}

1.5 Weaving(织入)—— 把切面应用到目标对象

织入是将切面逻辑与业务对象整合在一起的过程。Spring AOP 在运行时通过动态代理完成织入(区别于 AspectJ 的编译期织入)。

1.6 EduLearn 概念映射

在这里插入图片描述

AOP 概念EduLearn 实例代码位置
Aspect 切面LogAspect(日志)、AuthAspect(权限)aspect/LogAspect.java
Pointcut 切点service 包下所有方法@Pointcut("execution(*..service..*.*(..))")
Advice 通知@Before 日志输入、@Around 权限校验logBefore() / checkAuth()
JoinPoint 连接点每个 Service 方法都是候选点UserService.getUserById()
Weaving 织入Spring 启动时自动生成代理透明,开发者无感知

二、AOP 实现原理对比:JDK vs CGLIB

Spring AOP 的底层实现就是动态代理。JDK 和 CGLIB 是两种实现方式,面试常问。

在这里插入图片描述

2.1 JDK 动态代理(接口代理)

核心原理java.lang.reflect.Proxy + InvocationHandler

// JDK 动态代理核心流程
public class JdkProxyDemo {
    public static void main(String[] args) {
        // 1. 目标对象(必须实现接口)
        UserService target = new UserServiceImpl();

        // 2. 创建代理
        UserService proxy = (UserService) Proxy.newProxyInstance(
            target.getClass().getClassLoader(),
            target.getClass().getInterfaces(),
            (proxyObj, method, args1) -> {
                System.out.println(">>> 前置增强 <<<");
                Object result = method.invoke(target, args1); // 反射调用
                System.out.println(">>> 后置增强 <<<");
                return result;
            }
        );

        // 3. 通过代理调用
        proxy.getUserById(1L); // 走 InvocationHandler.invoke()
    }
}

调用链路:Client → $ProxyN.method()InvocationHandler.invoke()method.invoke(target, args) → 目标方法

前置条件:目标对象必须实现至少一个接口。没有接口的类无法使用 JDK 动态代理。

2.2 CGLIB 代理(子类代理)

核心原理:通过 ASM 字节码技术生成目标类的子类作为代理。

// CGLIB 代理核心流程
public class CglibProxyDemo {
    public static void main(String[] args) {
        // 1. 创建 Enhancer
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(CourseServiceImpl.class); // 继承目标类
        enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) -> {
            System.out.println(">>> 前置增强 <<<");
            Object result = proxy.invokeSuper(obj, args1); // FastClass 索引调用
            System.out.println(">>> 后置增强 <<<");
            return result;
        });

        // 2. 创建代理对象(目标类的子类实例)
        CourseServiceImpl proxy = (CourseServiceImpl) enhancer.create();
        proxy.listCourses();
    }
}

调用链路:Client → 子类代理.method()MethodInterceptor.intercept()FastClass.invoke(index, args) → 目标方法

前置条件:目标类不能是 final,方法不能是 final/private/static(无法被子类覆写)。

2.3 关键对比

对比维度JDK 动态代理CGLIB 代理
代理原理接口代理,Proxy.newProxyInstance()子类代理,Enhancer.create()
方法调用反射 method.invoke() —— 有性能开销FastClass 索引调用 —— 无反射开销
创建成本较小 —— 仅生成单个 Proxy 类较大 —— 需要 ASM 字节码操作
前置条件目标类必须实现接口目标类不能是 final,方法不能 final/private/static
构造器调用不拦截(接口无构造器可言)会拦截(子类构造器会调用父类构造器)
Spring Boot 2.x 默认(已不默认)默认且推荐

2.4 CGLIB 全面碾压?不一定

JDK 动态代理在 JDK 8+ 之后,反射性能已大幅优化(MethodHandle + LambdaMetafactory)。在调用链路较深的场景中,CGLIB FastClass 的索引查找也有开销。

但 CGLIB 最大的优势不是性能,而是不需要接口——这让你可以代理任意的 @Service@Component 类。

2.5 Spring AOP 选型规则

在 Spring AOP 中,DefaultAopProxyFactory 的选型逻辑如下:

// DefaultAopProxyFactory.createAopProxy() 简化逻辑
public AopProxy createAopProxy(AdvisedSupport config) {
    if (config.isOptimize() || config.isProxyTargetClass()
            || hasNoUserSuppliedProxyInterfaces(config)) {
        // 使用 CGLIB
        return new CglibAopProxy(config);
    } else {
        // 使用 JDK 动态代理
        return new JdkDynamicAopProxy(config);
    }
}

总结:只要满足「有接口」+「没有显式配置 proxyTargetClass=true」+「没有 optimize」,Spring 就用 JDK。否则一律 CGLIB。

在 Spring Boot 2.x 中,spring.aop.proxy-target-class 默认值为 true,所以默认走 CGLIB。


三、Spring AOP 注解实战:EduLearn 案例

理论到位了,来写代码。本节用 EduLearn 平台的两个真实场景展示 AOP 的完整用法。

3.1 环境准备

<!-- pom.xml 依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

注意:spring-boot-starter-web 已间接依赖 aop,但显式引入更清晰。

3.2 案例一:统一日志切面

需求:记录每个 Service 方法的入参、出参和耗时,方便排查线上问题。

package com.edulearn.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;

@Slf4j
@Aspect
@Component
public class LogAspect {

    // 切点:拦截 service 包下所有 public 方法
    @Pointcut("execution(public * com.edulearn.service..*.*(..))")
    public void serviceLayer() {}

    // 环绕通知:记录入参、出参和耗时
    @Around("serviceLayer()")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().toShortString();
        Object[] args = joinPoint.getArgs();

        // 1. 前置:记录入参
        log.info("[AOP] {} 入参: {}", methodName, Arrays.toString(args));

        long start = System.currentTimeMillis();
        Object result;
        try {
            // 2. 执行目标方法
            result = joinPoint.proceed();
        } catch (Throwable e) {
            // 3. 异常:记录异常信息
            log.error("[AOP] {} 异常: {}", methodName, e.getMessage());
            throw e;
        }

        // 4. 后置:记录出参和耗时
        long elapsed = System.currentTimeMillis() - start;
        log.info("[AOP] {} 出参: {} | 耗时: {}ms", methodName, result, elapsed);

        return result;
    }
}

核心要点

  • @Around 是最灵活的通知——你可以控制是否执行目标方法(joinPoint.proceed())、修改返回值、吞掉异常。
  • joinPoint.getSignature().toShortString() 输出如 UserServiceImpl.getUserById(..)
  • 耗时统计用 System.currentTimeMillis() 即可,对绝大多数场景精度足够。

3.3 案例二:权限校验切面

需求:某些方法需要特定角色才能调用(如「删除课程」需要 ADMIN 角色)。用自定义注解 + AOP 实现声明式鉴权。

// ===== 1. 自定义注解 =====
package com.edulearn.annotation;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequireRole {
    /** 需要的角色 */
    String value() default "USER";
}
// ===== 2. 权限校验切面 =====
package com.edulearn.aspect;

import com.edulearn.annotation.RequireRole;
import com.edulearn.common.util.UserContext;     // 线程绑定的当前用户信息
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
public class AuthAspect {

    // 切点:所有标注了 @RequireRole 的方法
    @Pointcut("@annotation(requireRole)")
    public void authPoint(RequireRole requireRole) {}

    @Around(value = "authPoint(requireRole)", argNames = "joinPoint,requireRole")
    public Object checkAuth(ProceedingJoinPoint joinPoint, RequireRole requireRole) throws Throwable {
        // 1. 获取当前登录用户的角色
        String currentRole = UserContext.getCurrentRole();
        String requiredRole = requireRole.value();

        // 2. 角色匹配
        if (!requiredRole.equals(currentRole)) {
            String msg = String.format("权限不足!接口 %s 需要角色 [%s],当前角色 [%s]",
                    joinPoint.getSignature().getName(), requiredRole, currentRole);
            log.warn("[Auth] {}", msg);
            throw new RuntimeException(msg); // 实际项目中抛出自定义异常
        }

        // 3. 鉴权通过,执行目标方法
        log.info("[Auth] 鉴权通过:{} 以 [{}] 角色调用", joinPoint.getSignature().getName(), currentRole);
        return joinPoint.proceed();
    }
}
// ===== 3. 在 Controller 中使用 =====
@RestController
@RequestMapping("/api/courses")
public class CourseController {

    @Autowired
    private CourseService courseService;

    // 普通用户都可以查看
    @GetMapping
    public List<CourseVO> listCourses() { return courseService.listCourses(); }

    // 只有 ADMIN 才能删除
    @RequireRole("ADMIN")
    @DeleteMapping("/{courseId}")
    public void deleteCourse(@PathVariable Long courseId) {
        courseService.deleteCourse(courseId);
    }
}

核心要点

  • @annotation(requireRole) 匹配被 @RequireRole 标注的方法,并直接将注解实例绑定到方法参数
  • 通过切面参数绑定获取注解值(requireRole.value()),无需在切面中反射读取。
  • UserContext 是一个线程绑定的工具类(通常通过 ThreadLocal + Filter/Interceptor 填充),确保并发安全。

3.4 结果验证

// 启动后访问:
// GET  /api/courses            → 无需 @RequireRole,直接返回课程列表
// DELETE /api/courses/1001     → 日志打印 "[Auth] 鉴权通过" 或 "权限不足"

四、切点表达式语法精讲

切点表达式是 AOP 中最容易写错的环节。本节覆盖 7 种指示符,附常见错误案例。

4.1 execution —— 最常用,精确匹配方法

语法骨架:

execution(修饰符? 返回值 包名.类名.方法名(参数列表) throws-异常?)
部分说明示例
*匹配任意单个部分* com..*.get*(..) — 任意返回值、任意包、get开头方法
..匹配任意多层com.edulearn.. — 匹配 com.edulearn 及其所有子包
+匹配子类UserService+ — 匹配 UserService 及其所有实现类
(..)任意参数无参到多参都匹配

实战表达式

// 1. 匹配 service 包下所有 public 方法,任意返回值
@Pointcut("execution(public * com.edulearn.service..*.*(..))")

// 2. 匹配 Controller 中所有返回 ResponseEntity 的方法
@Pointcut("execution(org.springframework.http.ResponseEntity com.edulearn.controller..*.*(..))")

// 3. 匹配第一个参数为 Long 类型的方法(常用于根据 ID 查询)
@Pointcut("execution(* com.edulearn.service..*.*(Long, ..))")

// 4. 匹配 save 或 update 开头的方法
@Pointcut("execution(* com.edulearn.service..*.save*(..)) || execution(* com.edulearn.service..*.update*(..))")

4.2 其他 6 种指示符速查

指示符匹配维度语法典型场景
within类/包范围within(com.edulearn.service.*)匹配某包下所有类的所有方法(不关心方法签名)
this代理对象类型this(com.edulearn.service.UserService)代理对象是某类型(注意:JDK代理时 this 是接口类型)
target目标对象类型target(com.edulearn.service.UserServiceImpl)目标对象是某类型(与 this 的区别见下文)
args方法参数类型args(java.lang.Long, ..)匹配第一个参数是 Long 的方法
@annotation方法上的注解@annotation(com.edulearn.annotation.RequireRole)匹配被指定注解标注的方法(可绑定参数)
@within类上的注解@within(org.springframework.stereotype.Service)匹配类上标注了 @Service 的所有方法
beanSpring Bean 名称bean(userService)匹配指定名称的 Bean(Spring 独有,非 AspectJ)

4.3 this vs target —— 面试高频细节

// 假设 UserServiceImpl implements UserService
// JDK 代理:代理对象类型 = UserService(接口)
// CGLIB 代理:代理对象类型 = UserServiceImpl 的子类

// this(UserService) → JDK 代理时匹配,CGLIB 代理时也匹配(子类 instanceof 接口)
// this(UserServiceImpl) → JDK 代理时不匹配(代理不是 UserServiceImpl),CGLIB 时匹配
// target(UserServiceImpl) → 无论如何都匹配(目标对象始终是 UserServiceImpl)

4.4 常见错误 Top 3

错误错误写法正确写法原因
包路径少了 ..execution(* com.edulearn.service.*.*(..))execution(* com.edulearn.service..*.*(..))service.* 只匹配 service 目录,不包含子包
@annotation 缺少全限定名@annotation(RequireRole)@annotation(com.edulearn.annotation.RequireRole)切点表达式不是 Java 代码,没有 import
修饰符不匹配execution(* *.privateMethod(..))Spring AOP 无法代理 private 方法private 方法无法被子类覆写,也无法被接口代理

五、通知类型与执行顺序

5.1 五种通知的完整对比

通知注解执行时机可否阻止执行可否获取返回值可否修改返回值
前置通知@Before目标方法执行前
返回通知@AfterReturning目标方法正常返回后
异常通知@AfterThrowing目标方法抛出异常后
最终通知@After方法执行后(finally)
环绕通知@Around包围目标方法

5.2 同一切面内的执行顺序

在这里插入图片描述

关键规则(面试必问):

  1. @Around 包裹一切:前置逻辑最先执行,后置逻辑最后执行
  2. @Before 在 proceed() 之后立即执行
  3. 方法正常返回:走 @AfterReturning → @After → @Around 后置
  4. 方法抛异常:走 @AfterThrowing → @After → @Around 后置
  5. @After 总是执行(类似于 finally 块),且 @AfterReturning/@AfterThrowing 与 @After 的顺序是固定的:前者先于后者
  6. @AfterReturning 和 @AfterThrowing 互斥

5.3 多切面执行顺序 —— @Order 洋葱模型

在这里插入图片描述

@Aspect @Component @Order(1)  // 数字越小,优先级越高 = 越外层
public class LogAspect { }

@Aspect @Component @Order(2)  // 中间层
public class AuthAspect { }

@Aspect @Component @Order(3)  // 最内层
public class CacheAspect { }

执行顺序(洋葱模型,从外到内再回到外):

请求进入:
  @Order(1) Around 前置 → @Order(2) Around 前置 → @Order(3) Around 前置
    → @Order(3) Before → @Order(2) Before → @Order(1) Before
    → 目标方法执行
    → @Order(1) AfterReturning → @Order(2) AfterReturning → @Order(3) AfterReturning
    → @Order(1) After → @Order(2) After → @Order(3) After
    → @Order(3) Around 后置 → @Order(2) Around 后置 → @Order(1) Around 后置
响应返回 ←

在这里插入图片描述

记忆口诀:@Order 值越小越「外面」—— 先进入、后退出,像剥洋葱。同一切面内 @AfterReturning 先于 @After。

5.4 失效场景

如果在 @Around 中吞掉了 proceed() 的返回值没有调用 proceed()

@Around("serviceLayer()")
public Object around(ProceedingJoinPoint pjp) {
    // ❌ 没有 return,也没有调用 proceed()
    //    后续的 @After 和 @AfterReturning 都不会执行
    return null; // 甚至返回 null 覆盖真实结果
}

规则:只要 proceed() 没被调用,目标方法就不执行,后续所有通知(@AfterReturning / @AfterThrowing / @After / @Around 后置)都不会触发。


六、源码走读:AOP 代理创建流程

面试中,当你说「Spring AOP 基于动态代理」后,面试官很可能追问:「代理是怎么创建的?」这一节逐层走读源码。

6.1 入口:AbstractAutoProxyCreator

Spring 通过 BeanPostProcessorpostProcessAfterInitialization 钩子在 Bean 初始化后创建代理。

// AbstractAutoProxyCreator.postProcessAfterInitialization()
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
    if (bean != null) {
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        if (this.earlyProxyReferences.remove(cacheKey) != bean) {
            // 关键调用:判断是否需要代理,需要则创建
            return wrapIfNecessary(bean, beanName, cacheKey);
        }
    }
    return bean;
}

核心判断:「这个 Bean 是否需要被代理?」——由 wrapIfNecessary 回答。

6.2 wrapIfNecessary:是否需要代理

protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
    // 1. 已经处理过?直接返回
    if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
        return bean;
    }
    // 2. 不应该被代理?跳过(Advice/Pointcut/Advisor/AopInfrastructureBean 自身)
    if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
        return bean;
    }
    // 3. 获取匹配的 Advisors(切面通知列表)
    Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
    if (specificInterceptors != DO_NOT_PROXY) {
        // 4. 有匹配的 Advisor → 创建代理!
        Object proxy = createProxy(bean.getClass(), beanName, specificInterceptors,
                                    new SingletonTargetSource(bean));
        this.proxyTypes.put(cacheKey, proxy.getClass());
        return proxy;
    }
    // 5. 无匹配 → 返回原始 Bean(不做代理)
    return bean;
}

关键步骤

  1. 检查是否已处理 / 是否为基础设施类
  2. 遍历所有 Advisor,看哪些切点匹配当前 Bean
  3. 如果有匹配 → createProxy(),没有 → 返回原 Bean

6.3 createProxy:创建代理对象

protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
        @Nullable Object[] specificInterceptors, TargetSource targetSource) {

    ProxyFactory proxyFactory = new ProxyFactory();
    proxyFactory.copyFrom(this);
    // 判断是否强制使用 CGLIB
    if (proxyFactory.isProxyTargetClass()) {
        if (Proxy.isProxyClass(beanClass)) {
            // JDK 代理的类再被代理 → 添加接口
            for (Class<?> ifc : beanClass.getInterfaces()) {
                proxyFactory.addInterface(ifc);
            }
        }
    } else {
        // 默认逻辑:有接口用 JDK,无接口自动升级 CGLIB
        // (由 DefaultAopProxyFactory 内部判断)
    }
    // ... 添加 Advisor、设置 TargetSource ...

    return proxyFactory.getProxy(getProxyClassLoader());
}

6.4 getProxy:选型与创建

// ProxyFactory.getProxy() → DefaultAopProxyFactory.createAopProxy()
public AopProxy createAopProxy(AdvisedSupport config) {
    if (!NativeDetector.inNativeImage() &&
        (config.isOptimize() || config.isProxyTargetClass()
         || hasNoUserSuppliedProxyInterfaces(config))) {
        return new ObjenesisCglibAopProxy(config);  // Spring 5.x+ 使用 Objenesis 实例化
    } else {
        return new JdkDynamicAopProxy(config);
    }
}

6.5 两种 AopProxy 的调用流程

JdkDynamicAopProxy.invoke()

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // 1. 获取拦截器链(Advisor 列表 → MethodInterceptor 列表)
    List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
    if (chain.isEmpty()) {
        // 无拦截器 → 直接反射调用目标方法
        return AopUtils.invokeJoinpointUsingReflection(target, method, args);
    }
    // 2. 构造 ReflectiveMethodInvocation,依次执行拦截链
    MethodInvocation invocation = new ReflectiveMethodInvocation(
        proxy, target, method, args, targetClass, chain);
    return invocation.proceed();
}

CglibAopProxy(DynamicAdvisedInterceptor)

public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) {
    // 1. 获取拦截器链
    List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
    if (chain.isEmpty()) {
        // 无拦截器 → 直接通过 methodProxy 调用(FastClass,无反射)
        return methodProxy.invoke(target, args);
    }
    // 2. 构造 CglibMethodInvocation
    CglibMethodInvocation invocation = new CglibMethodInvocation(
        proxy, target, method, args, targetClass, chain, methodProxy);
    return invocation.proceed(); // proceed() 依次执行拦截器链
}

6.6 拦截器链执行流程

ReflectiveMethodInvocation.proceed() 的核心逻辑:

public Object proceed() throws Throwable {
    // currentInterceptorIndex 从 -1 开始,每次 +1
    if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
        // 所有拦截器执行完毕 → 调用目标方法
        return invokeJoinpoint();
    }
    // 取出下一个拦截器,执行其 invoke(this)
    Object interceptorOrInterceptionAdvice =
        this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
    return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
}

5 种通知对应 MethodInterceptor 的实现类

通知MethodInterceptor 实现
@BeforeMethodBeforeAdviceInterceptor → 先调 advice.before(),再 proceed()
@AfterReturningAfterReturningAdviceInterceptor → 先 proceed() 得到结果,再调 advice.afterReturning()
@AfterThrowingAspectJAfterThrowingAdvice → try-proceed,catch 后调 invokeAdviceMethod()
@AfterAspectJAfterAdvice → try-finally,finally 中调 advice(一定执行)
@AroundAspectJAroundAdvice → 调 advice 的 around 方法,由开发者决定 proceed()

七、面试真题与追问链

本节模拟真实面试场景,每个考点逐层深入追问。

7.1 考点一:Spring AOP 的底层原理是什么?

第 1 层(初级回答)

Spring AOP 基于动态代理实现。如果目标对象实现了接口,用 JDK 动态代理;如果没有接口,用 CGLIB 代理。

第 2 层(追问:两种代理的区别?)

JDK 动态代理基于接口,通过 Proxy.newProxyInstance() 创建代理,调用时通过 InvocationHandler.invoke() → 反射 method.invoke() 调用目标方法。
CGLIB 基于子类继承,通过 Enhancer.create() 创建代理,调用时通过 MethodInterceptor.intercept() → FastClass 索引调用(无反射开销)。

第 3 层(追问:Spring 如何选择用哪个?)

DefaultAopProxyFactory.createAopProxy() 中判断:如果 config.isProxyTargetClass() 为 true,或目标类没有接口,就用 CGLIB。Spring Boot 2.x 默认 proxy-target-class=true,所以默认走 CGLIB。

第 4 层(追问:CGLIB 有什么限制?)

final 类无法被代理(不能继承),final/private/static 方法无法被增强(不能覆写)。另外 CGLIB 无法代理构造器调用(虽然会拦截子类构造器中的方法调用,但那是另一个话题)。

第 5 层(追问:你提到 CGLIB 用 FastClass 无反射,那它真的完全无反射吗?)

FastClass 通过方法签名到索引号的映射来避免反射。但 FastClass 本身是通过反射生成的(遍历目标类的方法)。创建时有一次反射成本,后续调用无反射。这是「空间换时间」策略。


7.2 考点二:Spring AOP 自调用失效,为什么?

场景描述

@Service
public class OrderService {
    @Transactional  // AOP 增强
    public void createOrder() {
        this.payOrder(); // 直接调用 this,不走代理!
    }

    @Transactional  // AOP 增强
    public void payOrder() { /* ... */ }
}

第 1 层

this.payOrder() 是直接调用当前对象的方法,没有经过代理对象,所以 AOP 拦截不到。

第 2 层(追问:为什么 this 不走代理?)

Spring AOP 的代理是「外套」——代理对象内部持有一个 target(原始对象)。外部调用 proxy.createOrder() 时经过代理外套,但 createOrder 内部的 this 指向的是 target(原始对象),它的方法调用直接发生在原始对象上,完全绕过了代理。

第 3 层(追问:怎么解决?)

方案一:自己注入自己(推荐,简单有效)

@Service
public class OrderService {
    @Autowired
    private OrderService self; // Spring 注入的是代理对象

    public void createOrder() {
        self.payOrder(); // 走代理!
    }
}

方案二:AopContext.currentProxy()(需要配置 exposeProxy=true

@EnableAspectJAutoProxy(exposeProxy = true)  // 配置类
@Service
public class OrderService {
    public void createOrder() {
        ((OrderService) AopContext.currentProxy()).payOrder();
    }
}

方案三:重构,将 payOrder 提取到独立的 Service。

第 4 层(追问:@Transactional 自调用失效是最常见的坑,原理一样吗?)

完全一样。@Transactional 本身就是通过 AOP 实现的(TransactionInterceptor 是一个 Advisor),所以自调用同样会让事务不生效。


7.3 考点三:@Transactional 为什么依赖 AOP?

第 1 层

Spring 的事务管理通过 AOP 实现。@Transactional 注解的方法被 TransactionInterceptor 拦截,在方法前开启事务,方法正常返回后提交,方法抛异常后回滚。

第 2 层(追问:TransactionInterceptor 在拦截器链中的位置?)

TransactionInterceptor 是一个 MethodInterceptor,它被封装在 BeanFactoryTransactionAttributeSourceAdvisor 中。和其他 Advisor 一样,在 AbstractAutoProxyCreator.wrapIfNecessary() 阶段被匹配和应用。

第 3 层(追问:如果同时有 @Transactional 和自定义 @Around,执行顺序?)

@Order 或 Advisor 的加载顺序决定。通常事务 Advisor 的优先级较高(在自定义切面之前),可以通过 @EnableTransactionManagement(order = N) 调整。

第 4 层(追问:事务传播机制和 AOP 有什么关系?)

事务传播机制(REQUIRED / REQUIRES_NEW / NESTED 等)通过 TransactionInterceptorinvoke() 中判断当前是否存在事务,决定是加入还是新建。每次方法调用都经过拦截器链,事务状态通过 TransactionSynchronizationManager 在 ThreadLocal 中维护。


7.4 追问链速查表

考点L1 基础L2 进阶L3 深入L4 发散
AOP 原理动态代理JDK vs CGLIB 区别DefaultAopProxyFactory 选型逻辑CGLIB 限制(final/构造器)
自调用失效this 不走代理代理=外套+target自己注入自己 / AopContext@Transactional 同原理
@TransactionalAOP 实现事务TransactionInterceptor 机制多 Advisor 排序事务传播与 ThreadLocal
循环依赖三级缓存AOP 提前曝光getEarlyBeanReference@Async 与循环依赖的冲突

必背概念速查表

概念一句话定义面试金句
AOP面向切面编程,将横切关注点模块化「在不修改源码的情况下动态添加功能」
Aspect 切面切点 + 通知的组合「Aspect = Where + What」
Pointcut 切点定义通知在何处执行「从无数 JoinPoint 中筛选目标」
Advice 通知在切点处执行的逻辑「5 种:Before / After / Around / AfterReturning / AfterThrowing」
JoinPoint 连接点可插入切面的候选位置「Spring AOP 只支持方法执行」
Weaving 织入将切面应用到目标对象「Spring = 运行时动态代理织入」
JDK 动态代理基于接口的代理「反射 method.invoke(),必须实现接口」
CGLIB 代理基于子类的代理「FastClass 索引调用,不能是 final 类」
自调用失效this 调用不走代理「代理=外套,this 指向原始 target」
@Order多切面排序「值越小越外层,洋葱模型」
proceed()执行目标方法「不调 proceed() = 后续通知全不触发」
exposeProxy暴露当前代理对象「AopContext.currentProxy() 解决自调用」

下期速递第五期「Spring 事务深度解析」—— 事务传播机制 7 种行为逐一带案例、事务失效的 5 大场景、@Transactional 源码走读,以及「为什么加了 @Transactional 却没回滚?」的灵魂追问。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值