这篇文章用 超详细版 + 完整代码 给你拆解Spring AOP,保证手把手教会!🚀
🌈 一、什么是AOP?
AOP(面向切面编程) 的核心思想:把横跨多个模块的通用功能(如日志、事务)抽出来,统一管理,再动态“织入”到需要的地方,让业务代码只关注核心逻辑!
举个🌰:
假设你开了一家奶茶店🍵,核心业务是“做奶茶”,但每次做奶茶前要检查材料库存,做完后要清理操作台。如果用AOP,就能把“检查库存”和“清理操作台”这两个步骤抽出来,自动加到所有奶茶制作流程中,而不需要每个奶茶配方都写一遍!
🔧 二、AOP核心概念
1. 切面(Aspect)
- 是什么:封装通用功能的类(比如日志模块)。
- 怎么做:用
@Aspect注解标记的Spring Bean。
@Aspect // 声明这是一个切面
@Component // 交给Spring管理
public class LogAspect { ... }
2. 通知(Advice)
- 是什么:切面中具体执行的代码逻辑(比如方法前打印日志)。
- 类型:
@Before:目标方法前执行@After:目标方法后执行(无论是否异常)@AfterReturning:目标方法正常返回后执行@AfterThrowing:目标方法抛出异常后执行@Around:包裹目标方法,控制是否执行
3. 切点(Pointcut)
- 是什么:通过表达式定义哪些方法需要被拦截。
- 常用表达式:
execution(* com.example.service.*.*(..)):拦截service包下所有类的所有方法@annotation(com.example.RequireAdmin):拦截带有@RequireAdmin注解的方法
4. 连接点(Joinpoint)
- 是什么:程序执行过程中可以插入切面的点(比如方法调用、异常抛出)。
🛠️ 三、代码示例(Spring Boot环境)
0. 添加依赖
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
1. 简单日志记录(@Before)
目标:在UserService所有方法执行前打印日志。
// UserService.java
@Service
public class UserService {
public void addUser(String username) {
System.out.println("添加用户: " + username);
}
}
// LogAspect.java
@Aspect
@Component
public class LogAspect {
// 定义切点:拦截UserService所有方法
@Pointcut("execution(* com.example.service.UserService.*(..))")
public void userServiceMethods() {}
// 前置通知
@Before("userServiceMethods()")
public void logBefore(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println("🔔 调用方法: " + methodName);
}
}
测试:
@SpringBootTest
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
public void testAddUser() {
userService.addUser("张三");
}
}
输出:
🔔 调用方法: addUser
添加用户: 张三
2. 方法性能监控(@Around)
目标:统计方法执行耗时。
// PerformanceAspect.java
@Aspect
@Component
public class PerformanceAspect {
// 拦截所有Service层方法
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods() {}
// 环绕通知
@Around("serviceMethods()")
public Object logTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed(); // 执行目标方法
long time = System.currentTimeMillis() - start;
String methodName = joinPoint.getSignature().getName();
System.out.println("⏱️ 方法 " + methodName + " 耗时: " + time + "ms");
return result;
}
}
输出:
🔔 调用方法: addUser
添加用户: 张三
⏱️ 方法 addUser 耗时: 12ms
3. 权限校验(自定义注解 + @Before)
目标:只有管理员能执行某些方法。
步骤1:定义自定义注解
// RequireAdmin.java
@Target(ElementType.METHOD) // 注解作用在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
public @interface RequireAdmin {
}
步骤2:在Service方法上添加注解
// UserService.java
@Service
public class UserService {
@RequireAdmin // 只有管理员能调用
public void deleteUser(int userId) {
System.out.println("删除用户: " + userId);
}
}
步骤3:实现权限校验切面
// SecurityAspect.java
@Aspect
@Component
public class SecurityAspect {
// 拦截所有带有@RequireAdmin注解的方法
@Before("@annotation(com.example.annotation.RequireAdmin)")
public void checkAdmin(JoinPoint joinPoint) {
// 模拟获取当前用户
User currentUser = getCurrentUser();
if (!currentUser.isAdmin()) {
throw new RuntimeException("🚨 权限不足!");
}
}
private User getCurrentUser() {
// 模拟返回普通用户
return new User("李四", false);
}
}
测试:
@Test(expected = RuntimeException.class)
public void testDeleteUserWithoutPermission() {
userService.deleteUser(1);
}
输出:
Exception: 🚨 权限不足!
4. 缓存优化(@Around + 缓存逻辑)
目标:优先从缓存读取数据,缓存不存在再查数据库。
// CacheAspect.java
@Aspect
@Component
public class CacheAspect {
// 模拟缓存(实际可用Redis)
private Map<String, Object> cache = new ConcurrentHashMap<>();
// 拦截带有@Cacheable注解的方法
@Around("@annotation(com.example.annotation.Cacheable)")
public Object cacheResult(ProceedingJoinPoint joinPoint) throws Throwable {
String key = generateCacheKey(joinPoint);
Object value = cache.get(key);
if (value != null) {
System.out.println("📦 从缓存获取数据: " + key);
return value;
}
// 缓存不存在,执行方法并缓存结果
value = joinPoint.proceed();
cache.put(key, value);
System.out.println("💾 缓存数据: " + key);
return value;
}
// 生成缓存Key(类名+方法名+参数)
private String generateCacheKey(ProceedingJoinPoint joinPoint) {
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
return className + ":" + methodName + ":" + Arrays.toString(args);
}
}
使用注解:
// UserService.java
@Service
public class UserService {
@Cacheable
public User getUserById(int userId) {
System.out.println("🔍 查询数据库用户: " + userId);
return new User("用户" + userId);
}
}
测试:
@Test
public void testGetUserById() {
userService.getUserById(1); // 第一次查数据库
userService.getUserById(1); // 第二次走缓存
}
输出:
🔍 查询数据库用户: 1
💾 缓存数据: UserService:getUserById:[1]
📦 从缓存获取数据: UserService:getUserById:[1]
🔍 四、AOP底层原理(简单版)
Spring AOP通过 动态代理 实现:
- 如果目标类实现了接口 → 使用 JDK动态代理
- 如果目标类没有实现接口 → 使用 CGLIB代理
代理对象在方法调用时,会按顺序执行切面逻辑(如前置通知→目标方法→后置通知)。
💡 五、AOP最佳实践
1️⃣ 切点表达式尽量精确:避免拦截不需要的方法(如execution(* com.example.service.UserService.*(..))比execution(* *.*(..))更安全)。
2️⃣ 优先使用注解配置:比XML配置更直观。
3️⃣ 注意代理失效场景:同一个类内部方法调用(AOP不生效),可通过注入自身Bean解决。
🎯 六、总结
AOP就像给你的代码加了一个“智能管家”🤖:
- 日志、权限、事务这些重复工作统统交给它!
- 业务代码只专注核心逻辑,不再臃肿!
- 需要加新功能(比如缓存)时,改一处就行,维护超省心!
下次看到代码里重复的try-catch或日志打印,就大喊:“切面来救!” 🌟