spring的AOP机制及原理

110 阅读15分钟

AOP(Aspect-Oriented Programming,面向切面编程)是 Spring 核心特性之一,其核心思想是分离 “核心业务逻辑” 与 “横切逻辑” (如日志、事务、权限、监控等),通过 “织入” 机制将横切逻辑动态融入核心业务,实现代码复用、降低耦合度。

一、为什么需要 AOP?

在传统编程中,横切逻辑(如每个方法的日志记录)会嵌入到核心业务代码中,导致以下问题:

  • 代码冗余:相同的横切逻辑重复出现在多个业务方法中;
  • 耦合度高:横切逻辑与核心业务强绑定,修改横切逻辑需改动所有相关业务代码;
  • 维护困难:核心业务代码被横切逻辑污染,可读性差。

AOP 通过 “切面” 封装横切逻辑,让核心业务代码只关注自身逻辑,横切逻辑集中管理,解决上述问题。

二、AOP 核心术语(必须掌握)

用 “日志切面” 举例,通俗解释关键概念:

术语定义(通俗理解)
核心业务逻辑(Target)被增强的目标对象 / 方法(如用户服务的 addUser()deleteUser() 方法)
横切逻辑(Advice)切面的具体逻辑(如日志记录的 “打印请求参数”“打印执行耗时”)
切面(Aspect)横切逻辑的封装类(如 LogAspect 类,包含日志逻辑 + 切入点规则)
连接点(JoinPoint)程序中 “可能被拦截” 的所有点(Spring AOP 仅支持方法级连接点,如所有方法的执行、异常抛出)
切入点(Pointcut)从所有连接点中 “筛选出需要增强的点”(如 “所有 com.example.service 包下的方法”)
织入(Weaving)将切面的横切逻辑动态融入目标对象的过程(Spring AOP 是运行时织入
代理对象(Proxy)织入后生成的对象(Spring 会为目标对象创建代理,开发者实际调用的是代理对象方法)
通知类型(Advice Type)横切逻辑的执行时机(如方法执行前、执行后、异常时)

关键区别

  • 连接点 = 所有可能被增强的 “候选点”(如 100 个方法);
  • 切入点 = 被选中的连接点(如 10 个符合规则的方法)。

三、Spring AOP 核心原理:动态代理

Spring AOP 的底层实现依赖动态代理,即运行时为目标对象创建代理对象,横切逻辑通过代理对象执行。Spring 支持两种动态代理方式,自动切换:

1. JDK 动态代理(默认,优先使用)

  • 适用场景:目标对象实现了接口(如 UserService 实现 IUserService);

  • 实现原理:基于 JDK 自带的 java.lang.reflect.Proxy 类和 InvocationHandler 接口;

  • 特点

    • 无需额外依赖,JDK 原生支持;
    • 代理类是目标接口的 “实现类”(运行时动态生成);
    • 只能代理接口中定义的方法(目标类中未实现接口的方法无法代理)。

简单示例(JDK 动态代理底层逻辑)

// 1. 目标接口
public interface IUserService {
    void addUser();
}

// 2. 目标实现类(核心业务)
public class UserService implements IUserService {
    @Override
    public void addUser() {
        System.out.println("核心业务:添加用户");
    }
}

// 3. 横切逻辑(日志)+ 代理处理器
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("日志:方法 " + method.getName() + " 开始执行");
        
        // 执行核心业务(目标对象的方法)
        Object result = method.invoke(target, args);
        
        // 横切逻辑:方法执行后
        System.out.println("日志:方法 " + method.getName() + " 执行结束");
        return result;
    }
}

// 4. 生成代理对象并调用
public class Test {
    public static void main(String[] args) {
        // 目标对象
        IUserService target = new UserService();
        // 代理处理器(绑定目标对象+横切逻辑)
        InvocationHandler handler = new LogInvocationHandler(target);
        // 生成代理对象(JDK 动态代理)
        IUserService proxy = (IUserService) Proxy.newProxyInstance(
            target.getClass().getClassLoader(),
            target.getClass().getInterfaces(),
            handler
        );
        
        // 调用代理对象方法(自动触发横切逻辑+核心业务)
        proxy.addUser();
    }
}

输出结果

