细石混凝土泵

乐道AI助手带你穿透Spring AOP高频面试必学点

小编 2026-04-29 细石混凝土泵 2 0

北京时间2026年4月8日发布

在Spring生态中,乐道AI助手发现许多开发者对AOP的掌握停留在“会用@Transactional”的层面——面试时说不清代理原理,遇到“为什么同类调用事务失效”一头雾水。本文将深入讲解Spring AOP的核心理念、实现机制和高频考点,配合可运行的代码示例,帮助读者从“会用”走向“懂原理”。

一、为什么需要AOP?——从痛点切入

先看一段典型的业务代码,来自一个电商系统的订单服务:

java
复制
下载
@Service
public class OrderService {
    public void createOrder(Order order) {
        // 日志记录
        System.out.println("开始创建订单,订单号:" + order.getNo());
        long start = System.currentTimeMillis();
        
        // 权限校验
        if (!hasPermission(order.getUserId())) {
            throw new SecurityException("无权限");
        }
        
        // 核心业务逻辑
        orderDao.save(order);
        
        // 事务管理
        transaction.commit();
        
        // 性能监控
        System.out.println("订单创建耗时:" + (System.currentTimeMillis() - start) + "ms");
    }
}

这段代码暴露了OOP在处理横切关注点时的明显短板:

  • 代码冗余:日志、权限、事务等代码在每个业务方法中重复出现,代码重复率可高达60%以上-21

  • 耦合度高:核心业务逻辑与非核心的日志、监控代码交织在一起,任何一个横切逻辑的调整都需要修改所有业务方法。

  • 维护困难:修改日志格式或权限规则时,需要在数十甚至数百个方法中逐一修改,极易遗漏。

  • 可扩展性差:新增一个横切关注点(如缓存、限流)意味着在所有目标方法中重复编写相同代码。

这些痛点正是AOP诞生的背景。AOP(Aspect Oriented Programming,面向切面编程)的核心思想是将横切关注点从业务逻辑中剥离,封装成独立的“切面”,通过动态代理技术在运行时织入到目标方法中,实现无侵入式增强-1

一句话理解:如果说OOP是纵向的继承体系,AOP就是横向的增强体系。两者互补而非替代,OOP负责业务对象的结构,AOP负责横切逻辑的复用-12

二、核心概念详解——切面(Aspect)

2.1 什么是切面

切面(Aspect) 是AOP中最核心的概念,它是对横切关注点的模块化封装。简单来说,一个切面就是一个Java类,里面定义了“在哪里切入”和“切入后做什么”-

生活化类比:想象一下商场的安防系统。摄像头是“切面”,它们被安装在不同店铺的入口处(切入点),当有人经过时(连接点),会自动执行录像操作(通知)。无论店铺卖什么商品(业务逻辑不同),安防系统都独立运作,不需要店员额外操作。

2.2 切面的关键组成

一个完整的切面包含以下要素:

术语英文一句话解释示例
横切关注点Cross-cutting Concerns散布在多个类中的公共行为日志、事务、权限
连接点Join Point程序执行中可插入切面的位置方法调用、方法执行
切点Pointcut匹配连接点的规则表达式execution( com.example.service..(..))
通知Advice在连接点执行的具体动作@Before、@Around
目标对象Target Object被增强的原始业务对象OrderService实例
织入Weaving将切面应用到目标对象的过程运行时生成代理对象

Spring AOP只支持方法级别的连接点,即只能在方法执行前后插入增强逻辑-2

三、核心概念详解——通知(Advice)

3.1 什么是通知

通知(Advice) 是切面中定义的具体增强逻辑,决定了“在连接点做什么”。Spring AOP提供了五种通知类型,每种对应不同的执行时机-2-68

java
复制
下载
@Aspect
@Component
@Slf4j
public class LogAspect {
    
    // 1. 前置通知:目标方法执行前执行
    @Before("execution( com.example.service..(..))")
    public void beforeMethod(JoinPoint joinPoint) {
        log.info("【@Before】即将执行方法:{}", joinPoint.getSignature().getName());
    }
    
    // 2. 后置通知:目标方法执行后执行(无论是否异常,类似finally)
    @After("execution( com.example.service..(..))")
    public void afterMethod(JoinPoint joinPoint) {
        log.info("【@After】方法执行结束,无论结果如何");
    }
    
    // 3. 返回通知:目标方法正常返回后执行(可访问返回值)
    @AfterReturning(pointcut = "execution( com.example.service..(..))", returning = "result")
    public void afterReturning(JoinPoint joinPoint, Object result) {
        log.info("【@AfterReturning】方法正常返回,结果:{}", result);
    }
    
