WD
Classnote Docs课程课件
12

Spring AOP - 面向切面编程

学习目标:

  • 理解 AOP 的设计思想和核心概念
  • 掌握切入点表达式(execution、@annotation、@target)
  • 掌握 5 种通知类型的执行时机和使用场景
  • 熟悉 AspectJ 注解的使用
  • 能解释 Spring AOP 为什么本质上是基于代理实现的

本章重点:

  • AOP 中切面、切点、通知、连接点之间的关系
  • execution / @annotation / @target 三类切点表达式的适用场景
  • Before / After / Around 等通知类型的区别与选择
  • 动态代理、JoinPoint、ProceedingJoinPoint 的底层理解
01 / Section

1. AOP概述

1.1 什么是AOP

AOP(Aspect-oriented Programming) 是面向切面编程,它补充了OOP(面向对象编程)的不足:

特性 OOP AOP
基本单元 类(Class) 切面(Aspect)
关注点 纵向业务逻辑 横向公共逻辑
解决问题 业务模块化 横切关注点分离

1.2 为什么需要AOP

想象一个场景:多个Service方法都需要记录日志

java
// 没有AOP时 - 日志代码分散各处
public class UserServiceImpl {
    public void createUser(String name) {
        System.out.println("[日志] 方法开始");  // 重复代码
        // 业务逻辑
        System.out.println("[日志] 方法结束");  // 重复代码
    }
}

public class OrderServiceImpl {
    public void createOrder(String product) {
        System.out.println("[日志] 方法开始");  // 重复代码
        // 业务逻辑
        System.out.println("[日志] 方法结束");  // 重复代码
    }
}

问题

  • ❌ 日志代码与业务逻辑强耦合
  • ❌ 代码重复,无法复用
  • ❌ 修改日志逻辑需要改动多处

AOP解决方案:将日志逻辑封装成切面,自动应用到目标方法

多个业务方法共享统一切面逻辑的结构图

1.3 切面要素

image-20260503025308652

AOP 真正要解决的问题,不是重新写业务方法,而是明确“哪些组件需要被增强、在方法执行的什么阶段做什么事”。所以一个切面至少要回答两个问题

  1. 增强范围怎么圈定
  1. 增强动作如何执行。

定义一个切面需要两个核心要素:

要素 说明 对应概念
What + When 封装什么逻辑,什么时候执行 Advice(通知)
Where 对哪些类的哪些方法生效 Pointcut(切入点)

AOP 的底层支撑就是动态代理。Spring 容器负责管理 Bean,AOP 的做法是在合适的 Bean 外层包上一层代理对象,让公共逻辑在不改业务代码的前提下被统一织入。

其中,“哪些组件、哪些方法需要增强”对应 Where;“增强逻辑做什么、在什么时候执行”对应 What + When。可以把它理解成:一个负责圈定范围,一个负责安排增强动作。

本章小结

本章核心概念:AOP 的价值不在于“多学一个新名词”,而在于把日志、事务、权限这类横切逻辑从业务代码中抽离出来,让系统更容易复用和维护。

你现在应该掌握

  • 能说明 AOP 与 OOP 分别解决什么问题
  • 能用 WhereWhat + When 解释切入点与通知的分工
  • 能从“动态代理 + Spring Bean”角度理解 AOP 的基本思路
02 / Section

2. AOP核心术语

术语 英文 说明
目标类 Target 需要被代理的原始类
代理类 Proxy 动态代理生成的类
连接点 JoinPoint 可以被增强的方法
切入点 PointCut 定义哪些连接点会被增强
通知 Advice 具体的增强逻辑
切面 Aspect 切入点 + 通知的模块化表示
织入 Weaving 将Advice应用到目标对象的过程

切面由切入点和通知组成的结构图

本章小结

本章核心概念:AOP 的术语并不是零散记忆的名词表,它们围绕“目标对象被代理后,增强逻辑如何命中并执行”这一条链路组织起来。

你现在应该掌握

  • 能区分 Target、Proxy、JoinPoint、Pointcut、Advice、Aspect 的含义
  • 能说清 Aspect = Pointcut + Advice 这层组合关系
  • 能理解 Weaving 指的是把增强逻辑应用到目标对象的过程
03 / Section

3. Spring AOP底层原理

3.1 动态代理实现