日志:方法 addUser 开始执行
核心业务:添加用户
日志:方法 addUser 执行结束

2. CGLIB 动态代理( fallback 方案)

  • 适用场景:目标对象未实现接口(如 UserService 无接口);

  • 实现原理:基于第三方库 cglib(Spring 已内置),通过继承目标类生成子类作为代理对象;

  • 特点

    • 无需目标类实现接口,适用范围更广;
    • 代理类是目标类的 “子类”,需重写目标方法(因此目标方法不能是 final,否则无法重写);
    • 性能略优于 JDK 动态代理(因为是直接操作字节码)。

Spring 代理选择规则

  1. 若目标对象实现接口 → 优先使用 JDK 动态代理;
  2. 若目标对象无接口 → 自动切换为 CGLIB 动态代理;
  3. 可通过配置强制使用 CGLIB(如 @EnableAspectJAutoProxy(proxyTargetClass = true))。

四、Spring AOP 核心组件及关系

Spring AOP 遵循 AOP 联盟规范,核心组件通过 “组合” 实现切面功能:

组件作用
Aspect(切面)封装横切逻辑(Advice)和切入点(Pointcut)的类(如 @Aspect 注解类)
Pointcut(切入点)定义 “哪些方法需要被增强”(通过表达式匹配,如 execution(* com.example.service.*.*(..))
Advice(通知)定义 “横切逻辑的执行时机”(如前置、后置、环绕通知)
Advisor(通知器)Spring AOP 内部核心组件,是 Pointcut + Advice 的组合(@Aspect 最终会被解析为 Advisor
Weaver(织入器)执行织入的组件(Spring 动态代理的底层实现)

核心关系Aspect = Pointcut + Advice → 解析为 Advisor → 由 Weaver 织入目标对象 → 生成 Proxy 代理对象。

五、Spring AOP 通知类型(执行时机)

Spring 支持 5 种通知类型,覆盖方法执行的全生命周期:

通知类型注解执行时机特点
前置通知@Before目标方法执行之前无返回值,不能阻止目标方法执行(除非抛异常)
后置返回通知@AfterReturning目标方法正常执行完成后(无异常)可获取目标方法的返回值
后置最终通知@After目标方法执行之后(无论是否异常)类似 finally,必定执行
异常通知@AfterThrowing目标方法抛出异常后可获取异常信息
环绕通知@Around目标方法执行前后(全程包裹)功能最强,可控制目标方法是否执行、修改参数 / 返回值

环绕通知示例(最常用)

@Aspect
@Component
public class LogAspect {
    // 切入点:匹配 com.example.service 包下所有方法
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void servicePointcut() {}

    // 环绕通知
    @Around("servicePointcut()")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        // 1. 前置逻辑:记录请求参数
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        System.out.println("环绕通知:方法 " + methodName + " 入参:" + Arrays.toString(args));

        long startTime = System.currentTimeMillis();
        Object result = null;
        try {
            // 2. 执行目标方法(必须调用,否则目标方法不执行)
            result = joinPoint.proceed(args); 
            // 3. 后置返回逻辑(无异常时执行)
            System.out.println("环绕通知:方法 " + methodName + " 返回值:" + result);
        } catch (Exception e) {
            // 4. 异常处理逻辑
            System.out.println("环绕通知:方法 " + methodName + " 抛出异常:" + e.getMessage());
            throw e; // 抛出异常,让后续异常通知处理
        } finally {
            // 5. 最终逻辑:记录执行耗时
            long cost = System.currentTimeMillis() - startTime;
            System.out.println("环绕通知:方法 " + methodName + " 执行耗时:" + cost + "ms");
        }
        return result;
    }
}

六、Spring AOP 切入点表达式(Pointcut Expression)

切入点表达式用于 “筛选需要增强的方法”,Spring 支持多种表达式,最常用的是 execution

1. 核心语法(execution)

execution(修饰符? 返回值 包名.类名.方法名(参数) 异常? )
  • ? 表示可选(修饰符、异常可省略);
  • * 表示任意(任意返回值、任意包、任意类、任意方法);
  • .. 表示任意子包 / 任意参数(如 com.example..* 表示 com.example 下所有子包)。

2. 常用示例

表达式含义
execution(* com.example.service.*.*(..))com.example.service 包下所有类的所有方法
execution(public * com.example.service.UserService.*(..))UserService 类的所有公共方法
execution(* com.example.service..*.*(String, ..))子包下所有方法,第一个参数为 String 类型
execution(* com.example.service.IUserService.add*(..))接口中所有以 add 开头的方法

3. 其他表达式(辅助匹配)

  • @annotation(注解全类名):匹配标注了指定注解的方法(如 @Transactional);
  • within(包名.类名):匹配指定类 / 包下的所有方法;
  • this(接口类型):匹配实现了指定接口的目标对象的所有方法。

七、Spring AOP 执行流程(完整链路)

以 @Aspect 注解方式为例,整体执行流程如下:

  1. 容器启动阶段

    • Spring 扫描带有 @Component 和 @Aspect 的切面类(如 LogAspect);
    • 解析切面类中的 @Pointcut 切入点表达式,确定需要增强的目标方法;
    • 解析 @Before/@Around 等通知,将 “切入点 + 通知” 封装为 Advisor 对象;
    • 扫描业务 Bean(如 UserService),判断其方法是否匹配 Advisor 的切入点。
  2. 代理对象创建阶段

    • 若业务 Bean 的方法匹配切入点,Spring 为该 Bean 创建动态代理对象(JDK/CGLIB);
    • 代理对象持有目标对象(target)和 Advisor 中的通知逻辑。
  3. 运行时调用阶段

    • 开发者调用代理对象的方法(而非目标对象);
    • 代理对象触发通知链执行(按 “环绕前置 → 前置 → 目标方法 → 环绕后置 → 后置返回 / 异常 → 后置最终” 顺序);
    • 执行完成后,代理对象返回结果给调用者。

八、Spring AOP 注意事项与常见问题

1. 与 AspectJ 的区别

  • Spring AOP:轻量级,基于动态代理(运行时织入),仅支持方法级连接点,依赖 Spring 容器;
  • AspectJ:完整的 AOP 框架,支持编译期 / 类加载期织入,可拦截字段访问、构造方法、静态方法等,功能更强(Spring 可整合 AspectJ 实现更复杂的 AOP 需求)。

2. 自调用问题(代理失效)

若目标对象的 A 方法调用自身的 B 方法,且 B 方法被切面增强,则通知不会执行(因为是 “目标对象内部调用”,未经过代理对象)。解决方案

  • 注入自身代理对象(@Autowired private UserService proxy;,需开启 exposeProxy = true);
  • 重构代码,避免自调用。

3. 代理选择与配置

  • 强制使用 CGLIB:在 Spring Boot 中添加 @EnableAspectJAutoProxy(proxyTargetClass = true)
  • JDK 动态代理只能代理接口方法,若目标类有非接口方法,需使用 CGLIB。

4. 通知执行顺序

  • 同一切面中:@Around(前置)→ @Before → 目标方法 → @Around(后置)→ @AfterReturning/@AfterThrowing → @After
  • 多个切面:通过 @Order(数字) 注解指定切面优先级(数字越小,优先级越高)。

九、spring的Aop几种实现方式

Spring AOP(面向切面编程)的核心是 通过 “切面” 封装横切逻辑(如日志、事务、权限) ,并动态织入目标方法,不侵入业务代码。其实现方式主要分为 4 种核心类型,本质差异在于 织入时机(编译期 / 类加载期 / 运行期)  和 技术底层(字节码操作 / 动态代理)

一、核心实现方式(按织入时机从早到晚排序)

1. 基于编译期织入:AspectJ 编译期织入(原生 AspectJ)

这是 最早期、织入时机最早 的 AOP 实现,完全依赖 AspectJ 框架(非 Spring 原生,需额外依赖),织入发生在 Java 代码编译为字节码的阶段

实现原理
  • 编写 AspectJ 语法的切面类(.aj 文件),通过 AspectJ 提供的编译器(ajc)替代 javac 编译源码;
  • 编译时,ajc 会直接修改目标类的字节码,将切面逻辑(如前置 / 后置通知)直接嵌入目标方法中;
  • 运行时无需任何代理,目标类本身已包含切面逻辑,性能最优(无运行时开销)。
关键特点
  • 依赖:需引入 AspectJ 核心依赖(aspectjrt.jaraspectjtools.jar);
  • 语法:需使用 AspectJ 专属注解(如 @AspectJ 注解但需配合 ajc 编译,或 .aj 原生语法);
  • 优点:性能最好(编译期织入,无运行时代理开销);
  • 缺点:侵入性强(需用 AspectJ 编译器,脱离 IDE 时配置复杂),Spring 项目中极少单独使用。
简单示例(.aj 文件切面)
// 切面类(.aj 文件)
public aspect LogAspect {
    // 切入点:匹配 com.example.service 包下所有类的所有方法
    pointcut serviceMethod() : execution(* com.example.service.*.*(..));
    
    // 前置通知
    before() : serviceMethod() {
        System.out.println("【编译期织入】方法执行前日志");
    }
}

2. 基于类加载期织入:AspectJ 加载时织入(LTW)

同样依赖 AspectJ 框架,但织入时机推迟到 目标类被类加载器加载到 JVM 的阶段,无需修改编译流程(用 javac 编译即可)。

实现原理
  • 编写普通的 AspectJ 注解切面(如 @Aspect 标注);
  • 启动 JVM 时,通过 -javaagent 参数指定 AspectJ 的织入代理(aspectjweaver.jar);
  • 类加载器加载目标类时,织入代理会拦截类加载过程,动态修改目标类的字节码,植入切面逻辑。
关键特点
  • 依赖:需引入 aspectjweaver.jar,并配置 JVM 代理参数;
  • 优点:无编译期侵入(用 javac 编译),性能接近编译期织入;
  • 缺点:需配置 JVM 启动参数(如 -javaagent:/path/to/aspectjweaver.jar),部署复杂度高,Spring 项目中较少使用(除非追求极致性能)。
启动参数示例
java -javaagent:lib/aspectjweaver-1.9.20.jar -jar your-spring-app.jar

3. 基于运行期动态代理:JDK 动态代理(Spring 原生默认)

这是 Spring AOP 最核心、最常用 的实现方式,织入发生在 应用运行时,基于 JDK 原生的 java.lang.reflect.Proxy 接口实现,无需额外依赖。

实现原理
  • 核心前提:目标类 必须实现接口(JDK 动态代理仅能代理接口方法);
  • 代理生成:Spring 容器启动时,对实现接口的目标类,通过 Proxy.newProxyInstance() 动态生成 代理类(实现目标类的所有接口);
  • 切面织入:代理类中嵌入切面逻辑,调用目标方法时,先执行切面的通知(如前置、后置),再反射调用目标类的真实方法。
关键特点
  • 依赖:仅依赖 JDK 原生 API,无额外 jar 包;
  • 优点:无侵入(目标类无需修改)、配置简单(Spring 自动生成代理);
  • 缺点:仅支持接口代理(目标类必须实现接口,否则无法代理);运行时通过反射调用,有轻微性能开销(可忽略,除非高频调用)。
Spring 示例(注解式 AOP)
// 1. 目标接口
public interface UserService {
    void addUser();
}

// 2. 目标实现类(必须实现接口)
@Service
public class UserServiceImpl implements UserService {
    @Override
    public void addUser() {
        System.out.println("执行添加用户业务");
    }
}

// 3. 切面类(Spring AOP 注解)
@Aspect
@Component
public class LogAspect {
    // 切入点:匹配 UserService 接口的所有方法
    @Pointcut("execution(* com.example.service.UserService.*(..))")
    public void servicePointcut() {}
    
    // 前置通知
    @Before("servicePointcut()")
    public void beforeAdvice() {
        System.out.println("【JDK动态代理】方法执行前日志");
    }
}

// 4. 启动类(开启 AOP 自动代理)
@SpringBootApplication
@EnableAspectJAutoProxy // 开启 AOP 支持(Spring Boot 可省略,默认自动开启)
public class AopDemoApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(AopDemoApplication.class, args);
        UserService userService = context.getBean(UserService.class);
        userService.addUser(); // 执行时会触发切面通知
    }
}