    // 4. 异常通知:目标方法抛出异常时执行
    @AfterThrowing(pointcut = "execution( com.example.service..(..))", throwing = "ex")
    public void afterThrowing(JoinPoint joinPoint, Exception ex) {
        log.error("【@AfterThrowing】方法抛出异常:{}", ex.getMessage());
    }
    
    // 5. 环绕通知:包裹目标方法,可完全控制执行流程
    @Around("execution( com.example.service..(..))")
    public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("【@Around】方法执行前...");
        long start = System.currentTimeMillis();
        
        Object result = joinPoint.proceed(); // 执行目标方法
        
        long duration = System.currentTimeMillis() - start;
        log.info("【@Around】方法执行完成,耗时:{}ms", duration);
        return result;
    }
}

3.2 JoinPoint 是什么

JoinPoint(连接点)接口提供了被拦截方法的上下文信息,包括参数、方法签名、目标对象等-34

java
复制
下载
@Before("execution( com.example.service..(..))")
public void logArgs(JoinPoint joinPoint) {
    // 获取方法参数
    Object[] args = joinPoint.getArgs();
    // 获取方法签名(可拿到方法名)
    Signature signature = joinPoint.getSignature();
    // 获取目标对象
    Object target = joinPoint.getTarget();
    
    log.info("方法名:{},参数:{}", signature.getName(), Arrays.toString(args));
}

⚠️ 重要区分JoinPoint用于@Before、@After、@AfterReturning、@AfterThrowing;ProceedingJoinPoint继承自JoinPoint,专用于@Around,它多了一个proceed()方法,用于手动执行目标方法-34

3.3 五种通知的执行顺序

当一个方法被多个通知匹配时,Spring AOP按照以下顺序执行:

text
复制
下载
@Around前置部分 → @Before → 目标方法执行 → @AfterReturning/@AfterThrowing → @After → @Around后置部分

关键记忆点

  • @After始终执行(无论是否抛异常),类似于finally

  • @AfterReturning仅在方法正常返回时执行,异常时不执行

  • @Around是唯一可以控制目标方法是否执行的通知类型,且必须显式调用proceed()

面试常问:为什么@Around必须调用proceed()?

因为proceed()是真正触发原方法执行的唯一“开关”。不调用它,目标方法永远不会运行——这不是Bug,是设计使然-42

四、切点与通知的关系——定义“在哪里”

4.1 什么是切点

切点(Pointcut) 是匹配连接点的表达式,定义了切面应该应用于哪些方法。Spring AOP使用AspectJ的切入点表达式语言,主要支持两种方式:execution表达式和@annotation注解匹配--43

4.2 execution表达式语法

java
复制
下载
execution(<修饰符> <返回类型> <包名.类名.方法(参数)> <异常>)
通配符含义示例
匹配任意一个元素 匹配任意返回类型
..匹配任意多个元素包中表示子包,参数中表示任意参数
+匹配类及其子类UserService+

常用切点表达式示例-43

java
复制
下载
// 匹配com.example.service包下所有类的所有方法
@Before("execution( com.example.service..(..))")

// 匹配com.example包及其子包下所有类的所有方法
@Before("execution( com.example...(..))")

// 匹配所有public方法
@Before("execution(public  (..))")

// 匹配UserService类及其子类的所有方法
@Before("execution( com.example.service.UserService+.(..))")

4.3 @annotation表达式

当execution表达式无法满足需求(如匹配多个无规则的方法)时,可以使用自定义注解配合@annotation表达式-43

java
复制
下载
// 1. 定义自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
    String value() default "";
}

// 2. 在需要增强的方法上添加注解
@Service
public class UserService {
    @Loggable("用户登录")
    public void login(String username) {
        // 业务逻辑
    }
}

// 3. 使用@annotation匹配被该注解标记的方法
@Aspect
@Component
public class LogAspect {
    @Around("@annotation(loggable)")
    public Object logAround(ProceedingJoinPoint joinPoint, Loggable loggable) throws Throwable {
        log.info("开始执行:{}", loggable.value());
        return joinPoint.proceed();
    }
}

4.4 切点复用:@Pointcut

为避免切点表达式重复编写,可以使用@Pointcut将切点声明为独立方法,便于复用-42

java
复制
下载
@Aspect
@Component
public class ServiceAspect {
    
