1. 第一篇:Spring IoC容器——控制反转的本质与Bean的生命周期
  2. 第二篇:Spring AOP——动态代理与切面编程的底层原理
  3. 第三篇:SpringMVC——一个HTTP请求在Spring中经历了什么?
  4. 第四篇:SpringBoot自动配置——约定大于配置的底层原理
  5. 第五篇:Spring事务管理——@Transactional的底层实现与失效场景
  6. 第六篇:Spring用了哪些设计模式?——从单例到代理,拆解框架中的经典设计
  7. 第七篇:Spring扩展点——如何优雅地介入Bean的创建流程
  8. 第八篇:Spring与微服务——从SpringBoot到SpringCloud的演进

前言

在上一篇文章中,我们拆解了Spring IoC容器——它解决了对象创建和依赖管理的问题。但有一个场景是IoC管不了的:横切关注点

比如你写了一个Service,需要在每个方法执行前打印日志、校验权限、开启事务。如果把这些代码硬写进每个方法里,它们会像藤蔓一样缠绕在核心业务逻辑上。AOP就是用来解决这个问题的——把这些"非核心但必须做"的事从业务代码中剥离出去,放到一个单独的地方统一管理。

面试中,AOP是Spring的第二大基石:

“JDK动态代理和CGLIB有什么区别?Spring默认用哪个?”
“AOP在项目中怎么用的?事务、日志、权限校验分别是怎么实现的?”
“多个切面同时作用于一个方法时,执行顺序是怎样的?”

本文从代理模式出发,拆解Spring AOP的底层原理,最终落到项目中的实战应用。

本文核心问题:

  1. AOP解决了什么问题?OOP为什么做不到?
  2. JDK动态代理和CGLIB有什么区别?Spring默认用哪个?
  3. Spring AOP的切面、切点、通知、连接点分别是什么?
  4. 多个切面同时作用于一个方法时,执行顺序是怎样的?为什么说"先入后出"?
  5. AOP在项目中用在哪些场景?事务、权限校验分别是如何实现的?
  6. Spring AOP和AspectJ有什么区别?

读完本文,你将对Spring AOP拥有从底层原理到项目实战的完整理解。


一、AOP解决了什么问题?

疑问:OOP(面向对象编程)已经很成熟了,为什么还需要AOP?

回答:OOP擅长纵向拆分功能模块,但处理不了"横切"在多个模块中的公共行为。AOP专门解决这类横切关注点——把分散的重复代码抽离到一个地方统一管理。

1.1 没有AOP时

public class OrderService {
    public void createOrder(Order order) {
        log.info("开始创建订单");           // ← 日志代码
        checkPermission("order:create");    // ← 权限代码
        transaction.begin();                // ← 事务代码
        try {
            orderMapper.insert(order);      // ← 核心业务
            transaction.commit();
        } catch (Exception e) {
            transaction.rollback();
            throw e;
        }
        log.info("订单创建完成");           // ← 日志代码
    }
    
    public void cancelOrder(Long orderId) {
        log.info("开始取消订单");           // ← 同样的日志代码
        checkPermission("order:cancel");    // ← 同样的权限代码
        transaction.begin();                // ← 同样的事务代码
        try {
            orderMapper.updateStatus(orderId, "CANCELLED");
            transaction.commit();
        } catch (Exception e) {
            transaction.rollback();
            throw e;
        }
        log.info("订单取消完成");           // ← 同样的日志代码
    }
}

问题:日志、权限、事务这些代码在每个方法中反复出现,和核心业务逻辑混在一起。修改日志格式需要改几十个方法,漏掉一个就出现不一致。业务代码被这些"辅助代码"撑得臃肿,核心逻辑反而不突出。

1.2 有AOP时

public class OrderService {
    // 只有核心业务,干净、简洁
    @RequirePermission("order:create")
    @Transactional
    public void createOrder(Order order) {
        orderMapper.insert(order);
    }
    
    @RequirePermission("order:cancel")
    @Transactional
    public void cancelOrder(Long orderId) {
        orderMapper.updateStatus(orderId, "CANCELLED");
    }
}