4. 基于运行期动态代理:CGLIB 动态代理(Spring 补充实现)

当目标类 没有实现接口 时,Spring AOP 会自动切换为 CGLIB 代理(基于字节码操作框架 cglib-nodep.jar),织入时机同样是 应用运行时

实现原理
  • 核心前提:目标类无需实现接口(但不能是 final 类,方法不能是 final 方法 ——CGLIB 无法代理 final 元素);
  • 代理生成:通过 CGLIB 的 Enhancer 类动态生成 目标类的子类(继承目标类);
  • 切面织入:子类中重写目标类的方法,嵌入切面逻辑,调用时先执行切面通知,再通过 super 调用父类(目标类)的真实方法。
关键特点
  • 依赖:Spring 内置 CGLIB 依赖(Spring 5+ 已集成 cglib-nodep.jar,无需手动引入);
  • 优点:支持类代理(无需接口),性能比 JDK 动态代理略好(直接调用子类方法,无反射开销);
  • 缺点:目标类不能是 final 类,方法不能是 final 方法(否则无法继承重写);生成子类字节码,有轻微类加载开销。
Spring 配置(切换为 CGLIB 代理)

Spring Boot 中默认策略:目标类有接口则用 JDK 代理,无接口则用 CGLIB 代理。若需强制使用 CGLIB 代理(即使有接口),可配置:

