文章目录
- 一、什么是AOP?
- 二、AOP核心概念
- 1. 切面(Aspect)
- 2. 通知(Advice)
- 3. 切点(Pointcut)
- 4. 连接点(Joinpoint)
- ️ 三、代码示例(Spring Boot环境)
- 0. 添加依赖
- 1. 简单日志记录(@Before)
- 2. 方法性能监控(@Around)
- 3. 权限校验(自定义注解 + @Before)
- 4. 缓存优化(@Around + 缓存逻辑)
- 四、AOP底层原理(简单版)
- 五、AOP最佳实践
- 六、总结
这篇文章用 超详细版 + 完整代码 给你拆解Spring AOP,保证手把手教会!
AOP(面向切面编程) 的核心思想:把横跨多个模块的通用功能(如日志、事务)抽出来,统一管理,再动态“织入”到需要的地方,让业务代码只关注核心逻辑!
举个:
假设你开了一家奶茶店,核心业务是“做奶茶”,但每次做奶茶前要检查材料库存,做完后要清理操作台。如果用AOP,就能把“检查库存”和“清理操作台”这两个步骤抽出来,自动加到所有奶茶制作流程中,而不需要每个奶茶配方都写一遍!
@Aspect
注解标记的Spring Bean。@Aspect // 声明这是一个切面
@Component // 交给Spring管理
public class LogAspect { ... }
@Before
:目标方法前执行@After
:目标方法后执行(无论是否异常)@AfterReturning
:目标方法正常返回后执行@AfterThrowing
:目标方法抛出异常后执行@Around
:包裹目标方法,控制是否执行execution(* com.example.service.*.*(..))
:拦截service
包下所有类的所有方法@annotation(com.example.RequireAdmin)
:拦截带有@RequireAdmin
注解的方法
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
目标:在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
添加用户: 张三
目标:统计方法执行耗时。
// 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
目标:只有管理员能执行某些方法。
步骤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: 权限不足!
目标:优先从缓存读取数据,缓存不存在再查数据库。
// 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]
Spring AOP通过 动态代理 实现:
代理对象在方法调用时,会按顺序执行切面逻辑(如前置通知→目标方法→后置通知)。
1️⃣ 切点表达式尽量精确:避免拦截不需要的方法(如execution(* com.example.service.UserService.*(..))
比execution(* *.*(..))
更安全)。
2️⃣ 优先使用注解配置:比XML配置更直观。
3️⃣ 注意代理失效场景:同一个类内部方法调用(AOP不生效),可通过注入自身Bean解决。
AOP就像给你的代码加了一个“智能管家”:
下次看到代码里重复的try-catch
或日志打印,就大喊:“切面来救!”