// 日志、权限、事务的逻辑都抽离到各自的切面中
@Aspect
public class LogAspect {
    @Around("execution(* com.example.service.*.*(..))")
    public Object log(ProceedingJoinPoint pjp) throws Throwable {
        log.info("开始执行: " + pjp.getSignature().getName());
        Object result = pjp.proceed();  // 执行原方法
        log.info("执行完成: " + pjp.getSignature().getName());
        return result;
    }
}

AOP把横切关注点从业务代码中剥离出去——业务代码只做业务,日志、权限、事务这些"辅助功能"由各自独立的切面统一处理。新增一个Service方法时不需要写一行重复的日志或权限代码。


二、AOP的底层实现——动态代理

疑问:Spring AOP是怎么做到"在方法前后自动插入逻辑"的?

回答:通过动态代理——在运行时动态生成一个代理对象包装目标对象,在代理对象中插入增强逻辑,再调用目标对象。

Spring AOP采用两种动态代理方式:JDK动态代理和CGLIB。

2.1 JDK动态代理

JDK动态代理要求目标对象必须实现一个接口。代理对象和被代理对象实现同一个接口,代理对象在方法调用前后插入增强逻辑。

public interface OrderService {
    void createOrder(Order order);
}

public class OrderServiceImpl implements OrderService {
    public void createOrder(Order order) {
        orderMapper.insert(order);
    }
}

// JDK动态代理(简化原理)
public class JdkProxy implements InvocationHandler {
    private Object target;  // 被代理的真实对象
    
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        log.info("开始执行: " + method.getName());     // 前置增强
        Object result = method.invoke(target, args);   // 调用真实方法
        log.info("执行完成: " + method.getName());     // 后置增强
        return result;
    }
}

核心Proxy.newProxyInstance() 在运行时动态生成一个实现目标接口的代理类。调用代理对象的任何方法时,都会经过 invoke 方法——AOP的增强逻辑就是在这里插入的。

2.2 CGLIB

CGLIB不要求目标对象实现接口。它通过继承目标类生成一个子类,在子类中重写父类方法,在重写的方法中插入增强逻辑。

// CGLIB代理(简化原理)
public class CglibProxy implements MethodInterceptor {
    private Object target;
    
    public Object intercept(Object obj, Method method, Object[] args, 
                           MethodProxy proxy) throws Throwable {
        log.info("开始执行: " + method.getName());     // 前置增强
        Object result = method.invoke(target, args);    // 调用父类方法
        log.info("执行完成: " + method.getName());     // 后置增强
        return result;
    }
}

核心:CGLIB通过ASM字节码技术,在运行时动态生成目标类的子类。子类重写父类的所有非final方法,在重写的方法中先调用AOP拦截逻辑,再调用父类方法。

2.3 JDK动态代理 vs CGLIB

维度 JDK动态代理 CGLIB
要求 必须实现接口 不要求接口,但不能是final类
代理方式 实现同一接口 生成子类继承目标类
性能 创建代理对象快,调用略有反射开销 创建慢(需生成字节码),调用快
限制 只能代理接口中定义的方法 无法代理final方法和static方法

2.4 Spring的代理选择策略

Spring Boot 2.x 默认策略:
  AOP默认用CGLIB(即使目标对象实现了接口)

为什么?
  CGLIB不需要接口,使用更简单
  避免因接口缺失导致的代理失败
  开发者不需要额外关心接口设计

也可以通过配置改回JDK动态代理:
  spring.aop.proxy-target-class=false

面试常问:如果你的Bean实现了接口,Spring一定会用JDK动态代理吗?答案是——Spring Boot 2.x及之后默认用CGLIB,不看你有没有接口。如果想要JDK动态代理,需要显式配置。


三、AOP的核心概念——切面、切点、通知、连接点

疑问:面试总问"切面和切点的区别",怎么回答?

回答:四个概念各司其职——连接点是"潜在的目标",切点是"筛选规则",通知是"要执行的动作",切面是"规则+动作的组合体"。

3.1 四个核心概念

概念 通俗理解 代码示例
连接点 所有可能被增强的方法(潜在目标) Service中的所有方法
切点 筛选哪些连接点需要被增强的表达式 execution(* com.example.service.*.*(..))
通知 在切点的什么时机执行什么操作 @Before、@After、@Around注解的方法
切面 切点 + 通知的组合体(筛选规则+执行动作) @Aspect修饰的类

