From 2a876d1d38ac8f59d217054018ef0b8d611d875c Mon Sep 17 00:00:00 2001 From: root Date: Thu, 26 Jun 2025 01:15:22 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0Spring=20AOP=E5=AD=A6?= =?UTF-8?q?=E4=B9=A0=E7=AC=94=E8=AE=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建Spring AOP详细学习笔记,包含基础概念、使用方式、切入点表达式等 - 创建Spring AOP自定义注解详解,包含实战案例和最佳实践 - 涵盖日志、权限、缓存、限流、重试等常见应用场景 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- Spring-AOP-学习笔记.md | 669 ++++++++++++++++++++++++ Spring-AOP-自定义注解详解.md | 971 +++++++++++++++++++++++++++++++++++ 2 files changed, 1640 insertions(+) create mode 100644 Spring-AOP-学习笔记.md create mode 100644 Spring-AOP-自定义注解详解.md diff --git a/Spring-AOP-学习笔记.md b/Spring-AOP-学习笔记.md new file mode 100644 index 0000000..7df78be --- /dev/null +++ b/Spring-AOP-学习笔记.md @@ -0,0 +1,669 @@ +# 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。 \ No newline at end of file diff --git a/Spring-AOP-自定义注解详解.md b/Spring-AOP-自定义注解详解.md new file mode 100644 index 0000000..4086034 --- /dev/null +++ b/Spring-AOP-自定义注解详解.md @@ -0,0 +1,971 @@ +# Spring AOP 自定义注解详解 + +## 一、为什么使用自定义注解配合AOP + +### 1.1 优势 +1. **代码简洁**:只需在方法上添加注解即可 +2. **语义明确**:注解名称直接表达意图 +3. **易于维护**:修改切面逻辑不影响业务代码 +4. **灵活配置**:通过注解参数传递配置信息 +5. **解耦合**:业务逻辑与横切关注点分离 + +### 1.2 对比传统方式 +```java +// 传统方式:基于方法名匹配 +@Pointcut("execution(* com.example.service.*.*(..))") + +// 注解方式:更精确、更灵活 +@Pointcut("@annotation(com.example.annotation.Log)") +``` + +## 二、自定义注解的创建 + +### 2.1 基本结构 +```java +@Target(ElementType.METHOD) // 注解作用目标 +@Retention(RetentionPolicy.RUNTIME) // 注解保留策略 +@Documented // 生成文档 +public @interface Log { + String value() default ""; // 注解参数 + String module() default ""; // 模块名 + LogLevel level() default LogLevel.INFO; // 日志级别 +} + +// 枚举类型 +public enum LogLevel { + DEBUG, INFO, WARN, ERROR +} +``` + +### 2.2 元注解说明 +- **@Target**:指定注解可以用在哪些地方 + - ElementType.METHOD:方法 + - ElementType.TYPE:类 + - ElementType.FIELD:字段 + - ElementType.PARAMETER:参数 + +- **@Retention**:指定注解的生命周期 + - RetentionPolicy.SOURCE:源码期 + - RetentionPolicy.CLASS:编译期 + - RetentionPolicy.RUNTIME:运行期(AOP必须使用) + +- **@Documented**:注解信息包含在JavaDoc中 +- **@Inherited**:子类可以继承父类的注解 + +## 三、完整示例:日志注解 + +### 3.1 创建日志注解 +```java +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface SysLog { + // 操作描述 + String value() default ""; + + // 操作类型 + OperationType type() default OperationType.OTHER; + + // 是否保存请求参数 + boolean saveParams() default true; + + // 是否保存响应结果 + boolean saveResult() default true; +} + +// 操作类型枚举 +public enum OperationType { + INSERT("新增"), + UPDATE("修改"), + DELETE("删除"), + SELECT("查询"), + EXPORT("导出"), + IMPORT("导入"), + OTHER("其他"); + + private final String desc; + + OperationType(String desc) { + this.desc = desc; + } + + public String getDesc() { + return desc; + } +} +``` + +### 3.2 创建日志切面 +```java +@Aspect +@Component +@Slf4j +public class SysLogAspect { + + @Autowired + private SysLogService sysLogService; + + @Autowired + private HttpServletRequest request; + + // 使用@annotation匹配带有@SysLog注解的方法 + @Pointcut("@annotation(com.example.annotation.SysLog)") + public void logPointCut() {} + + @Around("logPointCut()") + public Object around(ProceedingJoinPoint point) throws Throwable { + long beginTime = System.currentTimeMillis(); + + // 获取注解信息 + SysLog sysLog = getAnnotation(point); + + // 创建日志对象 + SysLogEntity logEntity = new SysLogEntity(); + logEntity.setOperation(sysLog.value()); + logEntity.setType(sysLog.type().getDesc()); + logEntity.setCreateTime(new Date()); + + // 获取请求信息 + logEntity.setIp(getIpAddress(request)); + logEntity.setUrl(request.getRequestURL().toString()); + logEntity.setMethod(request.getMethod()); + + // 获取用户信息 + String username = getCurrentUsername(); + logEntity.setUsername(username); + + // 获取方法信息 + String className = point.getTarget().getClass().getName(); + String methodName = point.getSignature().getName(); + logEntity.setClassName(className); + logEntity.setMethodName(methodName); + + // 保存请求参数 + if (sysLog.saveParams()) { + Object[] args = point.getArgs(); + String params = JSON.toJSONString(args); + logEntity.setParams(params); + } + + try { + // 执行方法 + Object result = point.proceed(); + + // 保存响应结果 + if (sysLog.saveResult() && result != null) { + String resultStr = JSON.toJSONString(result); + logEntity.setResult(resultStr); + } + + // 设置执行时间 + long time = System.currentTimeMillis() - beginTime; + logEntity.setTime(time); + logEntity.setStatus(1); // 成功 + + return result; + } catch (Throwable e) { + // 记录异常信息 + logEntity.setStatus(0); // 失败 + logEntity.setError(e.getMessage()); + logEntity.setTime(System.currentTimeMillis() - beginTime); + throw e; + } finally { + // 异步保存日志 + CompletableFuture.runAsync(() -> sysLogService.save(logEntity)); + } + } + + // 获取注解对象 + private SysLog getAnnotation(ProceedingJoinPoint point) { + MethodSignature signature = (MethodSignature) point.getSignature(); + Method method = signature.getMethod(); + return method.getAnnotation(SysLog.class); + } + + // 获取IP地址 + private String getIpAddress(HttpServletRequest request) { + String ip = request.getHeader("X-Forwarded-For"); + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + return ip; + } + + // 获取当前用户名(示例) + private String getCurrentUsername() { + // 从Security Context或Session中获取 + return "admin"; + } +} +``` + +### 3.3 使用示例 +```java +@RestController +@RequestMapping("/user") +public class UserController { + + @Autowired + private UserService userService; + + @PostMapping + @SysLog(value = "新增用户", type = OperationType.INSERT) + public Result createUser(@RequestBody User user) { + User savedUser = userService.save(user); + return Result.success(savedUser); + } + + @PutMapping("/{id}") + @SysLog(value = "修改用户信息", type = OperationType.UPDATE) + public Result updateUser(@PathVariable Long id, @RequestBody User user) { + user.setId(id); + User updatedUser = userService.update(user); + return Result.success(updatedUser); + } + + @DeleteMapping("/{id}") + @SysLog(value = "删除用户", type = OperationType.DELETE, saveResult = false) + public Result deleteUser(@PathVariable Long id) { + userService.delete(id); + return Result.success(); + } + + @GetMapping("/{id}") + @SysLog(value = "查询用户详情", type = OperationType.SELECT, saveParams = false) + public Result getUser(@PathVariable Long id) { + User user = userService.findById(id); + return Result.success(user); + } +} +``` + +## 四、实战案例集合 + +### 4.1 权限控制注解 +```java +// 权限注解定义 +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RequiresPermission { + // 权限标识 + String[] value(); + + // 逻辑关系:AND 或 OR + Logical logical() default Logical.AND; + + enum Logical { + AND, OR + } +} + +// 角色注解 +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RequiresRole { + String[] value(); + Logical logical() default Logical.AND; +} + +// 权限切面 +@Aspect +@Component +public class PermissionAspect { + + @Autowired + private AuthService authService; + + @Before("@annotation(permission)") + public void checkPermission(RequiresPermission permission) { + String[] requiredPerms = permission.value(); + Logical logical = permission.logical(); + + Set userPerms = authService.getCurrentUserPermissions(); + + boolean hasPermission = false; + if (logical == Logical.AND) { + // 需要所有权限 + hasPermission = userPerms.containsAll(Arrays.asList(requiredPerms)); + } else { + // 只需要其中一个权限 + hasPermission = Arrays.stream(requiredPerms) + .anyMatch(userPerms::contains); + } + + if (!hasPermission) { + throw new UnauthorizedException("权限不足:需要权限 " + Arrays.toString(requiredPerms)); + } + } + + @Before("@annotation(role)") + public void checkRole(RequiresRole role) { + String[] requiredRoles = role.value(); + Set userRoles = authService.getCurrentUserRoles(); + + boolean hasRole = Arrays.stream(requiredRoles) + .anyMatch(userRoles::contains); + + if (!hasRole) { + throw new UnauthorizedException("角色不足:需要角色 " + Arrays.toString(requiredRoles)); + } + } +} + +// 使用示例 +@RestController +@RequestMapping("/admin") +public class AdminController { + + @GetMapping("/users") + @RequiresPermission("user:view") + public List listUsers() { + // 查看用户列表 + } + + @PostMapping("/users") + @RequiresPermission({"user:create", "user:edit"}) + public User createUser(@RequestBody User user) { + // 创建用户(需要同时有创建和编辑权限) + } + + @DeleteMapping("/users/{id}") + @RequiresRole("ADMIN") + @RequiresPermission("user:delete") + public void deleteUser(@PathVariable Long id) { + // 删除用户(需要管理员角色和删除权限) + } +} +``` + +### 4.2 缓存注解 +```java +// 缓存注解 +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Cache { + // 缓存key前缀 + String prefix() default ""; + + // 缓存key(支持SpEL表达式) + String key() default ""; + + // 过期时间(秒) + int expire() default 300; + + // 条件(SpEL表达式,返回true时才缓存) + String condition() default ""; +} + +// 清除缓存注解 +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface CacheEvict { + String prefix() default ""; + String key() default ""; + boolean allEntries() default false; +} + +// 缓存切面 +@Aspect +@Component +public class CacheAspect { + + @Autowired + private RedisTemplate redisTemplate; + + private final SpelExpressionParser parser = new SpelExpressionParser(); + + @Around("@annotation(cache)") + public Object cache(ProceedingJoinPoint point, Cache cache) throws Throwable { + // 构建缓存key + String key = buildKey(point, cache.prefix(), cache.key()); + + // 检查条件 + if (!checkCondition(point, cache.condition())) { + return point.proceed(); + } + + // 尝试从缓存获取 + Object cached = redisTemplate.opsForValue().get(key); + if (cached != null) { + log.debug("缓存命中: {}", key); + return cached; + } + + // 执行方法 + Object result = point.proceed(); + + // 存入缓存 + if (result != null) { + redisTemplate.opsForValue().set(key, result, cache.expire(), TimeUnit.SECONDS); + log.debug("缓存存储: {}", key); + } + + return result; + } + + @After("@annotation(evict)") + public void evict(JoinPoint point, CacheEvict evict) { + if (evict.allEntries()) { + // 清除所有缓存 + Set keys = redisTemplate.keys(evict.prefix() + "*"); + if (keys != null && !keys.isEmpty()) { + redisTemplate.delete(keys); + } + } else { + // 清除指定缓存 + String key = buildKey(point, evict.prefix(), evict.key()); + redisTemplate.delete(key); + } + } + + // 构建缓存key + private String buildKey(JoinPoint point, String prefix, String key) { + if (StringUtils.isEmpty(key)) { + // 默认使用方法签名作为key + return prefix + ":" + point.getSignature().toLongString(); + } + + // 解析SpEL表达式 + EvaluationContext context = new StandardEvaluationContext(); + Method method = ((MethodSignature) point.getSignature()).getMethod(); + String[] paramNames = new LocalVariableTableParameterNameDiscoverer() + .getParameterNames(method); + Object[] args = point.getArgs(); + + for (int i = 0; i < paramNames.length; i++) { + context.setVariable(paramNames[i], args[i]); + } + + Expression expression = parser.parseExpression(key); + return prefix + ":" + expression.getValue(context, String.class); + } + + // 检查条件 + private boolean checkCondition(JoinPoint point, String condition) { + if (StringUtils.isEmpty(condition)) { + return true; + } + + // 解析条件表达式 + EvaluationContext context = new StandardEvaluationContext(); + // ... 设置变量 + + Expression expression = parser.parseExpression(condition); + return Boolean.TRUE.equals(expression.getValue(context, Boolean.class)); + } +} + +// 使用示例 +@Service +public class ProductService { + + @Cache(prefix = "product", key = "#id", expire = 600) + public Product findById(Long id) { + // 从数据库查询 + return productRepository.findById(id); + } + + @Cache(prefix = "product", key = "#category + ':' + #page", expire = 300) + public List findByCategory(String category, int page) { + // 分页查询 + return productRepository.findByCategory(category, page); + } + + @CacheEvict(prefix = "product", key = "#product.id") + public void updateProduct(Product product) { + // 更新产品,清除缓存 + productRepository.save(product); + } + + @CacheEvict(prefix = "product", allEntries = true) + public void clearAllCache() { + // 清除所有产品缓存 + } +} +``` + +### 4.3 限流注解 +```java +// 限流注解 +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RateLimit { + // 限流key + String key() default ""; + + // 时间窗口(秒) + int period() default 60; + + // 限流次数 + int count() default 100; + + // 限流类型 + LimitType limitType() default LimitType.IP; + + enum LimitType { + IP, // 根据IP限流 + USER, // 根据用户限流 + GLOBAL, // 全局限流 + CUSTOM // 自定义key + } +} + +// 限流切面 +@Aspect +@Component +@Slf4j +public class RateLimitAspect { + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private HttpServletRequest request; + + @Before("@annotation(rateLimit)") + public void limit(JoinPoint point, RateLimit rateLimit) { + String key = buildLimitKey(point, rateLimit); + + // 使用Redis实现滑动窗口限流 + long currentTime = System.currentTimeMillis(); + long windowStart = currentTime - rateLimit.period() * 1000; + + // 移除窗口外的记录 + redisTemplate.opsForZSet().removeRangeByScore(key, 0, windowStart); + + // 获取窗口内的请求数 + Long count = redisTemplate.opsForZSet().count(key, windowStart, currentTime); + + if (count != null && count >= rateLimit.count()) { + throw new RateLimitException("访问太频繁,请稍后再试"); + } + + // 添加当前请求 + redisTemplate.opsForZSet().add(key, UUID.randomUUID().toString(), currentTime); + + // 设置过期时间 + redisTemplate.expire(key, rateLimit.period(), TimeUnit.SECONDS); + } + + private String buildLimitKey(JoinPoint point, RateLimit rateLimit) { + StringBuilder key = new StringBuilder("rate_limit:"); + + switch (rateLimit.limitType()) { + case IP: + key.append(getIpAddress()).append(":"); + break; + case USER: + key.append(getCurrentUserId()).append(":"); + break; + case GLOBAL: + key.append("global:"); + break; + case CUSTOM: + key.append(rateLimit.key()).append(":"); + break; + } + + // 添加方法签名 + key.append(point.getSignature().toLongString()); + + return key.toString(); + } + + private String getIpAddress() { + // 获取真实IP + String ip = request.getHeader("X-Real-IP"); + if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("X-Forwarded-For"); + } + if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + return ip; + } + + private String getCurrentUserId() { + // 从认证信息中获取用户ID + return "user123"; + } +} + +// 使用示例 +@RestController +@RequestMapping("/api") +public class ApiController { + + @GetMapping("/search") + @RateLimit(period = 60, count = 10, limitType = LimitType.IP) + public Result search(@RequestParam String keyword) { + // IP限流:每个IP每分钟最多10次 + } + + @PostMapping("/order") + @RateLimit(period = 3600, count = 100, limitType = LimitType.USER) + public Result createOrder(@RequestBody Order order) { + // 用户限流:每个用户每小时最多100单 + } + + @GetMapping("/hot-deal") + @RateLimit(period = 1, count = 1000, limitType = LimitType.GLOBAL) + public Result getHotDeal() { + // 全局限流:每秒最多1000次请求 + } +} +``` + +### 4.4 重试注解 +```java +// 重试注解 +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Retry { + // 重试次数 + int maxAttempts() default 3; + + // 重试间隔(毫秒) + long delay() default 1000; + + // 指数退避 + boolean exponentialBackoff() default false; + + // 需要重试的异常类型 + Class[] retryFor() default {Exception.class}; + + // 不需要重试的异常类型 + Class[] noRetryFor() default {}; +} + +// 重试切面 +@Aspect +@Component +@Slf4j +public class RetryAspect { + + @Around("@annotation(retry)") + public Object retry(ProceedingJoinPoint point, Retry retry) throws Throwable { + int attempts = 0; + long delay = retry.delay(); + + while (attempts < retry.maxAttempts()) { + attempts++; + + try { + return point.proceed(); + } catch (Throwable e) { + // 检查是否需要重试 + if (!shouldRetry(e, retry)) { + throw e; + } + + if (attempts >= retry.maxAttempts()) { + log.error("重试{}次后仍然失败", retry.maxAttempts()); + throw e; + } + + log.warn("方法执行失败,{}ms后进行第{}次重试", delay, attempts); + + Thread.sleep(delay); + + // 指数退避 + if (retry.exponentialBackoff()) { + delay = delay * 2; + } + } + } + + throw new IllegalStateException("不应该到达这里"); + } + + private boolean shouldRetry(Throwable e, Retry retry) { + // 检查不需要重试的异常 + for (Class noRetryClass : retry.noRetryFor()) { + if (noRetryClass.isAssignableFrom(e.getClass())) { + return false; + } + } + + // 检查需要重试的异常 + for (Class retryClass : retry.retryFor()) { + if (retryClass.isAssignableFrom(e.getClass())) { + return true; + } + } + + return false; + } +} + +// 使用示例 +@Service +public class PaymentService { + + @Retry(maxAttempts = 3, delay = 2000, retryFor = {TimeoutException.class, ConnectException.class}) + public PaymentResult processPayment(PaymentRequest request) { + // 支付处理,可能超时 + return paymentGateway.process(request); + } + + @Retry(maxAttempts = 5, delay = 1000, exponentialBackoff = true) + public void sendNotification(String message) { + // 发送通知,使用指数退避策略 + notificationService.send(message); + } +} +``` + +### 4.5 参数校验注解 +```java +// 参数校验注解 +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ValidateParams { + // 是否校验请求参数 + boolean validateRequest() default true; + + // 是否校验响应结果 + boolean validateResponse() default false; + + // 校验组 + Class[] groups() default {}; +} + +// 校验切面 +@Aspect +@Component +@Slf4j +public class ValidationAspect { + + @Autowired + private Validator validator; + + @Around("@annotation(validateParams)") + public Object validate(ProceedingJoinPoint point, ValidateParams validateParams) throws Throwable { + // 校验请求参数 + if (validateParams.validateRequest()) { + Object[] args = point.getArgs(); + for (Object arg : args) { + if (arg != null && !isBasicType(arg)) { + Set> violations = validator.validate(arg, validateParams.groups()); + if (!violations.isEmpty()) { + throw new ValidationException(buildErrorMessage(violations)); + } + } + } + } + + // 执行方法 + Object result = point.proceed(); + + // 校验响应结果 + if (validateParams.validateResponse() && result != null) { + Set> violations = validator.validate(result, validateParams.groups()); + if (!violations.isEmpty()) { + log.error("响应数据校验失败: {}", buildErrorMessage(violations)); + } + } + + return result; + } + + private boolean isBasicType(Object obj) { + return obj instanceof String || obj instanceof Number || + obj instanceof Boolean || obj instanceof Character; + } + + private String buildErrorMessage(Set> violations) { + return violations.stream() + .map(v -> v.getPropertyPath() + " " + v.getMessage()) + .collect(Collectors.joining(", ")); + } +} + +// 使用示例 +@RestController +@RequestMapping("/user") +public class UserController { + + @PostMapping + @ValidateParams(groups = {Create.class}) + public Result createUser(@RequestBody User user) { + // 自动校验User对象的Create组约束 + return Result.success(userService.save(user)); + } + + @PutMapping("/{id}") + @ValidateParams(groups = {Update.class}, validateResponse = true) + public Result updateUser(@PathVariable Long id, @RequestBody User user) { + // 校验请求和响应 + return Result.success(userService.update(user)); + } +} + +// 实体类 +@Data +public class User { + @Null(groups = Create.class) + @NotNull(groups = Update.class) + private Long id; + + @NotBlank(message = "用户名不能为空") + @Size(min = 3, max = 20, message = "用户名长度必须在3-20之间") + private String username; + + @Email(message = "邮箱格式不正确") + private String email; + + @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") + private String phone; +} +``` + +## 五、高级技巧 + +### 5.1 组合注解 +```java +// 组合多个注解功能 +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@SysLog +@RequiresPermission +@RateLimit +public @interface SecureApi { + // 日志描述 + String value() default ""; + + // 需要的权限 + String[] permissions() default {}; + + // 限流配置 + int rateLimit() default 100; + int ratePeriod() default 60; +} + +// 使用组合注解 +@PostMapping("/sensitive-operation") +@SecureApi( + value = "执行敏感操作", + permissions = {"admin:sensitive:operate"}, + rateLimit = 10, + ratePeriod = 60 +) +public Result sensitiveOperation(@RequestBody Request request) { + // 一个注解实现:日志记录 + 权限校验 + 限流 +} +``` + +### 5.2 注解继承 +```java +// 基础审计注解 +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Auditable { + boolean enabled() default true; +} + +// 继承审计功能的业务注解 +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Auditable +public @interface BankingOperation { + String operationType(); + RiskLevel riskLevel() default RiskLevel.LOW; + + enum RiskLevel { + LOW, MEDIUM, HIGH, CRITICAL + } +} + +// 统一处理所有审计注解 +@Aspect +@Component +public class AuditAspect { + + @Around("@annotation(auditable) || @annotation(banking)") + public Object audit(ProceedingJoinPoint point, Auditable auditable, BankingOperation banking) throws Throwable { + // 处理审计逻辑 + } +} +``` + +### 5.3 动态注解处理 +```java +@Aspect +@Component +public class DynamicAnnotationAspect { + + @Around("@annotation(anno)") + public Object handleDynamically(ProceedingJoinPoint point, Annotation anno) throws Throwable { + // 动态获取注解类型 + Class annotationType = anno.annotationType(); + + // 根据不同注解类型执行不同逻辑 + if (annotationType == Cache.class) { + return handleCache(point, (Cache) anno); + } else if (annotationType == Log.class) { + return handleLog(point, (Log) anno); + } + + return point.proceed(); + } +} +``` + +## 六、最佳实践总结 + +### 6.1 设计原则 +1. **单一职责**:每个注解只负责一个功能 +2. **语义清晰**:注解名称要表达明确意图 +3. **参数简洁**:只提供必要的配置参数 +4. **默认值合理**:为参数提供合理的默认值 + +### 6.2 性能优化 +1. **异步处理**:日志等操作使用异步方式 +2. **缓存结果**:避免重复计算 +3. **条件判断**:提前判断是否需要执行切面逻辑 + +### 6.3 错误处理 +1. **异常传播**:不要吞掉业务异常 +2. **降级处理**:切面异常不应影响业务执行 +3. **日志记录**:记录关键错误信息 + +### 6.4 测试建议 +```java +@SpringBootTest +public class AopTest { + + @MockBean + private SysLogService logService; + + @Autowired + private UserService userService; + + @Test + public void testLogAspect() { + // 测试日志切面 + User user = new User(); + userService.createUser(user); + + // 验证日志服务被调用 + verify(logService, times(1)).save(any(SysLogEntity.class)); + } +} +``` + +自定义注解配合AOP是Spring中非常强大和优雅的编程方式,能够让代码更加模块化、可维护和可扩展。 \ No newline at end of file