Spring AOP在运行期通过代理方式向目标类织入增强代码,核心原理是动态代理

java
// 代理类示意(非真实代码)
public class UserServiceProxy implements UserService {
    private UserService target;  // 目标对象
    private LoggerAdvice advice; // 增强逻辑
    
    public void createUser(String name) {
        advice.before();                    // 前置增强
        target.createUser(name);            // 调用目标方法
        advice.after();                     // 后置增强
    }
}

3.2 两种代理方式

代理方式 适用场景 特点
JDK动态代理 目标类实现了接口 基于接口生成代理
CGLIB代理 目标类没有接口 基于子类生成代理
diagram
JDK动态代理: CGLIB代理: 接口 目标类 UserService UserService 代理类 子类(代理) implements extends UserService UserService
JDK动态代理:           CGLIB代理:
┌──────────────┐       ┌──────────────┐
│  接口         │       │   目标类      │
│  UserService  │       │ UserService  │
└──────┬───────┘       └──────┬───────┘
       │                      │
       ↓                      ↓
┌──────────────┐       ┌──────────────┐
│   代理类      │       │   子类(代理)  │
│ implements   │       │ extends      │
│ UserService  │       │ UserService  │
└──────────────┘       └──────────────┘

image-20260503025514204

,JDK 动态代理的核心是 implements 接口,而 CGLIB 代理的核心是 extends 目标类。这张图的重点在于搞清楚代理对象和原始对象之间到底是“实现同一接口”还是“继承原始类型”的关系。

本章小结

本章核心概念:Spring AOP 不是修改原始类源码,而是在运行期生成代理对象,把增强逻辑包进方法调用流程里。

你现在应该掌握

  • 能解释 Spring AOP 为什么依赖动态代理
  • 能区分 JDK 动态代理与 CGLIB 代理的适用场景
  • 能理解“代理对象代替目标对象对外提供能力”这件事
04 / Section

4. AOP快速入门

4.1 添加依赖

xml
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>6.2.15</version>
</dependency>

<!-- AspectJ依赖(Spring AOP使用了AspectJ的切点表达式) -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.9.1</version>
</dependency>

<!-- 测试验证需要的Spring Test与JUnit Jupiter -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>6.2.15</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.10.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.10.0</version>
    <scope>test</scope>
</dependency>

4.2 开启AOP功能

切面组件

  • 增加AspectJ的注解开关 → 配置类上增加一个注解@EnableAspectJAutoProxy
  • 把组件标记为切面组件 → @Aspect
java
@Configuration
@ComponentScan("com.example")
@EnableAspectJAutoProxy  // 开启Spring AOP
public class SpringConfiguration {
}

4.3 定义切面

java
@Component  // 注册为Spring Bean
@Aspect     // 标记为切面类
public class LoggingAspect {
    
    // 定义切入点:匹配com.example.service包下所有类的所有方法
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void servicePointCut() {}
    
    // 前置通知:方法执行前执行
    @Before("servicePointCut()")
    public void beforeLog(JoinPoint joinPoint) {
        System.out.println("[日志] 开始执行: " + joinPoint.getSignature().getName());
    }
    
    // 后置通知:方法执行后执行
    @After("servicePointCut()")
    public void afterLog(JoinPoint joinPoint) {
        System.out.println("[日志] 执行结束: " + joinPoint.getSignature().getName());
    }
}

4.4 效果验证

java
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = SpringConfiguration.class)
public class AopTest {
    
    @Autowired
    private UserService userService;
    
    @Test
    public void testAop() {
        userService.createUser("张三");
        // 输出:
        // [日志] 开始执行: createUser
        // 业务逻辑:创建用户
        // [日志] 执行结束: createUser
    }
}

本章小结

本章核心概念:AOP 的入门配置其实就是把“依赖、开关、切面类、切入点、通知方法”这几块拼起来,让 Spring 知道该在什么位置织入什么逻辑。

你现在应该掌握

  • 能完成 aspectjweaver 依赖引入
  • 能通过 @EnableAspectJAutoProxy 开启 AOP 功能
  • 能写出包含 @Aspect@Pointcut@Before/@After 的基础切面
05 / Section

5. 切入点表达式(PointCut)

5.1 execution表达式

最常用,用于直接匹配方法签名。

语法

