本文收录于「Java 学习日记」专栏,聚焦 Spring 核心特性 ——AOP(面向切面编程),从底层原理、核心概念拆解到注解式开发 + 日志实战,帮你彻底掌握 “无侵入式增强代码” 的精髓~
一、为什么要学 Spring AOP?
在传统开发中,我们会遇到一个核心问题:通用功能与业务逻辑耦合。
- 日志记录、性能监控、事务管理、权限校验等通用功能,需要在每个业务方法中重复编写;
- 修改通用功能时(如调整日志格式),需要修改所有相关业务代码,维护成本极高;
- 业务代码中混杂大量非核心逻辑,可读性差,不符合 “单一职责原则”。
而Spring AOP(Aspect-Oriented Programming) 就是为解决这些问题而生:
- 核心目标:将日志、事务、权限等 “横切逻辑” 从业务逻辑中抽离出来,以 “切面” 的形式统一管理,实现 “无侵入式增强”;
- 核心优势:不修改原有业务代码,仅通过配置 / 注解就能为目标方法添加增强逻辑;
- 实际应用:Spring 事务管理(@Transactional)、全局日志、接口权限校验、性能监控等,都是 AOP 的典型场景。
今天这篇日记,我们从 AOP 核心概念→底层原理→注解开发→日志实战,逐步拆解 Spring AOP 的使用方式。
二、AOP 核心概念(必须先理解)
AOP 的核心是 “切面” 和 “连接点”,先通过通俗的例子理解核心术语:
假设你要给公司所有员工的 “上班打卡” 方法添加两个通用逻辑:① 打卡前验证身份(前置增强);② 打卡后记录打卡日志(后置增强)。
- 员工的 “上班打卡” 方法 = 连接点;
- 所有需要打卡的员工 = 切入点;
- 身份验证 + 日志记录 = 通知(增强逻辑);
- 把通知绑定到切入点的规则 = 切面;
- 整个添加增强的过程 = 织入。
AOP 核心术语对照表
| Joinpoint(连接点) | 连接点 | 可以被增强的方法(如 Service 的业务方法、Controller 的请求方法) |
| Pointcut(切入点) | 切入点 | 实际被增强的连接点(如指定包下的所有 Service 方法) |
| Advice(通知 / 增强) | 通知 / 增强 | 切入到连接点的通用逻辑(如日志、事务),分前置、后置、环绕等类型 |
| Aspect(切面) | 切面 | 切入点 + 通知的组合(定义 “对哪些方法,在什么时候,做什么增强”) |
| Weaving(织入) | 织入 | 将切面逻辑嵌入到目标方法的过程(Spring 在运行时织入) |
| Target(目标对象) | 目标对象 | 被增强的原始对象(如 UserService) |
| Proxy(代理对象) | 代理对象 | 织入切面后生成的对象(Spring AOP 默认用 JDK 动态代理 / CGLIB 代理) |
通知(Advice)的 5 种类型
| @Before | 目标方法执行前 | 无法阻止目标方法执行(除非抛异常) |
| @AfterReturning | 目标方法正常执行后 | 可获取方法返回值 |
| @AfterThrowing | 目标方法抛出异常后 | 可获取异常信息 |
| @After | 目标方法执行后(无论是否异常) | 类似 finally,总会执行 |
| @Around | 目标方法执行前后(环绕) | 可控制目标方法的执行时机、是否执行,功能最强 |
三、Spring AOP 底层原理(动态代理)
Spring AOP 的核心是动态代理,通过代理对象包裹目标对象,在不修改目标对象代码的前提下,为方法添加增强逻辑。Spring 支持两种动态代理方式:
1. JDK 动态代理(默认)
- 适用场景:目标对象实现了接口;
- 核心原理:通过java.lang.reflect.Proxy类动态生成代理类,代理类实现与目标对象相同的接口;
- 特点:基于接口代理,无侵入,JDK 原生支持。
2. CGLIB 代理
- 适用场景:目标对象没有实现接口;
- 核心原理:通过 ASM 字节码框架生成目标对象的子类,重写目标方法;
- 特点:基于子类代理,需保证目标方法非 final(final 方法无法重写)。
动态代理核心流程(伪代码)
java
运行
// 1. 定义增强逻辑(通知)
public class LogAdvice {
public void before() { System.out.println("方法执行前记录日志"); }
public void after() { System.out.println("方法执行后记录日志"); }
}
// 2. 生成代理对象
UserService target = new UserServiceImpl(); // 目标对象
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
(proxyObj, method, args) -> {
// 前置增强
new LogAdvice().before();
// 执行目标方法
Object result = method.invoke(target, args);
// 后置增强
new LogAdvice().after();
return result;
}
);
// 3. 调用代理对象方法(触发增强)
proxy.getUserById("1001");
四、Spring AOP 实战:注解式开发(日志案例)
1. 环境准备
(1)Maven 依赖(核心)
xml
<dependencies>
<!– Spring Context(包含AOP基础) –>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.29</version>
</dependency>
<!– Spring AOP核心依赖 –>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.29</version>
</dependency>
<!– AspectJ(AOP注解解析) –>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.9.1</version>
</dependency>
</dependencies>
(2)配置类(开启 AOP 注解)
java
运行
package com.example.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
// 开启AOP注解支持
@EnableAspectJAutoProxy
@Configuration
@ComponentScan("com.example") // 扫描注解
public class SpringConfig {
}
2. 编写业务目标对象
java
运行
// 业务接口
package com.example.service;
public interface UserService {
String getUserById(String userId);
void deleteUser(String userId) throws Exception;
}
// 业务实现类(目标对象)
package com.example.service.impl;
import com.example.service.UserService;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
@Override
public String getUserById(String userId) {
System.out.println("【业务逻辑】查询用户ID:" + userId);
return "用户-" + userId;
}
@Override
public void deleteUser(String userId) throws Exception {
System.out.println("【业务逻辑】删除用户ID:" + userId);
// 模拟异常
if ("1002".equals(userId)) {
throw new Exception("删除用户失败:用户不存在");
}
}
}
3. 编写切面类(核心)
java
运行
package com.example.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
// 1. 标记为组件(让Spring管理)
@Component
// 2. 标记为切面
@Aspect
public class LogAspect {
// 3. 定义切入点(通用表达式,可被多个通知复用)
// 切入点表达式:匹配com.example.service包下所有类的所有方法
@Pointcut("execution(* com.example.service.*.*(..))")
public void servicePointcut() {}
// 4. 前置通知:目标方法执行前执行
@Before("servicePointcut()")
public void beforeAdvice(JoinPoint joinPoint) {
// 获取方法名、参数
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("[前置通知] 方法:" + methodName + ",参数:" + Arrays.toString(args));
}
// 5. 后置返回通知:目标方法正常执行后执行(可获取返回值)
@AfterReturning(value = "servicePointcut()", returning = "result")
public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[后置返回通知] 方法:" + methodName + ",返回值:" + result);
}
// 6. 后置异常通知:目标方法抛出异常后执行(可获取异常)
@AfterThrowing(value = "servicePointcut()", throwing = "e")
public void afterThrowingAdvice(JoinPoint joinPoint, Exception e) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[后置异常通知] 方法:" + methodName + ",异常:" + e.getMessage());
}
// 7. 最终通知:目标方法执行后执行(无论是否异常)
@After("servicePointcut()")
public void afterAdvice(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[最终通知] 方法:" + methodName + " 执行完成");
}
// 8. 环绕通知:功能最强,可控制目标方法执行
@Around("servicePointcut()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
long startTime = System.currentTimeMillis();
Object result = null;
try {
// 执行前置逻辑
System.out.println("[环绕通知-前置] 方法:" + methodName + " 开始执行");
// 执行目标方法(必须调用,否则目标方法不执行)
result = joinPoint.proceed();
// 执行后置返回逻辑
System.out.println("[环绕通知-后置返回] 方法:" + methodName + " 执行成功");
} catch (Throwable e) {
// 执行后置异常逻辑
System.out.println("[环绕通知-后置异常] 方法:" + methodName + " 执行异常:" + e.getMessage());
throw e; // 抛出异常,不影响原有业务逻辑
} finally {
// 执行最终逻辑
long costTime = System.currentTimeMillis() – startTime;
System.out.println("[环绕通知-最终] 方法:" + methodName + " 耗时:" + costTime + "ms");
}
return result;
}
}
4. 切入点表达式(核心语法)
上面的@Pointcut("execution(* com.example.service.*.*(..))")是切入点表达式,核心语法:
plaintext
execution(修饰符 返回值 包名.类名.方法名(参数))
- *:通配符,匹配任意(如返回值任意、方法名任意);
- ..:匹配任意参数个数、任意类型;
- 示例:
- execution(public * com.example.service.UserService.*(..)):匹配 UserService 的所有公有方法;
- execution(* com.example.service..*.*(String)):匹配 service 包及子包下所有类的单个 String 参数方法;
- @annotation(com.example.annotation.Log):匹配标注了 @Log 注解的方法(自定义注解切入点)。
5. 测试 AOP 增强效果
java
运行
package com.example.test;
import com.example.config.SpringConfig;
import com.example.service.UserService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class AopTest {
public static void main(String[] args) {
// 1. 创建Spring容器
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
// 2. 获取代理对象(注意:这里获取的是代理对象,而非原始UserServiceImpl)
UserService userService = context.getBean(UserService.class);
// 3. 测试正常方法
System.out.println("===== 测试正常方法 =====");
userService.getUserById("1001");
// 4. 测试异常方法
System.out.println("\\n===== 测试异常方法 =====");
try {
userService.deleteUser("1002");
} catch (Exception e) {
// 捕获异常,不影响程序运行
}
// 5. 关闭容器
context.close();
}
}
6. 运行结果(核心流程验证)
plaintext
===== 测试正常方法 =====
[环绕通知-前置] 方法:getUserById 开始执行
[前置通知] 方法:getUserById,参数:[1001]
【业务逻辑】查询用户ID:1001
[后置返回通知] 方法:getUserById,返回值:用户-1001
[最终通知] 方法:getUserById 执行完成
[环绕通知-后置返回] 方法:getUserById 执行成功
[环绕通知-最终] 方法:getUserById 耗时:1ms
===== 测试异常方法 =====
[环绕通知-前置] 方法:deleteUser 开始执行
[前置通知] 方法:deleteUser,参数:[1002]
【业务逻辑】删除用户ID:1002
[后置异常通知] 方法:deleteUser,异常:删除用户失败:用户不存在
[最终通知] 方法:deleteUser 执行完成
[环绕通知-后置异常] 方法:deleteUser 执行异常:删除用户失败:用户不存在
[环绕通知-最终] 方法:deleteUser 耗时:0ms
五、AOP 进阶:自定义注解实现精准切入
1. 自定义注解
java
运行
package com.example.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 标注在方法上
@Target(ElementType.METHOD)
// 运行时生效
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
// 注解属性:日志描述
String value() default "";
}
2. 修改切入点表达式(匹配自定义注解)
java
运行
@Aspect
@Component
public class LogAspect {
// 匹配标注了@Log注解的方法
@Pointcut("@annotation(com.example.annotation.Log)")
public void logAnnotationPointcut() {}
// 通知绑定自定义切入点
@Around("logAnnotationPointcut() && @annotation(log)")
public Object aroundLogAnnotation(ProceedingJoinPoint joinPoint, Log log) throws Throwable {
String desc = log.value();
System.out.println("[自定义注解通知] 日志描述:" + desc);
return joinPoint.proceed();
}
}
3. 业务方法添加自定义注解
java
运行
@Service
public class UserServiceImpl implements UserService {
@Override
@Log("查询用户信息") // 添加自定义注解
public String getUserById(String userId) {
System.out.println("【业务逻辑】查询用户ID:" + userId);
return "用户-" + userId;
}
}
4. 测试结果
plaintext
[自定义注解通知] 日志描述:查询用户信息
[前置通知] 方法:getUserById,参数:[1001]
【业务逻辑】查询用户ID:1001
…
六、AOP 避坑指南与最佳实践
1. 常见问题与解决方案
| AOP 增强不生效 | 1. 未加@EnableAspectJAutoProxy;2. 目标类 / 切面类未被 Spring 管理;3. 切入点表达式错误 | 1. 检查注解开启;2. 确保添加@Component/@Service;3. 验证切入点表达式 |
| 环绕通知中未调用proceed() | 目标方法不会执行 | 必须调用joinPoint.proceed() |
| CGLIB 代理报错(final 方法) | 目标方法是 final,无法被子类重写 | 移除 final 修饰符,或改用 JDK 代理 |
| 内部方法调用 AOP 不生效 | 目标对象内部方法调用,未经过代理对象 | 1. 注入自身代理对象;2. 通过 ApplicationContext 获取代理对象 |
2. 最佳实践
- 简单增强(如日志)用@Before/@AfterReturning;
- 需控制目标方法执行(如性能监控、限流)用@Around;
七、今日实战小任务
总结
下一篇【Day42】预告:Spring 声明式事务:@Transactional 注解使用与事务传播机制,关注专栏继续解锁 Spring 核心特性~若本文对你有帮助,欢迎点赞 + 收藏 + 关注,你的支持是我更新的最大动力💖!