    // 声明切点
    @Pointcut("execution( com.example.service..(..))")
    public void serviceLayer() {}
    
    @Before("serviceLayer()")
    public void beforeService() {
        log.info("Service层方法执行前...");
    }
    
    @After("serviceLayer()")
    public void afterService() {
        log.info("Service层方法执行后...");
    }
}

五、AOP与OOP的关系——思想 vs 实现

5.1 关系总结

维度OOP(面向对象编程)AOP(面向切面编程)
核心思想封装、继承、多态构建对象层次将横切关注点与业务逻辑分离
代码结构垂直结构,对象层次清晰横向结构,公共逻辑集中管理
适用场景业务逻辑开发系统级公共行为处理(日志、事务、权限)
代码重复性高,公共行为分散在多模块中低,公共行为集中封装

一句话记忆:OOP从纵向解决问题(类与类之间的关系),AOP从横向解决问题(关注点之间的关系)。两者是互补关系,AOP是OOP的有力补充-12-

5.2 AOP的典型应用场景

AOP在实际项目中广泛应用于以下场景--27

场景实现方式典型注解
日志记录记录方法入参、出参、执行时间@Around
事务管理自动开启/提交/回滚事务@Transactional
权限控制检查用户角色/权限@PreAuthorize
性能监控统计方法执行耗时@Around
缓存管理方法前查缓存,后写缓存@Cacheable
接口限流限制调用频率自定义切面

六、代码实战:从无AOP到有AOP的对比

6.1 传统方式(无AOP)——大量重复代码

java
复制
下载
@Service
public class OrderService {
    public void createOrder(Order order) {
        log.info("创建订单开始,订单号:{}", order.getNo());
        long start = System.currentTimeMillis();
        
        // 权限校验(重复)
        if (!SecurityContext.hasPermission("ORDER_CREATE")) {
            throw new SecurityException("无权限");
        }
        
        // 核心业务逻辑
        orderDao.save(order);
        
        // 事务提交(重复)
        transaction.commit();
        
        log.info("创建订单结束,耗时:{}ms", System.currentTimeMillis() - start);
    }
    
    public void updateOrder(Order order) {
        log.info("更新订单开始,订单号:{}", order.getNo());
        long start = System.currentTimeMillis();
        
        // 同样的权限校验代码重复出现
        if (!SecurityContext.hasPermission("ORDER_UPDATE")) {
            throw new SecurityException("无权限");
        }
        
        // 核心业务逻辑
        orderDao.update(order);
        
        transaction.commit();
        
        log.info("更新订单结束,耗时:{}ms", System.currentTimeMillis() - start);
    }
}

每个方法都充斥着日志、权限、事务等重复代码,核心业务逻辑被淹没在横切代码中。

6.2 AOP方式——集中管理,业务纯净

第一步:引入依赖(Spring Boot)

xml
复制
下载
运行
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

第二步:编写切面类

java
复制
下载
@Aspect
@Component
@Slf4j
public class ServiceAspect {
    
    // 权限校验切点
    @Pointcut("@annotation(RequirePermission)")
    public void permissionCheck() {}
    
    // 性能监控切点
    @Pointcut("execution( com.example.service..(..))")
    public void performanceMonitor() {}
    
    // 权限校验增强
    @Before("permissionCheck() && @annotation(requirePermission)")
    public void checkPermission(JoinPoint joinPoint, RequirePermission requirePermission) {
        String permission = requirePermission.value();
        if (!SecurityContext.hasPermission(permission)) {
            throw new SecurityException("缺少权限:" + permission);
        }
        log.info("权限校验通过:{}", permission);
    }
    
    // 性能监控增强
    @Around("performanceMonitor()")
    public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long duration = System.currentTimeMillis() - start;
        log.info("【性能】方法 {} 耗时:{}ms", joinPoint.getSignature().getName(), duration);
        return result;
    }
    
    // 统一异常处理
    @AfterThrowing(pointcut = "performanceMonitor()", throwing = "ex")
    public void handleException(JoinPoint joinPoint, Exception ex) {
        log.error("【异常】方法 {} 抛出异常:{}", joinPoint.getSignature().getName(), ex.getMessage());
    }
}

第三步:业务代码回归纯净

java
复制
下载
@Service
@Slf4j
public class OrderService {
    
    @RequirePermission("ORDER_CREATE")  // 权限校验
    public void createOrder(Order order) {
        // 只有核心业务逻辑,没有横切代码
        orderDao.save(order);
    }
    