text
execution(访问修饰符 返回值 包名.类名.方法名(参数列表))

通配符

  • *:匹配任意字符(单个)
  • ..:匹配任意字符(多个)

示例

表达式 含义
execution(public * *(..)) 任意public方法
execution(* set*(..)) 任意以set开头的方法
execution(* com.service.*.*(..)) service包下任意类的任意方法
execution(* com.service..*.*(..)) service包及其子包下任意类的任意方法
execution(* com.service.UserService.*(..)) UserService接口的所有方法
execution(* com.service.*.create*(..)) service包下任意类以create开头的方法
execution(String com.service.*.find*(..)) 返回值为String且以find开头的方法
execution(* com.service.*.*(String, *)) 第一个参数为String,第二个为任意类型

5.2 @annotation表达式

匹配标注了指定注解的方法。

java
// 1. 定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogOperation {
}

// 2. 使用注解
@Service
public class UserServiceImpl {
    @LogOperation
    public void createUser(String name) {
        // ...
    }
}

// 3. 定义切入点
@Pointcut("@annotation(com.example.annotation.LogOperation)")
public void logPointCut() {}

5.3 @target表达式

匹配目标对象类上标注了指定注解的方法(类级别的注解)。

java
// 1. 定义类级别注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ServiceLog {
}

// 2. 标注在类上
@Service
@ServiceLog
public class UserServiceImpl {
    public void createUser(String name) { }  // 会被匹配
    public void deleteUser(String name) { }  // 也会被匹配
}

// 3. 定义切入点
@Pointcut("@target(com.example.annotation.ServiceLog)")
public void serviceLogPointCut() {}

5.4 表达式对比

表达式 匹配粒度 使用场景
execution 方法签名 按包、类、方法名匹配
@annotation 方法注解 精确控制哪些方法需要增强
@target 类注解 对整个类的所有方法增强

本章小结

本章核心概念:切入点表达式的关键不是死记语法,而是根据业务需要选择“按方法签名拦截”还是“按注解拦截”。

你现在应该掌握

  • 能读懂 execution(...) 常见写法的含义
  • 能区分 execution@annotation@target 三种表达式的应用边界
  • 能根据“整包拦截 / 指定方法 / 指定类”选择合适的切点表达式
06 / Section

6. 通知类型(Advice)

6.1 5种通知类型

通知类型 注解 执行时机 特点
前置通知 @Before 方法执行前 无法阻止方法执行
后置通知 @AfterReturning 方法正常返回后 可获取返回值
异常通知 @AfterThrowing 方法抛出异常后 可获取异常对象
最终通知 @After 方法执行结束后 无论正常或异常
环绕通知 @Around 方法执行前后 最强控制力,可控制是否执行目标方法

6.2 执行顺序

text
正常执行流程:
@Before → 目标方法 → @AfterReturning → @After

异常执行流程:
@Before → 目标方法(抛出异常) → @AfterThrowing → @After

6.3 各类通知详解

@Before(前置通知)

java
@Before("servicePointCut()")
public void beforeAdvice(JoinPoint joinPoint) {
    // 获取方法签名
    String methodName = joinPoint.getSignature().getName();
    // 获取参数
    Object[] args = joinPoint.getArgs();
    // 获取目标对象
    Object target = joinPoint.getTarget();
    
    System.out.println("[前置通知] 准备执行: " + methodName);
}

@AfterReturning(返回通知)

java
@AfterReturning(
    value = "servicePointCut()", 
    returning = "result"  // 绑定返回值到参数
)
public void afterReturning(JoinPoint joinPoint, Object result) {
    System.out.println("[返回通知] 返回值: " + result);
}

@AfterThrowing(异常通知)

java
@AfterThrowing(
    value = "servicePointCut()",
    throwing = "ex"  // 绑定异常到参数
)
public void afterThrowing(JoinPoint joinPoint, Exception ex) {
    System.out.println("[异常通知] 发生异常: " + ex.getMessage());
}

@After(最终通知)

java
@After("servicePointCut()")
public void afterAdvice(JoinPoint joinPoint) {
    System.out.println("[最终通知] 方法执行结束");
}

@Around(环绕通知)

最强大的通知类型,可以控制目标方法是否执行

