# Spring AOP 详细学习笔记 ## 一、AOP基础概念 ### 1.1 什么是AOP AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,它通过将横切关注点(cross-cutting concerns)从业务逻辑中分离出来,实现了关注点的模块化。 ### 1.2 AOP的核心概念 #### 1.2.1 切面(Aspect) 切面是横切关注点的模块化,包含了通知和切入点的定义。 #### 1.2.2 连接点(Join Point) 程序执行过程中的某个特定点,如方法调用、方法执行、字段访问等。在Spring AOP中,连接点总是方法的执行。 #### 1.2.3 通知(Advice) 切面在特定连接点执行的动作。主要有以下类型: - **前置通知(Before)**:在目标方法执行前执行 - **后置通知(After)**:在目标方法执行后执行(无论是否异常) - **返回通知(After Returning)**:在目标方法正常返回后执行 - **异常通知(After Throwing)**:在目标方法抛出异常后执行 - **环绕通知(Around)**:包围目标方法,可以在方法执行前后都执行自定义逻辑 #### 1.2.4 切入点(Pointcut) 用于匹配连接点的谓词表达式。通过切入点表达式,我们可以指定哪些方法需要被增强。 #### 1.2.5 引入(Introduction) 允许向现有类添加新方法或属性。 #### 1.2.6 目标对象(Target Object) 被一个或多个切面通知的对象。 #### 1.2.7 AOP代理(AOP Proxy) AOP框架创建的对象,用于实现切面契约。在Spring中,AOP代理可以是JDK动态代理或CGLIB代理。 #### 1.2.8 织入(Weaving) 将切面应用到目标对象并创建代理对象的过程。 ### 1.3 Spring AOP的特点 - 基于代理的AOP实现 - 只支持方法级别的连接点 - 与Spring IoC容器完美集成 - 支持基于注解和XML的配置方式 ## 二、Spring AOP的使用方式 ### 2.1 引入依赖 ```xml org.springframework spring-aop 5.3.23 org.aspectj aspectjweaver 1.9.7 ``` ### 2.2 启用AOP #### 注解方式启用 ```java @Configuration @EnableAspectJAutoProxy public class AppConfig { // 配置类内容 } ``` #### XML方式启用 ```xml ``` ## 三、基于注解的AOP ### 3.1 定义切面类 ```java @Aspect @Component public class LoggingAspect { // 定义切入点 @Pointcut("execution(* com.example.service.*.*(..))") public void serviceLayer() {} // 前置通知 @Before("serviceLayer()") public void logBefore(JoinPoint joinPoint) { System.out.println("执行方法: " + joinPoint.getSignature().getName()); System.out.println("参数: " + Arrays.toString(joinPoint.getArgs())); } // 后置通知 @After("serviceLayer()") public void logAfter(JoinPoint joinPoint) { System.out.println("方法执行完毕: " + joinPoint.getSignature().getName()); } // 返回通知 @AfterReturning(pointcut = "serviceLayer()", returning = "result") public void logAfterReturning(JoinPoint joinPoint, Object result) { System.out.println("方法返回值: " + result); } // 异常通知 @AfterThrowing(pointcut = "serviceLayer()", throwing = "error") public void logAfterThrowing(JoinPoint joinPoint, Throwable error) { System.out.println("方法异常: " + error.getMessage()); } // 环绕通知 @Around("serviceLayer()") public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("环绕通知 - 方法执行前"); long startTime = System.currentTimeMillis(); try { Object result = joinPoint.proceed(); long endTime = System.currentTimeMillis(); System.out.println("方法执行时间: " + (endTime - startTime) + "ms"); return result; } catch (Throwable e) { System.out.println("环绕通知 - 捕获异常: " + e.getMessage()); throw e; } } } ``` ### 3.2 示例服务类 ```java @Service public class UserService { public User findUserById(Long id) { System.out.println("查找用户: " + id); return new User(id, "张三"); } public void saveUser(User user) { System.out.println("保存用户: " + user.getName()); } public void deleteUser(Long id) { System.out.println("删除用户: " + id); if (id == null) { throw new IllegalArgumentException("用户ID不能为空"); } } } ``` ## 四、切入点表达式详解 ### 4.1 execution表达式 最常用的切入点表达式,用于匹配方法执行。 语法:`execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)` 示例: ```java // 匹配所有public方法 @Pointcut("execution(public * *(..))") // 匹配指定包下所有类的所有方法 @Pointcut("execution(* com.example.service.*.*(..))") // 匹配指定类的所有方法 @Pointcut("execution(* com.example.service.UserService.*(..))") // 匹配所有返回值为void的方法 @Pointcut("execution(void *(..))") // 匹配第一个参数为Long类型的方法 @Pointcut("execution(* *(Long,..))") // 匹配所有以save开头的方法 @Pointcut("execution(* save*(..))") ``` ### 4.2 within表达式 限制匹配特定类型内的连接点。 ```java // 匹配指定包下的所有方法 @Pointcut("within(com.example.service.*)") // 匹配指定类的所有方法 @Pointcut("within(com.example.service.UserService)") ``` ### 4.3 @annotation表达式 匹配带有特定注解的方法。 ```java // 自定义注解 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Loggable { String value() default ""; } // 切入点表达式 @Pointcut("@annotation(com.example.annotation.Loggable)") public void loggableMethods() {} // 使用注解 @Service public class OrderService { @Loggable("创建订单") public void createOrder(Order order) { // 业务逻辑 } } ``` ### 4.4 @within表达式 匹配特定注解标注的类内的所有方法。 ```java @Pointcut("@within(org.springframework.stereotype.Service)") public void allServiceMethods() {} ``` ### 4.5 args表达式 匹配特定参数类型的方法。 ```java // 匹配只有一个String参数的方法 @Pointcut("args(String)") // 匹配第一个参数为String的方法 @Pointcut("args(String,..)") ``` ### 4.6 组合切入点表达式 使用&&、||、!组合多个切入点表达式。 ```java @Pointcut("execution(* com.example.service.*.*(..)) && @annotation(Loggable)") public void serviceLoggableMethods() {} @Pointcut("within(com.example.service.*) || within(com.example.dao.*)") public void serviceOrDaoMethods() {} @Pointcut("execution(* com.example.service.*.*(..)) && !execution(* com.example.service.*.get*(..))") public void nonGetterServiceMethods() {} ``` ## 五、通知参数传递 ### 5.1 获取方法参数 ```java @Aspect @Component public class ParameterAspect { @Before("execution(* com.example.service.*.*(..)) && args(id,name,..)") public void logWithParams(Long id, String name) { System.out.println("方法参数 - ID: " + id + ", Name: " + name); } @Around("@annotation(loggable)") public Object logWithAnnotation(ProceedingJoinPoint joinPoint, Loggable loggable) throws Throwable { System.out.println("注解值: " + loggable.value()); return joinPoint.proceed(); } } ``` ### 5.2 获取目标对象和代理对象 ```java @Before("execution(* com.example.service.*.*(..))") public void logTarget(JoinPoint joinPoint) { Object target = joinPoint.getTarget(); // 目标对象 Object proxy = joinPoint.getThis(); // 代理对象 System.out.println("目标类: " + target.getClass().getName()); } ``` ## 六、基于XML的AOP配置 ### 6.1 XML配置示例 ```xml ``` ### 6.2 XML配置的切面类 ```java public class LoggingAspect { public void logBefore(JoinPoint joinPoint) { System.out.println("XML配置 - 前置通知: " + joinPoint.getSignature().getName()); } public void logAfter(JoinPoint joinPoint) { System.out.println("XML配置 - 后置通知: " + joinPoint.getSignature().getName()); } public void logAfterReturning(JoinPoint joinPoint, Object result) { System.out.println("XML配置 - 返回通知: " + result); } public void logAfterThrowing(JoinPoint joinPoint, Throwable error) { System.out.println("XML配置 - 异常通知: " + error.getMessage()); } public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("XML配置 - 环绕通知开始"); Object result = joinPoint.proceed(); System.out.println("XML配置 - 环绕通知结束"); return result; } } ``` ## 七、实际应用场景 ### 7.1 日志记录 ```java @Aspect @Component @Slf4j public class LogAspect { @Around("@annotation(Log)") public Object log(ProceedingJoinPoint joinPoint) throws Throwable { String methodName = joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); log.info("开始执行方法: {}, 参数: {}", methodName, Arrays.toString(args)); long startTime = System.currentTimeMillis(); try { Object result = joinPoint.proceed(); long costTime = System.currentTimeMillis() - startTime; log.info("方法执行成功: {}, 耗时: {}ms, 返回值: {}", methodName, costTime, result); return result; } catch (Throwable e) { long costTime = System.currentTimeMillis() - startTime; log.error("方法执行失败: {}, 耗时: {}ms, 异常: {}", methodName, costTime, e.getMessage()); throw e; } } } ``` ### 7.2 权限控制 ```java @Aspect @Component public class SecurityAspect { @Autowired private SecurityService securityService; @Before("@annotation(RequiresPermission)") public void checkPermission(JoinPoint joinPoint) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); RequiresPermission permission = signature.getMethod().getAnnotation(RequiresPermission.class); if (!securityService.hasPermission(permission.value())) { throw new UnauthorizedException("没有权限执行此操作: " + permission.value()); } } } // 使用示例 @Service public class AdminService { @RequiresPermission("admin:user:delete") public void deleteUser(Long userId) { // 删除用户逻辑 } } ``` ### 7.3 缓存处理 ```java @Aspect @Component public class CacheAspect { private Map cache = new ConcurrentHashMap<>(); @Around("@annotation(Cacheable)") public Object cache(ProceedingJoinPoint joinPoint) throws Throwable { String key = generateKey(joinPoint); if (cache.containsKey(key)) { System.out.println("从缓存获取数据: " + key); return cache.get(key); } Object result = joinPoint.proceed(); cache.put(key, result); System.out.println("将数据放入缓存: " + key); return result; } private String generateKey(ProceedingJoinPoint joinPoint) { return joinPoint.getSignature().toString() + Arrays.toString(joinPoint.getArgs()); } } ``` ### 7.4 事务管理 ```java @Aspect @Component public class TransactionAspect { @Autowired private TransactionManager transactionManager; @Around("@annotation(Transactional)") public Object manageTransaction(ProceedingJoinPoint joinPoint) throws Throwable { transactionManager.beginTransaction(); try { Object result = joinPoint.proceed(); transactionManager.commit(); return result; } catch (Exception e) { transactionManager.rollback(); throw e; } } } ``` ### 7.5 性能监控 ```java @Aspect @Component public class PerformanceAspect { private static final int SLOW_EXECUTION_THRESHOLD = 1000; // 1秒 @Around("@annotation(MonitorPerformance)") public Object monitor(ProceedingJoinPoint joinPoint) throws Throwable { String methodName = joinPoint.getSignature().toShortString(); long startTime = System.currentTimeMillis(); try { return joinPoint.proceed(); } finally { long executionTime = System.currentTimeMillis() - startTime; if (executionTime > SLOW_EXECUTION_THRESHOLD) { log.warn("方法执行缓慢: {} 耗时 {}ms", methodName, executionTime); // 可以发送告警通知 } // 记录性能指标 recordMetrics(methodName, executionTime); } } private void recordMetrics(String methodName, long executionTime) { // 将性能数据发送到监控系统 } } ``` ### 7.6 重试机制 ```java @Aspect @Component public class RetryAspect { @Around("@annotation(Retry)") public Object retry(ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Retry retry = signature.getMethod().getAnnotation(Retry.class); int attempts = retry.attempts(); long delay = retry.delay(); Exception lastException = null; for (int i = 0; i < attempts; i++) { try { return joinPoint.proceed(); } catch (Exception e) { lastException = e; if (i < attempts - 1) { log.warn("方法执行失败,{}ms后重试,当前第{}次尝试", delay, i + 1); Thread.sleep(delay); } } } throw new RuntimeException("重试" + attempts + "次后仍然失败", lastException); } } // 使用示例 @Service public class EmailService { @Retry(attempts = 3, delay = 1000) public void sendEmail(String to, String subject, String content) { // 发送邮件逻辑,可能会失败 } } ``` ## 八、最佳实践 ### 8.1 切面优先级 当多个切面作用于同一个连接点时,可以通过@Order注解指定优先级: ```java @Aspect @Component @Order(1) // 数字越小,优先级越高 public class SecurityAspect { // 安全检查逻辑 } @Aspect @Component @Order(2) public class LoggingAspect { // 日志记录逻辑 } ``` ### 8.2 避免循环依赖 切面不应该通知自己,避免无限循环: ```java @Aspect @Component public class LoggingAspect { // 排除切面类本身 @Pointcut("execution(* com.example..*.*(..)) && !within(com.example.aspect..*)") public void applicationMethods() {} } ``` ### 8.3 合理使用通知类型 - 使用@Before进行参数验证和权限检查 - 使用@AfterReturning处理返回值 - 使用@AfterThrowing进行异常处理和告警 - 使用@Around进行性能监控和事务管理 - 简单场景避免使用@Around,因为它最强大但也最复杂 ### 8.4 切入点表达式优化 - 尽量缩小切入点范围,避免过度匹配 - 使用within()限定包或类范围 - 组合使用多个表达式提高精确度 ### 8.5 异常处理 在环绕通知中正确处理异常: ```java @Around("applicationMethods()") public Object handleException(ProceedingJoinPoint joinPoint) throws Throwable { try { return joinPoint.proceed(); } catch (BusinessException e) { // 业务异常,记录日志后继续抛出 log.error("业务异常: {}", e.getMessage()); throw e; } catch (Exception e) { // 系统异常,转换为统一的异常类型 log.error("系统异常", e); throw new SystemException("系统错误", e); } } ``` ## 九、常见问题 ### 9.1 AOP不生效的原因 1. 目标类没有被Spring管理(未添加@Component等注解) 2. 调用的是类内部方法(self-invocation) 3. 方法是private的(Spring AOP只能代理public方法) 4. 没有启用AOP(@EnableAspectJAutoProxy) ### 9.2 JDK代理 vs CGLIB代理 - JDK代理:基于接口,目标类必须实现接口 - CGLIB代理:基于继承,可以代理没有实现接口的类 配置使用CGLIB代理: ```java @EnableAspectJAutoProxy(proxyTargetClass = true) ``` ### 9.3 内部方法调用问题 解决方案: ```java @Service public class UserService { @Autowired private UserService self; // 注入自己的代理对象 public void method1() { // 通过代理对象调用,AOP生效 self.method2(); } @Transactional public void method2() { // 业务逻辑 } } ``` ## 十、总结 Spring AOP提供了一种优雅的方式来处理横切关注点,通过本笔记的学习,你应该掌握了: 1. AOP的核心概念和原理 2. 基于注解和XML的AOP配置方式 3. 各种切入点表达式的使用 4. 不同类型通知的应用场景 5. 实际项目中的AOP应用案例 6. AOP的最佳实践和常见问题解决 建议通过实际编码练习来加深理解,特别是在日志记录、权限控制、事务管理等场景中应用AOP。