3.2 五种通知类型

@Aspect
public class LogAspect {
    
    // @Before:目标方法执行前
    @Before("execution(* com.example.service.*.*(..))")
    public void before(JoinPoint joinPoint) {
        log.info("开始执行: " + joinPoint.getSignature().getName());
    }
    
    // @After:目标方法执行后(无论成功还是异常)
    @After("execution(* com.example.service.*.*(..))")
    public void after(JoinPoint joinPoint) {
        log.info("执行结束: " + joinPoint.getSignature().getName());
    }
    
    // @AfterReturning:目标方法执行成功返回后
    @AfterReturning(value = "execution(* com.example.service.*.*(..))", 
                    returning = "result")
    public void afterReturning(JoinPoint joinPoint, Object result) {
        log.info("执行成功,返回: " + result);
    }
    
    // @AfterThrowing:目标方法抛出异常后
    @AfterThrowing(value = "execution(* com.example.service.*.*(..))", 
                   throwing = "ex")
    public void afterThrowing(JoinPoint joinPoint, Exception ex) {
        log.error("执行异常: " + ex.getMessage());
    }
    
    // @Around:目标方法前后都执行(最强大的通知,包裹整个方法调用)
    @Around("execution(* com.example.service.*.*(..))")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        log.info("开始执行: " + pjp.getSignature().getName());
        Object result = pjp.proceed();  // 手动调用目标方法
        log.info("执行完成: " + pjp.getSignature().getName());
        return result;
    }
}
通知类型 执行时机 能否获取返回值 能否捕获异常
@Before 目标方法执行前
@After 目标方法执行后(类似finally)
@AfterReturning 目标方法成功返回后 能(通过returning属性)
@AfterThrowing 目标方法抛出异常后 能(通过throwing属性)
@Around 包裹目标方法,前后都可执行 能(直接拿到返回值) 能(try-catch)

3.3 @Around和其他通知的执行顺序

同一个切面中,不同通知的执行顺序:

@Around 的前置部分
  → @Before
    → 目标方法执行
  → @AfterReturning(成功时)/ @AfterThrowing(异常时)
  ← @After(finally)
← @Around 的后置部分

四、多个切面的执行顺序——为什么"先入后出"?

疑问:两个切面同时作用于一个方法,谁先执行?

回答:多个切面按@Order注解的值从小到大顺序执行前置通知,后置通知逆序执行——“先入后出”。

4.1 两个切面的执行顺序

@Aspect
@Order(1)
public class LogAspect {
    @Around("...")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        log.info("[Log] 前置");
        Object result = pjp.proceed();
        log.info("[Log] 后置");
        return result;
    }
}

@Aspect
@Order(2)
public class TransactionAspect {
    @Around("...")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        transaction.begin();
        log.info("[Tx] 前置");
        Object result = pjp.proceed();
        log.info("[Tx] 后置");
        transaction.commit();
        return result;
    }
}

// 执行结果(@Order值小的先执行前置):
// [Log] 前置
// [Tx]  前置
// ... 目标方法执行 ...
// [Tx]  后置
// [Log] 后置

// 切面嵌套关系:
// @Order(1) 日志
//     @Order(2) 事务
//         目标方法

4.2 一个实际例子——权限和事务的顺序

@Aspect
@Order(1)  // 最先执行:先检查权限
public class PermissionAspect { ... }

@Aspect
@Order(2)  // 中间:开启事务
public class TransactionAspect { ... }

@Aspect
@Order(3)  // 最后:记录日志
public class LogAspect { ... }

执行流程

Log 前置 → Tx 前置 → Permission 前置 → 目标方法 → Permission 后置 → Tx 后置 → Log 后置

最外层日志包裹所有操作,中间层事务包裹业务执行,最内层权限先通过验证才能进入业务。

五、AOP在项目中的实战应用

5.1 权限控制——方法级鉴权

// 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequirePermission {
    String value();  // 权限码
}

// 切面实现
@Aspect
@Component
public class PermissionAspect {
    