java
@Around("servicePointCut()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
    // 1. 前置逻辑
    System.out.println("[环绕通知-前]");
    
    // 2. 获取信息
    String methodName = joinPoint.getSignature().getName();
    Object[] args = joinPoint.getArgs();
    
    Object result = null;
    try {
        // 3. 执行目标方法(如果不调用proceed,目标方法不会执行)
        result = joinPoint.proceed(args);
        
        // 4. 后置逻辑(正常返回)
        System.out.println("[环绕通知-后] 返回值: " + result);
    } catch (Throwable e) {
        // 5. 异常处理
        System.out.println("[环绕通知-异常] " + e.getMessage());
        throw e;
    } finally {
        // 6. 最终逻辑
        System.out.println("[环绕通知-最终]");
    }
    
    return result;
}

6.4 通知选择指南

diagram
记录日志/权限检查(不关心结果) @Before 记录返回值/结果处理 @AfterReturning 异常处理/告警 @AfterThrowing 资源清理(必须执行) @After 事务控制/性能监控(完全控制) @Around
┌─────────────────────────────────────────────────┐
│  记录日志/权限检查(不关心结果)      → @Before   │
│  记录返回值/结果处理                 → @AfterReturning │
│  异常处理/告警                       → @AfterThrowing  │
│  资源清理(必须执行)                → @After    │
│  事务控制/性能监控(完全控制)       → @Around   │
└─────────────────────────────────────────────────┘

本章小结

本章核心概念:不同通知类型的差异,本质上是“它们能介入目标方法执行流程的哪个阶段、能拿到哪些信息、能控制到什么程度”。

你现在应该掌握

  • 能区分 @Before@AfterReturning@AfterThrowing@After@Around 的执行时机
  • 能根据日志、异常处理、资源清理、事务控制等场景选择合适通知
  • 能理解 @Around 中必须调用 proceed() 才会真正执行目标方法
07 / Section

7. JoinPoint与ProceedingJoinPoint

7.1 JoinPoint API

java
public interface JoinPoint {
    // 获取方法签名(包含方法名、参数类型等)
    Signature getSignature();
    
    // 获取目标对象(被代理的原始对象)
    Object getTarget();
    
    // 获取代理对象
    Object getThis();
    
    // 获取方法参数
    Object[] getArgs();
}

7.2 ProceedingJoinPoint(环绕通知专用)

java
public interface ProceedingJoinPoint extends JoinPoint {
    // 执行目标方法
    Object proceed() throws Throwable;
    
    // 带参数执行目标方法
    Object proceed(Object[] args) throws Throwable;
}

7.3 使用示例

java
@Around("servicePointCut()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
    // 方法信息
    String className = joinPoint.getTarget().getClass().getSimpleName();
    String methodName = joinPoint.getSignature().getName();
    Object[] args = joinPoint.getArgs();
    
    // 记录入参
    System.out.println("[" + className + "." + methodName + "] 入参: " + Arrays.toString(args));
    
    long startTime = System.currentTimeMillis();
    
    // 执行目标方法
    Object result = joinPoint.proceed();
    
    long endTime = System.currentTimeMillis();
    
    // 记录出参和执行时间
    System.out.println("[" + className + "." + methodName + "] 出参: " + result + " 耗时: " + (endTime - startTime) + "ms");
    
    return result;
}

本章小结

本章核心概念:JoinPoint 负责提供方法执行时的上下文,ProceedingJoinPoint 则在此基础上进一步提供了对目标方法执行过程的控制能力。

你现在应该掌握

  • 能说出 JoinPoint 常用 API 分别能拿到什么信息
  • 能解释 ProceedingJoinPoint 为什么只在环绕通知里使用
  • 能在 @Around 通知中结合参数、返回值和耗时做基础增强
08 / Section

附录:核心注解速查表

注解 作用 使用位置
@EnableAspectJAutoProxy 开启AOP功能 配置类
@Aspect 标记切面类
@Pointcut 定义切入点 方法
@Before 前置通知 方法
@AfterReturning 返回通知 方法
@AfterThrowing 异常通知 方法
@After 最终通知 方法
@Around 环绕通知 方法
09 / Section

实战练习

<!-- 实战练习内容已分离到 ../practices/markdown/12-spring-aop-practice.md -->

建议先完成本章重点内容复习,再进入配套练习。 练习建议按照:基础 → 进阶 → 综合挑战 的顺序完成。