// 启动类中开启 AOP 并强制使用 CGLIB
@SpringBootApplication
@EnableAspectJAutoProxy(proxyTargetClass = true) // proxyTargetClass=true 强制使用 CGLIB
public class AopDemoApplication { ... }
示例(目标类无接口)
// 目标类(无接口)
@Service
public class OrderService { // 无接口,Spring 自动用 CGLIB 代理
    public void createOrder() {
        System.out.println("执行创建订单业务");
    }
}

// 切面类(与 JDK 代理共用,无需修改)
@Aspect
@Component
public class LogAspect {
    @Pointcut("execution(* com.example.service.OrderService.*(..))")
    public void orderPointcut() {}
    
    @Before("orderPointcut()")
    public void beforeAdvice() {
        System.out.println("【CGLIB代理】方法执行前日志");
    }
}

二、4 种实现方式核心对比

实现方式织入时机底层技术核心依赖支持目标类要求性能侵入性Spring 中常用度
AspectJ 编译期织入编译期AspectJ 编译器(ajc)aspectjrt、aspectjtools无(任意类)最优极低
AspectJ 加载时织入(LTW)类加载期AspectJ 织入代理aspectjweaver无(任意类)接近最优
JDK 动态代理运行期JDK Proxy + 反射JDK 原生 API必须实现接口中等(反射)极高
CGLIB 动态代理运行期CGLIB(字节码子类生成)Spring 内置 cglib不能是 final 类 / 方法中等偏优极高