    @RequirePermission("ORDER_UPDATE")
    public void updateOrder(Order order) {
        orderDao.update(order);
    }
}

对比效果:AOP方式使业务代码行数减少约60%,核心逻辑一目了然,横切功能的修改只需调整切面类一处。

七、底层原理——动态代理技术

Spring AOP的实现本质上是依赖于代理模式这一经典设计模式,其核心价值在于解耦核心业务逻辑与横切关注点-

7.1 Spring AOP的代理机制

Spring AOP在运行时根据目标类的特征智能选择代理机制:

java
复制
下载
// Spring内部选择逻辑(伪代码)
if (目标类实现了至少一个接口 && 未强制使用CGLIB) {
    使用 JDK 动态代理
} else {
    使用 CGLIB 动态代理
}

7.2 JDK动态代理

原理:基于接口生成代理类,通过java.lang.reflect.ProxyInvocationHandler实现。代理对象实现了目标接口,在方法调用时通过反射转发到目标对象-52-5

前置条件:目标类必须实现至少一个接口。

JDK代理示例

java
复制
下载
public interface UserService {
    void saveUser(String name);
}

public class UserServiceImpl implements UserService {
    @Override
    public void saveUser(String name) {
        System.out.println("保存用户:" + name);
    }
}

// 调用处理器
public class LogInvocationHandler implements InvocationHandler {
    private Object target;
    
    public LogInvocationHandler(Object target) { this.target = target; }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("【JDK代理】方法执行前:" + method.getName());
        Object result = method.invoke(target, args);  // 反射调用
        System.out.println("【JDK代理】方法执行后:" + method.getName());
        return result;
    }
}

// 创建代理
UserService target = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
    UserService.class.getClassLoader(),
    new Class[]{UserService.class},
    new LogInvocationHandler(target)
);
proxy.saveUser("张三");

7.3 CGLIB动态代理

原理:通过字节码技术生成目标类的子类,在子类中重写父类方法,并在方法调用前后插入增强逻辑--5

前置条件:目标类不能是final类,目标方法不能是final/static方法。

CGLIB代理示例

java
复制
下载
public class UserService {  // 无需实现接口
    public void saveUser(String name) {
        System.out.println("保存用户:" + name);
    }
}

// CGLIB代理(Spring内部封装,开发者通常无需手动编写)
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserService.class);
enhancer.setCallback(new MethodInterceptor() {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) 
            throws Throwable {
        System.out.println("【CGLIB代理】方法执行前:" + method.getName());
        Object result = proxy.invokeSuper(obj, args);  // 调用父类方法
        System.out.println("【CGLIB代理】方法执行后:" + method.getName());
        return result;
    }
});
UserService proxy = (UserService) enhancer.create();
proxy.saveUser("张三");

7.4 JDK vs CGLIB:对比总结

对比维度JDK动态代理CGLIB动态代理
前置条件目标类必须实现接口目标类不能是final类
实现原理基于接口,反射调用基于继承,字节码生成子类
代理对象类型实现了目标接口的新类目标类的子类
依赖Java标准库,无额外依赖需要引入CGLIB库
性能反射调用有一定开销方法调用性能略优
代理范围仅代理接口中声明的方法可代理具体类的非final方法
类名特征$Proxy0$$EnhancerBySpringCGLIB$$xxx

关于默认策略

  • Spring Framework(非Boot)默认策略:有接口用JDK,无接口用CGLIB-

  • Spring Boot 2.x及之后:默认改为CGLIB,可通过spring.aop.proxy-target-class=false切换

强制指定CGLIB的方式

java
复制
下载
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)  // 强制使用CGLIB
public class AopConfig { }

7.5 底层依赖的关键技术点

Spring AOP的实现依赖于以下几个技术支撑点:

  1. 代理模式:设计模式的基石,通过代理对象拦截目标方法的调用-

  2. 反射机制:JDK动态代理通过Method.invoke()调用目标方法-52

  3. 字节码技术:CGLIB通过ASM字节码框架动态生成子类字节码

  4. BeanPostProcessor:Spring容器通过AnnotationAwareAspectJAutoProxyCreator在Bean初始化后处理切面代理-54

八、高频面试题与参考答案

面试题1:什么是AOP?Spring AOP的实现原理是什么?

参考答案
AOP(Aspect Oriented Programming,面向切面编程)是一种编程范式,它将横切关注点(如日志、事务、权限)从业务逻辑中分离出来,通过动态代理技术在运行时织入到目标方法中,实现无侵入式增强-