    @Around("@annotation(requirePermission)")
    public Object checkPermission(ProceedingJoinPoint pjp, 
                                  RequirePermission requirePermission) throws Throwable {
        // 获取当前用户
        User currentUser = SecurityContextHolder.getContext().getUser();
        
        // 获取用户权限集合(从Redis或数据库)
        Set<String> permissions = permissionService.getPermissions(currentUser.getUserId());
        
        // 检查是否拥有所需权限
        if (!permissions.contains(requirePermission.value())) {
            throw new AccessDeniedException("权限不足: " + requirePermission.value());
        }
        
        // 权限通过,继续执行
        return pjp.proceed();
    }
}

// 业务代码中使用
@RequirePermission("user:delete")
public void deleteUser(Long userId) {
    userMapper.deleteById(userId);
}

面试可以这样讲:“项目中的权限控制就是用AOP实现的。自定义了@RequirePermission注解,写了切面在方法执行前拦截,从Redis获取当前用户的权限集合并比对。如果权限不足,抛异常被全局异常处理器捕获返回统一错误响应。”

5.2 事务管理——@Transactional

@Transactional是Spring AOP最经典的应用。Spring在TransactionInterceptor中实现事务的开启、提交、回滚逻辑,通过AOP包装被@Transactional注解的方法。

// 简化的事务拦截器逻辑
@Around("@annotation(transactional)")
public Object manageTransaction(ProceedingJoinPoint pjp, 
                                Transactional transactional) throws Throwable {
    TransactionStatus status = transactionManager.begin();
    try {
        Object result = pjp.proceed();         // 执行业务方法
        transactionManager.commit(status);     // 成功则提交
        return result;
    } catch (Exception e) {
        transactionManager.rollback(status);   // 异常则回滚
        throw e;
    }
}

六、Spring AOP vs AspectJ

维度 Spring AOP AspectJ
实现方式 动态代理(运行时生成代理) 编译时/类加载时字节码织入
性能 略有代理开销 接近原生调用
适用范围 仅Spring管理的Bean 任何Java对象
使用复杂度 简单,配合Spring生态 需要额外编译器,配置较多
默认选择 Spring Boot项目默认 需要显式集成

Spring Boot默认使用Spring AOP,因为动态代理已经满足绝大多数场景,不需要引入额外的编译工具链。


七、面试中这样回答

面试官:“AOP在项目中怎么用的?”

回答框架

“项目中最典型的两处AOP应用。一是权限控制——用自定义注解@RequirePermission声明需要的权限,切面在方法执行前拦截,从Redis获取用户权限集合做比对。二是事务管理——@Transactional就是Spring基于AOP实现的事务拦截,切面在方法执行前后负责开启和提交/回滚事务。另外日志打印也是通过切面统一处理的,不散落在各个业务方法中。”

面试官:“JDK动态代理和CGLIB有什么区别?”

回答

“JDK动态代理要求目标对象必须实现接口,代理对象和目标对象实现同一个接口——生成的代理对象是接口的实现者。CGLIB是通过继承目标类生成子类,在子类中重写父类方法并插入增强逻辑,不要求接口但不能是final类。Spring Boot 2.x及之后默认用CGLIB,无论目标对象是否实现接口。JDK代理创建快但有反射开销,CGLIB创建慢但调用快。”


总结

  • AOP解决横切关注点——将分散在多个方法中的日志、权限、事务抽离到统一的切面中管理,业务代码只保留核心逻辑
  • JDK动态代理要求接口,生成接口实现者CGLIB通过继承生成子类,不要求接口但不能代理final类。Spring Boot默认用CGLIB
  • 切面 = 切点(筛选规则)+ 通知(执行动作):@Before在前,@After在后,@Around包裹整个调用。@Around是最强大的通知类型
  • 多切面按@Order值从小到大执行前置,后置逆序——形成"先入后出"的嵌套结构
  • 项目中AOP的两个实际应用:权限注解的拦截校验、@Transactional的事务管理。两个都是通过@Around环绕通知实现的
  • Spring AOP基于动态代理实现,依赖容器的Bean管理;AspectJ通过编译时字节码织入,范围更广但需要额外配置。两者各自对应不同的场景和复杂度需求

下一篇预告:Spring原理(三)——SpringMVC:一个HTTP请求在Spring中经历了什么?拆解DispatcherServlet的请求处理全链路,HandlerMapping、HandlerAdapter、ViewResolver各自的职责,以及拦截器和过滤器的区别。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