Spring AOP 零基础入门
AOP(面向切面编程)是 Spring 三大核心(IoC、AOP、事务)之一,也是面试高频考点。它解决了 OOP(面向对象编程)中通用逻辑与业务逻辑耦合的问题,让日志、权限、事务、性能监控这些通用功能与业务代码完全解耦,无需修改原有业务代码即可添加增强功能。
本文用最通俗的语言、最直观的示例,带你从零掌握 Spring AOP 所有核心知识点:核心概念、五大通知、切点表达式、切面优先级、底层原理。
一、核心概念
先给你一个最容易理解的比喻:
假设你开了一家餐厅,所有厨师都在专心炒菜(业务逻辑)。现在需要给每道菜加一道 "消毒" 工序(通用逻辑)。
- 不用 AOP:每个厨师炒菜前都要自己消毒,代码重复、耦合严重
- 用 AOP:雇一个专门的消毒员,在所有菜下锅前自动消毒,厨师只需要专心炒菜
AOP 的核心思想:横向切割业务流程,在不修改原有业务代码的前提下,在指定位置插入通用逻辑。
5 个必懂核心术语
| 术语 | 通俗解释 | 代码对应 |
|---|---|---|
| 连接点(JoinPoint) | 程序执行过程中可以被拦截的点,通常是方法 | 所有可以被增强的方法 |
| 切点(PointCut) | 实际需要被拦截的连接点(筛选后的连接点) | 你写的表达式匹配到的方法 |
| 通知(Advice) | 拦截到方法后要执行的通用逻辑("消毒" 动作) | 日志、权限、事务代码 |
| 切面(Aspect) | 切点 + 通知的组合("消毒员") | 被 @Aspect 标记的类 |
| 目标对象(Target) | 被增强的对象("厨师") | 业务类对象 |
简单记:切面 = 在哪里切(切点) + 切了干什么(通知)
二、五大通知类型(核心)
通知定义了在切点方法的什么时机执行增强逻辑,Spring AOP 提供 5 种通知注解:
1. 五大通知详解
| 注解 | 执行时机 | 常用场景 |
|---|---|---|
@Before | 目标方法执行前执行 | 参数校验、权限检查 |
@After | 目标方法执行后执行(无论是否抛出异常) | 资源释放、日志收尾 |
@Around | 目标方法执行前后都执行 | 性能监控、事务管理(最强大) |
@AfterReturning | 目标方法正常返回后执行 | 结果处理、日志记录 |
@AfterThrowing | 目标方法抛出异常后执行 | 异常日志、告警通知 |
2. 完整代码示例(统一演示)
第一步:引入依赖(SpringBoot 项目)
<!-- Spring AOP 起步依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
第二步:编写业务类(被增强的目标)
import org.springframework.stereotype.Service;
@Service
public class UserService {
public void addUser(String name) {
System.out.println("执行业务逻辑:添加用户 " + name);
}
public String getUserById(Integer id) {
System.out.println("执行业务逻辑:查询用户 " + id);
return "用户" + id;
}
}
第三步:编写日志切面(核心)
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
// 标记为切面类 + 交给Spring管理
@Aspect
@Component
public class LogAspect {
// 定义切点:匹配UserService下的所有方法
@Pointcut("execution(* com.yanglei.service.UserService.*(..))")
public void logPointCut() {}
// 前置通知:方法执行前
@Before("logPointCut()")
public void before(JoinPoint joinPoint) {
System.out.println("【前置通知】方法名:" + joinPoint.getSignature().getName());
}
// 后置通知:方法执行后(无论是否异常)
@After("logPointCut()")
public void after(JoinPoint joinPoint) {
System.out.println("【后置通知】方法执行完成");
}
// 环绕通知:方法执行前后(最强大)
@Around("logPointCut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("【环绕通知-前】开始计时");
long start = System.currentTimeMillis();
// 执行目标方法(必须手动调用,否则目标方法不会执行!)
Object result = pjp.proceed();
long end = System.currentTimeMillis();
System.out.println("【环绕通知-后】方法耗时:" + (end - start) + "ms");
return result;
}
// 返回通知:方法正常返回后
@AfterReturning(value = "logPointCut()", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
System.out.println("【返回通知】方法返回值:" + result);
}
// 异常通知:方法抛出异常后
@AfterThrowing(value = "logPointCut()", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Exception e) {
System.out.println("【异常通知】方法抛出异常:" + e.getMessage());
}
}
3. 执行顺序
运行测试调用 addUser() 方法,输出顺序:
【环绕通知-前】开始计时
【前置通知】方法名:addUser
执行业务逻辑:添加用户 张三
【返回通知】方法返回值:null
【后置通知】方法执行完成
【环绕通知-后】方法耗时:1ms
关键注意点:
@Around必须手动调用pjp.proceed()执行目标方法,否则目标方法不会运行@AfterThrowing只有方法抛出异常时才会执行@After无论方法是否抛出异常都会执行
三、@PointCut 切点复用
@PointCut 注解专门用于定义可复用的切点表达式,避免多个通知重复写相同的表达式。
1. 基本用法
// 定义切点
@Pointcut("execution(* com.yanglei.service.*.*(..))")
public void servicePointCut() {}
// 引用切点
@Before("servicePointCut()")
public void before() {}
2. 跨类引用切点
如果多个切面需要使用同一个切点,可以定义一个公共切点类:
// 公共切点类
public class CommonPointCut {
@Pointcut("execution(* com.yanglei.service.*.*(..))")
public static void allServicePointCut() {}
}
// 其他切面引用
@Before("com.yanglei.aspect.CommonPointCut.allServicePointCut()")
public void before() {}
四、常用切点表达式(两种核心)
切点表达式用于精准匹配需要增强的方法,最常用的有两种:execution 表达式和 @annotation 表达式。
1. execution 表达式(最常用)
用于根据方法签名匹配方法,语法格式:
execution(修饰符 返回值类型 包名.类名.方法名(参数列表) 异常)
常用写法示例
// 匹配UserService下的所有方法
execution(* com.yanglei.service.UserService.*(..))
// 匹配service包下所有类的所有方法
execution(* com.yanglei.service.*.*(..))
// 匹配所有public方法
execution(public * *(..))
// 匹配所有以add开头的方法
execution(* add*(..))
// 匹配所有返回值为String的方法
execution(String *(..))
符号说明
*:匹配任意字符..:匹配任意参数列表.:包分隔符
2. @annotation 表达式(最灵活)
用于匹配被指定注解标记的方法,适合自定义业务场景(比如给需要日志的方法加自定义注解)。
步骤 1:自定义注解
import java.lang.annotation.*;
// 自定义日志注解
@Target(ElementType.METHOD) // 只能标记在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时保留
public @interface Log {
String value() default "";
}
步骤 2:在业务方法上标记注解
@Service
public class UserService {
@Log("添加用户") // 标记需要日志的方法
public void addUser(String name) {
System.out.println("添加用户:" + name);
}
}
步骤 3:编写切面匹配注解
@Aspect
@Component
public class LogAnnotationAspect {
// 匹配所有被@Log注解标记的方法
@Before("@annotation(log)")
public void before(JoinPoint joinPoint, Log log) {
System.out.println("【自定义注解日志】操作:" + log.value());
}
}
优势:粒度更细,哪个方法需要增强就给哪个方法加注解,灵活可控。
五、切面优先级 @Order
当多个切面作用于同一个方法时,需要用 @Order 注解指定切面的执行顺序。
1. 优先级规则
@Order(n):n 是整数,数字越小,优先级越高- 优先级高的切面:前置通知先执行,后置通知后执行(先进后出)
2. 示例
// 切面1:优先级1(最高)
@Aspect
@Component
@Order(1)
public class AuthAspect {
@Before("execution(* com.yanglei.service.*.*(..))")
public void before() {
System.out.println("【权限校验】");
}
}
// 切面2:优先级2
@Aspect
@Component
@Order(2)
public class LogAspect {
@Before("execution(* com.yanglei.service.*.*(..))")
public void before() {
System.out.println("【日志记录】");
}
}
执行顺序:
【权限校验】(优先级高先执行)
【日志记录】
执行业务逻辑
六、Spring AOP 底层原理(面试)
Spring AOP 的底层是动态代理,Spring 会在运行时动态生成代理对象,通过代理对象调用目标方法,在调用过程中插入增强逻辑。
1. 两种动态代理方式
Spring AOP 支持两种动态代理实现,自动根据目标对象是否实现接口选择:
表格
| 代理方式 | 实现原理 | 适用场景 | 限制 |
|---|---|---|---|
| JDK 动态代理 | 基于接口实现,生成目标接口的实现类代理 | 目标对象实现了接口 | 只能代理实现了接口的类 |
| CGLIB 动态代理 | 基于继承实现,生成目标类的子类代理 | 目标对象没有实现接口 | 不能代理 final 类和 final 方法 |
2. SpringBoot 默认配置
- SpringBoot 2.x 及以上版本:默认使用 CGLIB 动态代理,即使目标对象实现了接口
- 如需强制使用 JDK 动态代理,可在
application.yml中配置:spring: aop: proxy-target-class: false
3. AOP 执行流程
- Spring 容器启动时,扫描所有
@Aspect注解的切面类 - 为匹配切点的目标对象生成代理对象
- 当调用目标方法时,实际调用的是代理对象的方法
- 代理对象按照通知顺序,依次执行前置、目标方法、后置等增强逻辑
七、总结
1. 核心知识点速记
- AOP 核心:横向切割,解耦通用逻辑与业务逻辑
- 五大通知:@Before、@After、@Around、@AfterReturning、@AfterThrowing
- 切点表达式:execution(按方法匹配)、@annotation(按注解匹配)
- 切面优先级:@Order (n),数字越小优先级越高
- 底层原理:JDK 动态代理(接口)、CGLIB 动态代理(继承)
2. 面试高频问题
- AOP 和 OOP 的区别?OOP 是纵向继承,解决业务代码复用;AOP 是横向切割,解决通用逻辑复用。
- Spring AOP 和 AspectJ 的区别?Spring AOP 是运行时动态代理,只能代理方法;AspectJ 是编译时织入,功能更强大,支持字段、构造器等增强。
- @Around 为什么必须调用 proceed ()?@Around 会完全拦截目标方法的执行,必须手动调用 proceed () 才能触发目标方法运行。
。
更多推荐



所有评论(0)