AOP幂等组件

一、整体目的

解决 接口被重复调用 的问题。比如用户连续点 3 次"提交订单"按钮、网络重试、定时任务重跑------业务方法只应该真正执行一次,其余拦截。

实现手段: AOP 拦截 + Redis 分布式锁 + 幂等成功标志位 。

注意它用了 两个 Redis key :

  • LOCK:{key} :短时锁(10s),防止并发抢占

  • REPEAT_LIMIT:{key} :幂等标志(durationTime),防止窗口期内重复提交

二、7 步执行流详解

java 复制代码
String lockName = generateLockName(joinPoint, repeatLimit);

SpEL 解析注解里的 keys ,结合方法参数动态生成。比如 keys = "#userId + #showId" ,最终 lockName 是 order:create:1001:2002 。

Step 2:第一次快速判定(无锁)

java 复制代码
String flagObject = redisTemplate.opsForValue().get(repeatFlagName);
if (SUCCESS_FLAG.equals(flagObject)) { throw ... }

先无锁查一次 ,如果发现已经有 SUCCESS 标志,直接拒绝。

✅ 优点 :高频重复请求时不用每次都抢锁,省一次 Redis 写操作。

Step 3:抢分布式锁

Boolean locked = redisTemplate.opsForValue().setIfAbsent("LOCK:" + lockName, "1",

10, TimeUnit.SECONDS);

用 SETNX + 过期时间原子加锁。

Step 4:双重检查(DCL)

flagObject = redisTemplate.opsForValue().get(repeatFlagName);

if (SUCCESS_FLAG.equals(flagObject)) { throw ... }

拿到锁后 再查一次 SUCCESS 标志。

✅ 为什么需要? 防止"步骤 2 查询时还没有,步骤 3 抢锁前别人刚好执行完设置了标志"这个时间窗的漏网之鱼。 这是经典的 DCL(Double-Checked Locking)思想。

Step 5:执行业务

Object result = joinPoint.proceed();

Step 6:业务成功后打标

if (durationTime > 0) {

redisTemplate.opsForValue().set(repeatFlagName, SUCCESS_FLAG, durationTime,

TimeUnit.SECONDS);

}

注意 :成功标记只有在业务正常返回后才设置。如果业务抛异常,不会打标,下次还能重试。 durationTime 是注解里配置的"幂等窗口时长",比如 5 分钟。

Step 7:释放锁

redisTemplate.delete("LOCK:" + lockName);

finally 块保证一定释放。

三、Spring AOP 的 代理机制 ,分三步讲清楚。

一、首先:需要两样东西配合

  1. 自定义注解 (标记作用)
java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatExecuteLimit {
    String name();
    String[] keys();
    int durationTime() default 5;
    String message() default "请勿重复提交";
}

关键是 @Retention(RetentionPolicy.RUNTIME) ------必须保留到运行时,否则 Spring 反射拿不到。

  • @Target(ElementType.METHOD)
    表示该注解只能打在方法上。也就是说,你只能把它写在普通的 Java 方法(通常是 Controller 接口方法)上,不能写在类或字段上。
  • @Retention(RetentionPolicy.RUNTIME)
    表示该注解的生命周期会保留到运行期。这意味着在程序运行时,我们可以通过反射(或 Spring AOP)读取到这个注解的信息,从而执行相关的拦截逻辑。
  1. 切面类
java 复制代码
@Aspect          // ← 告诉 Spring 这是一个切面
@Component       // ← 交给 Spring 容器管理
public class RepeatExecuteLimitAspect {

    @Around("@annotation(repeatLimit)")   // ← 切入点表达式
    public Object around(ProceedingJoinPoint joinPoint, RepeatExecuteLimit repeatLimit) {
        // 切面逻辑
    }
}

二、Spring 是怎么"发现"并"织入"的

这一步是 最核心的 ,面试被问"为什么加了注解就生效"就答这个:

启动阶段

Step 1:开启 AOP 自动代理

  • Spring Boot 默认通过 AopAutoConfiguration 自动开启

  • 实际生效的是 AnnotationAwareAspectJAutoProxyCreator ,它是一个 BeanPostProcessor

Step 2:扫描切面

  • 容器启动时,这个后置处理器会扫描 所有 Bean

  • 找到带 @Aspect 注解的类(即你的 RepeatExecuteLimitAspect )

  • 解析里面所有的 advice 方法( @Around 、 @Before 等)

  • 拿到切入点表达式 "@annotation(repeatLimit)" ,意思是"匹配所有带 @RepeatExecuteLimit 注解的方法"

Step 3:创建代理对象

  • 遍历每个 Bean,检查它的方法是否能匹配到任何切面的切入点

  • 匹配上 → 创建代理对象 (不是匹配上才创建,是 匹配上才代理 )

  • 代理方式默认是 CGLIB(继承方式),也可以配成 JDK 动态代理(接口方式)

  • 原始 Bean 放进 targetSource ,代理对象替换原始 Bean 放进容器

调用阶段

Step 4:方法调用时拦截

调用 orderService.create(userId, showId)

实际拿到的是 Spring 创建的 CGLIB 代理对象

代理对象的同名方法被拦截,进入 CGLIB 拦截器链

拦截器链找到匹配的 Advisor(你的 @Aspect)

调用 @Around 增强方法(RepeatExecuteLimitAspect.around)

方法签名里的 repeatLimit 参数被 Spring 反射注入

执行你的幂等逻辑

joinPoint.proceed() 真正调用原始 orderService.create()

返回结果

这个问题分三块讲: 注解定义、切面声明、Spring 代理机制 。

注解 用 @Target(METHOD) + @Retention(RUNTIME) 定义,标记在需要幂等的方法上。

切面类 加 @Aspect 和 @Component ,用 @Around("@annotation(repeatLimit)") 写切入点。这个表达式意思是"拦截所有带 @RepeatExecuteLimit 注解的方法",方法参数 RepeatExecuteLimit repeatLimit 是 Spring 通过反射把注解实例注入进来的。

@Around 是环绕通知,可以控制目标方法是否执行。 "@annotation(repeatLimit)" 是切入点表达式, @annotation 是 AspectJ 的指示符,表示"匹配所有带指定注解的方法", repeatLimit 是形参名,Spring 会通过反射把方法上的注解实例注入到这个参数里。这样 新方法只要加 @RepeatExecuteLimit 注解就能自动被拦截,符合开闭原则 。

为什么加了注解就生效? 关键在 Spring 的 AnnotationAwareAspectJAutoProxyCreator ,它是个 BeanPostProcessor 。容器启动时,它会扫描所有 @Aspect 类,解析切入点表达式, 对匹配的方法所在 Bean 创建 CGLIB 代理 。之后每次调用方法,都会先经过代理,进入切面逻辑,调用 joinPoint.proceed() 时才真正执行原方法。