学习目标:
- 理解 AOP 的设计思想和核心概念
- 掌握切入点表达式(execution、@annotation、@target)
- 掌握 5 种通知类型的执行时机和使用场景
- 熟悉 AspectJ 注解的使用
- 能解释 Spring AOP 为什么本质上是基于代理实现的
本章重点:
- AOP 中切面、切点、通知、连接点之间的关系
- execution / @annotation / @target 三类切点表达式的适用场景
- Before / After / Around 等通知类型的区别与选择
- 动态代理、JoinPoint、ProceedingJoinPoint 的底层理解
1. AOP概述
1.1 什么是AOP
AOP(Aspect-oriented Programming) 是面向切面编程,它补充了OOP(面向对象编程)的不足:
| 特性 | OOP | AOP |
|---|---|---|
| 基本单元 | 类(Class) | 切面(Aspect) |
| 关注点 | 纵向业务逻辑 | 横向公共逻辑 |
| 解决问题 | 业务模块化 | 横切关注点分离 |
1.2 为什么需要AOP
想象一个场景:多个Service方法都需要记录日志
// 没有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 切面要素

AOP 真正要解决的问题,不是重新写业务方法,而是明确“哪些组件需要被增强、在方法执行的什么阶段做什么事”。所以一个切面至少要回答两个问题
- 增强范围怎么圈定
- 增强动作如何执行。
定义一个切面需要两个核心要素:
| 要素 | 说明 | 对应概念 |
|---|---|---|
| What + When | 封装什么逻辑,什么时候执行 | Advice(通知) |
| Where | 对哪些类的哪些方法生效 | Pointcut(切入点) |
AOP 的底层支撑就是动态代理。Spring 容器负责管理 Bean,AOP 的做法是在合适的 Bean 外层包上一层代理对象,让公共逻辑在不改业务代码的前提下被统一织入。
其中,“哪些组件、哪些方法需要增强”对应 Where;“增强逻辑做什么、在什么时候执行”对应 What + When。可以把它理解成:一个负责圈定范围,一个负责安排增强动作。
本章小结
本章核心概念:AOP 的价值不在于“多学一个新名词”,而在于把日志、事务、权限这类横切逻辑从业务代码中抽离出来,让系统更容易复用和维护。
你现在应该掌握:
- 能说明 AOP 与 OOP 分别解决什么问题
- 能用
Where和What + When解释切入点与通知的分工 - 能从“动态代理 + Spring Bean”角度理解 AOP 的基本思路
2. AOP核心术语
| 术语 | 英文 | 说明 |
|---|---|---|
| 目标类 | Target | 需要被代理的原始类 |
| 代理类 | Proxy | 动态代理生成的类 |
| 连接点 | JoinPoint | 可以被增强的方法 |
| 切入点 | PointCut | 定义哪些连接点会被增强 |
| 通知 | Advice | 具体的增强逻辑 |
| 切面 | Aspect | 切入点 + 通知的模块化表示 |
| 织入 | Weaving | 将Advice应用到目标对象的过程 |
本章小结
本章核心概念:AOP 的术语并不是零散记忆的名词表,它们围绕“目标对象被代理后,增强逻辑如何命中并执行”这一条链路组织起来。
你现在应该掌握:
- 能区分 Target、Proxy、JoinPoint、Pointcut、Advice、Aspect 的含义
- 能说清
Aspect = Pointcut + Advice这层组合关系 - 能理解 Weaving 指的是把增强逻辑应用到目标对象的过程
3. Spring AOP底层原理
3.1 动态代理实现
Spring AOP在运行期通过代理方式向目标类织入增强代码,核心原理是动态代理:
// 代理类示意(非真实代码)
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代理 | 目标类没有接口 | 基于子类生成代理 |
JDK动态代理: CGLIB代理:
┌──────────────┐ ┌──────────────┐
│ 接口 │ │ 目标类 │
│ UserService │ │ UserService │
└──────┬───────┘ └──────┬───────┘
│ │
↓ ↓
┌──────────────┐ ┌──────────────┐
│ 代理类 │ │ 子类(代理) │
│ implements │ │ extends │
│ UserService │ │ UserService │
└──────────────┘ └──────────────┘
,JDK 动态代理的核心是 implements 接口,而 CGLIB 代理的核心是 extends 目标类。这张图的重点在于搞清楚代理对象和原始对象之间到底是“实现同一接口”还是“继承原始类型”的关系。
本章小结
本章核心概念:Spring AOP 不是修改原始类源码,而是在运行期生成代理对象,把增强逻辑包进方法调用流程里。
你现在应该掌握:
- 能解释 Spring AOP 为什么依赖动态代理
- 能区分 JDK 动态代理与 CGLIB 代理的适用场景
- 能理解“代理对象代替目标对象对外提供能力”这件事
4. AOP快速入门
4.1 添加依赖
<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
@Configuration
@ComponentScan("com.example")
@EnableAspectJAutoProxy // 开启Spring AOP
public class SpringConfiguration {
}4.3 定义切面
@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 效果验证
@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的基础切面
5. 切入点表达式(PointCut)
5.1 execution表达式
最常用,用于直接匹配方法签名。
语法:
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表达式
匹配标注了指定注解的方法。
// 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表达式
匹配目标对象类上标注了指定注解的方法(类级别的注解)。
// 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三种表达式的应用边界 - 能根据“整包拦截 / 指定方法 / 指定类”选择合适的切点表达式
6. 通知类型(Advice)
6.1 5种通知类型
| 通知类型 | 注解 | 执行时机 | 特点 |
|---|---|---|---|
| 前置通知 | @Before | 方法执行前 | 无法阻止方法执行 |
| 后置通知 | @AfterReturning | 方法正常返回后 | 可获取返回值 |
| 异常通知 | @AfterThrowing | 方法抛出异常后 | 可获取异常对象 |
| 最终通知 | @After | 方法执行结束后 | 无论正常或异常 |
| 环绕通知 | @Around | 方法执行前后 | 最强控制力,可控制是否执行目标方法 |
6.2 执行顺序
正常执行流程:
@Before → 目标方法 → @AfterReturning → @After
异常执行流程:
@Before → 目标方法(抛出异常) → @AfterThrowing → @After6.3 各类通知详解
@Before(前置通知)
@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(返回通知)
@AfterReturning(
value = "servicePointCut()",
returning = "result" // 绑定返回值到参数
)
public void afterReturning(JoinPoint joinPoint, Object result) {
System.out.println("[返回通知] 返回值: " + result);
}@AfterThrowing(异常通知)
@AfterThrowing(
value = "servicePointCut()",
throwing = "ex" // 绑定异常到参数
)
public void afterThrowing(JoinPoint joinPoint, Exception ex) {
System.out.println("[异常通知] 发生异常: " + ex.getMessage());
}@After(最终通知)
@After("servicePointCut()")
public void afterAdvice(JoinPoint joinPoint) {
System.out.println("[最终通知] 方法执行结束");
}@Around(环绕通知)
最强大的通知类型,可以控制目标方法是否执行。
@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 通知选择指南
┌─────────────────────────────────────────────────┐ │ 记录日志/权限检查(不关心结果) → @Before │ │ 记录返回值/结果处理 → @AfterReturning │ │ 异常处理/告警 → @AfterThrowing │ │ 资源清理(必须执行) → @After │ │ 事务控制/性能监控(完全控制) → @Around │ └─────────────────────────────────────────────────┘
本章小结
本章核心概念:不同通知类型的差异,本质上是“它们能介入目标方法执行流程的哪个阶段、能拿到哪些信息、能控制到什么程度”。
你现在应该掌握:
- 能区分
@Before、@AfterReturning、@AfterThrowing、@After、@Around的执行时机 - 能根据日志、异常处理、资源清理、事务控制等场景选择合适通知
- 能理解
@Around中必须调用proceed()才会真正执行目标方法
7. JoinPoint与ProceedingJoinPoint
7.1 JoinPoint API
public interface JoinPoint {
// 获取方法签名(包含方法名、参数类型等)
Signature getSignature();
// 获取目标对象(被代理的原始对象)
Object getTarget();
// 获取代理对象
Object getThis();
// 获取方法参数
Object[] getArgs();
}7.2 ProceedingJoinPoint(环绕通知专用)
public interface ProceedingJoinPoint extends JoinPoint {
// 执行目标方法
Object proceed() throws Throwable;
// 带参数执行目标方法
Object proceed(Object[] args) throws Throwable;
}7.3 使用示例
@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通知中结合参数、返回值和耗时做基础增强
附录:核心注解速查表
| 注解 | 作用 | 使用位置 |
|---|---|---|
@EnableAspectJAutoProxy |
开启AOP功能 | 配置类 |
@Aspect |
标记切面类 | 类 |
@Pointcut |
定义切入点 | 方法 |
@Before |
前置通知 | 方法 |
@AfterReturning |
返回通知 | 方法 |
@AfterThrowing |
异常通知 | 方法 |
@After |
最终通知 | 方法 |
@Around |
环绕通知 | 方法 |
实战练习
<!-- 实战练习内容已分离到 ../practices/markdown/12-spring-aop-practice.md -->
建议先完成本章重点内容复习,再进入配套练习。 练习建议按照:基础 → 进阶 → 综合挑战 的顺序完成。