三、Spring AOP 实现方式的选择逻辑

Spring 容器启动时,会自动选择代理方式,核心逻辑:

  1. 若开启 @EnableAspectJAutoProxy(proxyTargetClass = true):强制使用 CGLIB 代理;

  2. 若未开启(默认 proxyTargetClass = false):

    • 目标类实现接口 → 优先用 JDK 动态代理;
    • 目标类未实现接口 → 自动用 CGLIB 代理;
  3. 若引入 AspectJ 依赖并配置 LTW / 编译期织入:优先使用 AspectJ 织入(需手动配置)。

四、关键总结

  1. 日常开发首选:Spring 原生的 JDK 动态代理 + CGLIB 动态代理(无需手动干预,Spring 自动适配),配合 @Aspect 注解式切面,开发效率最高、侵入性最低;
  2. 极致性能需求:可考虑 AspectJ 加载时织入(LTW),但需接受部署复杂度;
  3. 历史场景 / 特殊需求:AspectJ 编译期织入(仅适合老项目或对性能要求极致的场景);
  4. 核心区别:AspectJ 是 “静态织入”(修改字节码),Spring 原生代理是 “动态织入”(运行时生成代理);静态织入性能好但侵入性强,动态织入灵活但有轻微运行时开销。

十、总结

Spring AOP 的核心是动态代理切面织入,核心价值是 “分离关注点”:

  • 核心业务代码专注于业务逻辑,横切逻辑集中在切面类中,实现复用;
  • 底层通过 JDK/CGLIB 动态代理生成代理对象,运行时织入通知逻辑;
  • 基于注解的 @AspectJ 方式简化了 AOP 配置,是实际开发中的首选。