Spring AOP 核心知识笔记
一、AOP 核心思想与实现原理
AOP(面向切面编程)的核心是抽取重复语句,与 IOC(抽取重复定义对象)共同构成 Spring 两大核心特性,用于解决代码复用与解耦问题。
实现原理
Spring AOP 通过动态代理技术实现功能增强,具体分为两种方式:
- JDK 动态代理:基于接口实现,要求目标对象必须实现接口。
- CGLIB 动态代理:基于继承实现,无需目标对象实现接口(默认优先使用 JDK 代理,无接口时自动切换为 CGLIB)。
AOP 核心优势
- 减少重复代码(如日志、权限校验等通用逻辑)
- 提高开发效率(通用功能统一维护)
- 代码无侵入(不修改原始业务代码)
- 维护方便(通用逻辑修改仅需改一处)
二、AOP 核心概念
| 概念(英文) | 核心定义 |
|---|---|
| 连接点(JoinPoint) | 被 "添加功能" 的方法 |
| 通知(Advice) | 要添加的 "通用功能" 本身(如日志记录、权限校验,是具体的方法逻辑) |
| 切入点(PointCut) | 筛选 "需要加功能的方法" 的规则 |
| 切面(Aspect) | 通知与切入点的组合,明确 "哪个功能(通知)加到哪些方法(切入点)上" |
| 目标对象(Target) | 被添加功能的原始对象(如 UserService 类的实例) |
三、通知类型(Advice Type)
不同通知对应不同的执行时机,核心区别在于 "执行阶段" 和 "异常时是否运行"。
| 注解 | 通知类型 | 执行时机 | 异常时运行状态 | 正常时运行状态 |
|---|---|---|---|---|
@Around |
环绕通知 | 目标方法前后均执行 | 执行(需捕获异常,或触发 finally 逻辑) |
执行 |
@Before |
前置通知 | 目标方法执行前触发 | 执行(仅在方法执行前,不受后续异常影响) | 执行 |
@After |
后置通知 | 目标方法执行后触发 | 执行(类似 finally,异常后必触发) |
执行(类似 finally,正常后必触发) |
@AfterReturning |
返回后通知 | 目标方法正常返回后触发 | 不执行 | 执行(可获取方法返回值) |
@AfterThrowing |
异常后通知 | 目标方法抛出异常后触发 | 执行(可捕获指定异常类型) | 不执行 |
各通知类型特殊说明
1. @Around(环绕通知)
- 唯一需传入
ProceedingJoinPoint参数的通知,需通过proceed()方法执行原始目标方法; - 返回值必须为
Object,用于接收原始方法的返回值; - 唯一能主动捕获并处理目标方法异常的通知;
2. @After(后置通知)
- 优先级最低,无论目标方法正常执行或抛出异常,最终都会触发;
- 常用于资源释放类操作(如关闭数据库连接、IO 流)。
3. @AfterReturning & @AfterThrowing
- 二者互斥触发 :目标方法正常返回时
@AfterReturning执行,抛出异常时@AfterThrowing执行; @AfterReturning可通过returning属性绑定并获取方法返回值;@AfterThrowing可通过throwing属性指定捕获的异常类型,仅匹配对应异常时触发。
通知执行流程
1. 正常流程
@Before → @Around(前逻辑)→ 目标方法 → @Around(后逻辑)→ @AfterReturning → @After
2. 异常流程
@Before → @Around(前逻辑)→ 目标方法(抛异常)→ @Around(异常捕获 /finally)→ @AfterThrowing → @After
四、通知执行顺序(多切面场景)
当多个切面作用于同一目标方法时,执行顺序通过以下规则控制:
1. 默认规则(无 @Order 时)
按切面类的类名首字母排序(ASCII 码顺序):
- 目标方法前 的通知:字母排名靠前的切面先执行(如
AspectA比AspectB先执行前置通知)。 - 目标方法后 的通知:字母排名靠前的切面后执行(如
AspectA比AspectB后执行后置通知)。
2. 自定义规则(用 @Order 注解)
在切面类上添加 @Order(数字) 控制顺序(数字越小优先级越高):
- 目标方法前 的通知:数字小的切面先执行(如
@Order(1)比@Order(2)先执行前置通知)。 - 目标方法后 的通知:数字小的切面后执行(如
@Order(1)比@Order(2)后执行后置通知)。
五、切入点表达式(PointCut Expression)
作用:定义 "哪些方法需要加入通知",核心有两种形式:execution(按方法签名匹配)和 @annotation(按注解匹配)。
1. execution(按方法签名匹配)
通过方法返回值、包名、类名、方法名、参数定位方法,语法格式:
plaintext
execution(访问修饰符 返回值 包名.类名(或接口名).方法名(方法参数) throws 异常)
关键说明
-
可省略部分:
- 访问修饰符(如 public、protected,一般省略);
- 包名.类名(省略后匹配范围更宽,一般不省略);
- throws 异常(指定方法声明的异常,一般省略)。
-
通配符用法:
通配符 作用 示例 *匹配单个任意符号 execution(* com.service.*.add*(..))..匹配多个连续任意符号 execution(* com..*.find*(String, ..)) -
组合逻辑 :支持用
&&(且)、||(或)、!(非)组合表达式,如:plaintext// 匹配 com.service 包下所有类的 add 或 update 开头的方法 execution(* com.service.*.add*(..)) || execution(* com.service.*.update*(..))
切入点复用(@Pointcut)
在切面类中定义一个空方法,用 @Pointcut 标注表达式,后续通知可直接引用该方法,避免重复写表达式:
java
// 1. 定义切入点(空方法 + @Pointcut)
@Pointcut("execution(* com.service.UserService.*(..))")
private void userServicePointCut() {}
// 2. 通知引用切入点
@Before("userServicePointCut()")
public void beforeAdvice(JoinPoint joinPoint) {
// 通知逻辑
}
2. @annotation(按注解匹配)
通过 "方法是否被特定注解标记" 来匹配,适用于无统一方法名但需加相同功能的场景。
步骤
-
创建自定义注解 :添加
@Retention(RUNTIME)(运行时有效)和@Target(METHOD)(仅作用于方法):java@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Log { // 自定义注解名,如 @Log } -
标记目标方法:在需要加通知的方法上添加自定义注解:
java@Service public class UserService { // 用 @Log 标记该方法,会被 AOP 拦截 @Log public void addUser(User user) { // 业务逻辑 } } -
定义切入点 :通过
@annotation(注解全类名)匹配被标记的方法:java@Aspect @Component public class LogAspect { // 切入点:匹配被 @Log 注解标记的方法 @Pointcut("@annotation(com.annotation.Log)") private void logPointCut() {} // 环绕通知引用切入点 @Around("logPointCut()") public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable { // 日志逻辑 return joinPoint.proceed(); // 执行原始方法 } }
六、连接点(JoinPoint)API
JoinPoint 是 Spring 对 "方法执行连接点" 的抽象,用于获取方法运行时的关键信息(如参数、方法名、目标对象)。
1. 类型区别
| 通知类型 | 必须使用的连接点类型 | 说明 |
|---|---|---|
@Around |
ProceedingJoinPoint |
JoinPoint 子类,支持通过 proceed() 执行原始方法 |
@Before/@After 等 |
JoinPoint |
非环绕通知的通用类型,无 proceed() 方法 |
2. 常用 API
| API 方法 | 返回值类型 | 核心作用 |
|---|---|---|
getTarget() |
Object |
获取被代理的原始目标对象(如 UserService 实例) |
getSignature().getName() |
String |
获取目标方法名(如 "addUser") |
getArgs() |
Object[] |
获取目标方法入参数组(如 [user 对象]) |
getSignature().getDeclaringTypeName() |
String |
获取方法所在类的全类名(如 "com.service.UserService") |
(MethodSignature)getSignature() |
MethodSignature |
强转后可获取 Method 对象(反射操作) |
proceed()(仅 ProceedingJoinPoint) |
Object |
执行原始目标方法(@Around 必备) |
七、AOP 实战案例:记录操作日志
需求:记录方法执行时的操作人、操作时间、全类名、方法名、参数、返回值、执行时长。
步骤
-
导入依赖(Spring Boot 项目):
xml<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> -
创建日志实体类(对应数据库表):
java@Data public class OperateLog { private Long id; private String operator; // 操作人 private LocalDateTime operateTime; // 操作时间 private String className; // 全类名 private String methodName; // 方法名 private String args; // 方法参数(JSON 格式) private String returnValue; // 返回值(JSON 格式) private Long costTime; // 执行时长(毫秒) } -
创建自定义注解 @Log(标记需要记录日志的方法):
java@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Log {} -
定义切面类 LogAspect:
java@Aspect @Component public class LogAspect { @Autowired private HttpServletRequest request; // 用于获取登录用户(如从 Token 中解析) @Autowired private OperateLogMapper operateLogMapper; // 数据库操作 mapper @Autowired private ObjectMapper objectMapper; // 用于参数/返回值转 JSON // 切入点:匹配被 @Log 标记的方法 @Pointcut("@annotation(com.annotation.Log)") private void logPointCut() {} // 环绕通知:记录日志 @Around("logPointCut()") public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable { // 1. 前置逻辑:记录开始时间、获取基础信息 long startTime = System.currentTimeMillis(); OperateLog operateLog = new OperateLog(); // 获取操作人(示例:从请求头 Token 解析,实际需结合登录逻辑) String token = request.getHeader("Token"); String operator = parseOperatorFromToken(token); // 自定义方法:从 Token 取用户名 operateLog.setOperator(operator); // 获取全类名、方法名 String className = joinPoint.getSignature().getDeclaringTypeName(); String methodName = joinPoint.getSignature().getName(); operateLog.setClassName(className); operateLog.setMethodName(methodName); // 获取方法参数(转 JSON) Object[] args = joinPoint.getArgs(); String argsJson = objectMapper.writeValueAsString(args); operateLog.setArgs(argsJson); // 2. 执行原始方法 Object result = null; try { result = joinPoint.proceed(); // 执行目标方法 // 3. 正常返回:记录返回值 String resultJson = objectMapper.writeValueAsString(result); operateLog.setReturnValue(resultJson); } finally { // 4. 后置逻辑:记录执行时长、操作时间,插入数据库(finally 确保必执行) long costTime = System.currentTimeMillis() - startTime; operateLog.setCostTime(costTime); operateLog.setOperateTime(LocalDateTime.now()); operateLogMapper.insert(operateLog); // 插入日志表 } return result; } // 自定义方法:从 Token 解析操作人(示例,实际需结合 JWT 等逻辑) private String parseOperatorFromToken(String token) { // 省略 Token 解析逻辑,返回用户名 return "admin"; } } -
标记目标方法 :在需要记录日志的业务方法上添加
@Log:java@Service public class UserService { @Log // 标记该方法需要记录操作日志 public User addUser(User user) { // 业务逻辑:新增用户 return user; } }
八、切入点表达式书写建议
- 规范方法命名 :如查询方法统一用
find开头、更新用update开头,方便execution表达式匹配。 - 基于接口描述 :优先匹配接口方法(如
execution(* com.service.UserService.*(..))),而非实现类,增强扩展性。 - 缩小匹配范围 :避免使用过于宽泛的表达式(如
execution(* com..*(..))),减少不必要的拦截,提升性能。