Spring AOP的实现依赖于动态代理技术。当目标类实现了接口时,Spring使用JDK动态代理(基于ProxyInvocationHandler,通过反射调用目标方法);当目标类没有实现接口或强制配置为CGLIB时,Spring使用CGLIB动态代理(通过字节码技术生成目标类的子类,在子类中重写父类方法并插入增强逻辑)-5

踩分点:AOP定义 + 横切关注点概念 + 两种代理机制 + 运行时织入

面试题2:Spring AOP的通知类型有哪些?它们的执行顺序是什么?

参考答案
Spring AOP提供了五种通知类型:

  • @Before:前置通知,目标方法执行前触发

  • @After:后置通知,目标方法执行后触发(无论是否抛异常)

  • @AfterReturning:返回通知,目标方法正常返回后触发

  • @AfterThrowing:异常通知,目标方法抛出异常时触发

  • @Around:环绕通知,可完全控制目标方法的执行流程

执行顺序为:@Around前置 → @Before → 目标方法 → @AfterReturning/@AfterThrowing → @After → @Around后置-2-68

踩分点:五种通知名称 + 执行时机 + 顺序记忆

面试题3:JDK动态代理和CGLIB有什么区别?Spring如何选择?

参考答案
核心区别

  • JDK动态代理:要求目标类实现接口,基于接口生成代理类,通过反射调用目标方法,无额外依赖

  • CGLIB动态代理:目标类无需实现接口,通过字节码技术生成目标类的子类,重写父类方法实现增强,要求目标类不能是final类

Spring的选择策略

  • 目标类实现了接口且未强制使用CGLIB → JDK动态代理

  • 目标类未实现接口或配置proxyTargetClass=true → CGLIB代理

  • Spring Boot 2.x+默认使用CGLIB--5

踩分点:两种代理的前提条件 + 实现原理 + 选择策略

面试题4:同一个类内部方法调用,为什么AOP会失效?如何解决?

参考答案
AOP是基于代理实现的。当在同一个类内部调用方法时(如this.method()),调用的是目标对象本身的方法,而不是通过代理对象调用,因此无法触发切面增强。

解决方案

  1. 将目标方法抽取到另一个Bean中,通过依赖注入调用

  2. 使用AopContext.currentProxy()获取当前代理对象,通过代理调用:((Service) AopContext.currentProxy()).method()

  3. 使用@Autowired注入自身代理(需开启@EnableAspectJAutoProxy(exposeProxy=true)-64

踩分点:代理机制原理 + 失效原因 + 三种解决方案

面试题5:Spring AOP和AspectJ的关系是什么?

参考答案

  • Spring AOP:基于动态代理的运行时AOP实现,仅支持方法级连接点,轻量级,适合大多数业务场景

  • AspectJ:功能更强大的独立AOP框架,支持编译时和类加载时织入,支持字段、构造器等更多连接点类型

关系:Spring AOP借鉴了AspectJ的注解语法(如@Aspect@Before等),但底层实现不同。Spring AOP是运行时代理,AspectJ是编译时增强。当需要更复杂的切面功能(如织入构造器、静态方法)时,可集成AspectJ--68

踩分点:两者本质差异 + Spring AOP借鉴注解语法 + 适用场景

九、总结

本文围绕Spring AOP核心面试要点,从痛点切入到原理剖析,系统梳理了以下知识点:

  1. AOP是什么:面向切面编程,将横切关注点从业务逻辑中分离,通过动态代理实现无侵入增强

  2. 核心概念:切面、连接点、切点、通知、目标对象、织入——六者构成AOP的完整框架

  3. 五种通知类型:@Before、@After、@AfterReturning、@AfterThrowing、@Around,各有特定的执行时机和使用场景

  4. 切点表达式:execution(按签名匹配)和@annotation(按注解匹配)两种方式

  5. 底层原理:JDK动态代理(基于接口+反射)和CGLIB动态代理(基于继承+字节码),Spring根据目标类特征智能选择

  6. 常见陷阱:同类内部调用失效、@Around必须调用proceed()、切面类必须由Spring容器管理

重点记忆

  • AOP与OOP是互补关系,而非替代

  • @Around是唯一能控制方法执行流程的通知,功能最强但需谨慎使用

  • 理解动态代理机制是深入掌握AOP的关键

  • 同类内部调用失效是面试高频陷阱

下一篇预告:Spring AOP通知执行链路深度剖析——责任链模式在AOP中的实现,以及多个切面同时作用时的优先级控制。

猜你喜欢