2025/12/31 10:27:47
网站建设
项目流程
网站建设规划与管理 试卷,做网站必须会编程吗,室内展厅设计公司,怎样用模板建一个网站在白嫖之前#xff0c;希望你会内疚#xff0c;最起码点个赞收藏再自取吧#xff0c;源码在最后#xff0c;自取#xff1b;
在白嫖之前#xff0c;希望你会内疚#xff0c;最起码点个赞收藏再自取吧#xff0c;源码在最后#xff0c;自取#xff1b;
在白嫖之前希望你会内疚最起码点个赞收藏再自取吧源码在最后自取在白嫖之前希望你会内疚最起码点个赞收藏再自取吧源码在最后自取在白嫖之前希望你会内疚最起码点个赞收藏再自取吧源码在最后自取基于自定义注解AOP实现BladeX接口防抖功能详解一、功能概述本方案基于Spring AOP Redis原子操作实现BladeX框架下的接口防抖限流功能核心解决接口重复调用/重复提交问题支持分布式环境、SpEL动态生成Key、自定义过期时间/提示语适配BladeX原生返回结果R开箱即用且具备高鲁棒性。二、整体架构与设计思路1. 架构分层层级组件职责注解层Debounce定义防抖规则Key前缀、SpEL表达式、过期时间、提示语等切面层BladeDebounceAspect拦截标注Debounce的方法解析注解参数、生成Redis Key、调用防抖工具类工具层BladeDebounceUtil封装Redis原子操作实现防抖锁的获取/释放处理序列化、异常降级等核心逻辑业务层业务接口如getNameAndCardNum标注Debounce注解无感接入防抖功能2. 核心设计思路无侵入式接入通过自定义注解AOP实现业务代码仅需添加注解无需修改核心逻辑分布式兼容基于Redis原子操作setIfAbsent实现分布式锁适配BladeX集群部署动态Key生成支持SpEL表达式解析方法参数实现“接口用户/参数”级别的精准防抖鲁棒性保障Redis异常时降级放行、永久Key自动清理、序列化器全局配置避免影响主业务原生适配返回BladeX框架标准R对象兼容全局异常处理、统一返回格式。3. 执行流程客户端调用接口 → 2. AOP切面拦截方法 → 3. 解析Debounce注解参数 → 4. SpEL解析生成Redis Key → 5. 调用工具类尝试获取防抖锁 → 6. 锁获取成功则执行原业务方法并返回结果锁获取失败则返回限流提示R.fail三、代码模块逐行解析1. 自定义注解DebounceTarget({ElementType.METHOD})// 仅作用于方法Retention(RetentionPolicy.RUNTIME)// 运行时生效支持AOP解析Documented// 生成文档publicinterfaceDebounce{// Redis Key前缀用于区分不同接口Stringprefix()defaultblade:debounce:;// SpEL表达式用于动态拼接Key如取方法参数、参数属性Stringkey()default;// 防抖过期时间默认5秒longexpireTime()default5;// 时间单位默认秒TimeUnittimeUnit()defaultTimeUnit.SECONDS;// 重复操作提示语返回给前端Stringmessage()default操作过于频繁请稍后再试;// 是否使用分布式锁集群环境默认开启booleanuseDistributedLock()defaulttrue;// 分布式锁等待时间默认0立即返回longlockWaitTime()default0;}关键解析Target(ElementType.METHOD)限定注解仅作用于方法符合接口防抖的场景key()支持SpEL核心灵活点可通过#miniUserId、#root.args[0].id等表达式精准定位到“用户接口”维度预留useDistributedLock为后续“本地锁/分布式锁”切换预留扩展点。2. 防抖工具类BladeDebounceUtil核心Slf4jComponentpublicclassBladeDebounceUtil{// 可配置常量便于维护privatestaticfinalStringDEFAULT_DEBOUNCE_VALUE1;privatestaticfinallongDEFAULT_EXPIRE_SECONDS5;privatestaticfinalbooleanLOG_ENABLEtrue;ResourceprivateBladeRedisbladeRedis;privateRedisTemplateString,ObjectredisTemplate;privateValueOperationsString,ObjectvalueOps;// 初始化方法序列化器全局配置仅执行1次提升性能PostConstructpublicvoidinit(){if(bladeRedis!null){this.redisTemplatebladeRedis.getRedisTemplate();if(this.redisTemplate!null){// 强制String序列化避免Redis参数解析异常StringRedisSerializerstringSerializernewStringRedisSerializer();this.redisTemplate.setKeySerializer(stringSerializer);this.redisTemplate.setValueSerializer(stringSerializer);this.redisTemplate.setHashKeySerializer(stringSerializer);this.redisTemplate.setHashValueSerializer(stringSerializer);this.valueOpsthis.redisTemplate.opsForValue();log.info(【防抖工具类】初始化完成Redis序列化器配置成功);}}else{log.warn(【防抖工具类】BladeRedis注入失败防抖功能降级为直接放行);}}// 重载方法简化调用支持默认过期时间publicbooleantryAcquire(Stringkey){returntryAcquire(key,DEFAULT_EXPIRE_SECONDS,TimeUnit.SECONDS);}// 重载方法支持自定义value便于区分不同业务锁publicbooleantryAcquire(Stringkey,Stringvalue,longexpireTime,TimeUnittimeUnit){returninnerTryAcquire(key,value,expireTime,timeUnit);}// 核心方法对外暴露的标准调用入口publicbooleantryAcquire(Stringkey,longexpireTime,TimeUnittimeUnit){returninnerTryAcquire(key,DEFAULT_DEBOUNCE_VALUE,expireTime,timeUnit);}// 内部核心逻辑抽离通用逻辑便于维护privatebooleaninnerTryAcquire(Stringkey,Stringvalue,longexpireTime,TimeUnittimeUnit){// 1. 前置校验参数合法性Redis降级try{Assert.hasText(key,防抖Key不能为空);Assert.hasText(value,防抖Value不能为空);}catch(IllegalArgumentExceptione){log.error(【防抖工具类】参数非法{},e.getMessage());returnfalse;}// Redis未初始化时降级放行避免影响主业务if(redisTemplatenull||valueOpsnull){if(LOG_ENABLE){log.warn(【防抖工具类】Redis未初始化防抖功能降级直接放行Key{},key);}returntrue;}// 2. 过期时间合法性校验longexpireSecondstimeUnit.toSeconds(expireTime);if(expireSeconds0){if(LOG_ENABLE){log.error(【防抖工具类】过期时间非法Key{}时间{}秒,key,expireSeconds);}returnfalse;}booleanlockSuccessfalse;Longttl-1L;try{// 3. 原子操作判断Key不存在则设置值过期时间核心防抖逻辑BooleansetResultvalueOps.setIfAbsent(key,value,expireTime,timeUnit);lockSuccessBoolean.TRUE.equals(setResult);// 4. 双重保障强制设置过期时间防止setIfAbsent参数解析失败if(lockSuccess){booleanexpireSuccessBoolean.TRUE.equals(redisTemplate.expire(key,expireTime,timeUnit));if(LOG_ENABLE){log.info(【防抖工具类】获取锁成功Key{}过期时间{}秒过期设置{},key,expireSeconds,expireSuccess?成功:失败);}}else{// 5. 异常处理检测到永久Keyttl-1自动清理并重试获取锁ttlredisTemplate.getExpire(key,TimeUnit.SECONDS);if(ttl-1){if(LOG_ENABLE){log.error(【防抖工具类】检测到永久有效Key强制删除{},key);}redisTemplate.delete(key);// 重新尝试获取锁setResultvalueOps.setIfAbsent(key,value,expireTime,timeUnit);lockSuccessBoolean.TRUE.equals(setResult);// 重新获取ttl保证日志准确性ttllockSuccess?Long.valueOf(expireSeconds):redisTemplate.getExpire(key,TimeUnit.SECONDS);if(lockSuccessLOG_ENABLE){log.info(【防抖工具类】清理永久Key后获取锁成功Key{},key);}}}}catch(Exceptione){// Redis异常时降级放行核心业务优先log.error(【防抖工具类】获取锁异常Key{}异常信息{},key,e.getMessage(),e);returntrue;}// 6. 日志输出仅在锁获取失败且日志开启时打印减少日志量if(!lockSuccessLOG_ENABLE){StringttlDescswitch(ttl.intValue()){case-1-永久有效;case-2-Key不存在;default-ttl秒;};log.warn(【防抖工具类】获取锁失败Key{}剩余过期时间{},key,ttlDesc);}returnlockSuccess;}// 手动释放锁增加异常捕获避免释放失败影响主业务publicvoidreleaseLock(Stringkey){if(bladeRedisnull||keynull||key.isEmpty()){return;}try{bladeRedis.del(key);if(LOG_ENABLE){log.info(【防抖工具类】释放锁成功Key{},key);}}catch(Exceptione){log.error(【防抖工具类】释放锁失败Key{}异常信息{},key,e.getMessage(),e);}}}关键解析PostConstruct初始化序列化器仅配置1次避免每次调用重复创建对象提升性能setIfAbsent原子操作等价于Redis命令SET key value NX EX expire保证“判断-设置-过期”原子性避免并发问题降级逻辑Redis未初始化/异常时直接放行核心业务不受影响永久Key清理检测到ttl-1永久Key时自动删除并重试解决历史残留Key导致的误限流重载方法适配不同调用场景提升易用性。3. AOP切面BladeDebounceAspectSlf4jAspectComponentpublicclassBladeDebounceAspect{ResourceprivateBladeDebounceUtilbladeDebounceUtil;// SpEL解析器BladeX内部同款保证解析规则一致privatefinalExpressionParserspelParsernewSpelExpressionParser();// 参数名解析器解析方法参数名支持SpEL引用参数privatefinalParameterNameDiscovererparameterNameDiscoverernewDefaultParameterNameDiscoverer();// 切点匹配所有标注Debounce的方法Pointcut(annotation(org.springblade.business.aspect.annotation.Debounce))publicvoiddebouncePointcut(){}// 环绕通知核心拦截逻辑Around(debouncePointcut())publicObjectaround(ProceedingJoinPointjoinPoint)throwsThrowable{// 目标方法标识类名.方法名便于日志定位StringtargetMethodjoinPoint.getTarget().getClass().getSimpleName().joinPoint.getSignature().getName();log.info(【自定义防抖切面】开始执行目标方法{},targetMethod);try{// 1. 获取方法和注解信息MethodSignaturesignature(MethodSignature)joinPoint.getSignature();Methodmethodsignature.getMethod();Debouncedebouncemethod.getAnnotation(Debounce.class);if(debouncenull){log.warn(【自定义防抖切面】目标方法{}未解析到Debounce注解直接放行,targetMethod);returnjoinPoint.proceed();}log.info(【自定义防抖切面】目标方法{}解析防抖注解成功配置前缀{}过期时间{}{}提示语{},targetMethod,debounce.prefix(),debounce.expireTime(),debounce.timeUnit().name(),debounce.message());// 2. 生成Redis Key前缀SpEL解析的动态KeyStringredisKeygenerateRedisKey(joinPoint,debounce);log.info(【自定义防抖切面】目标方法{}生成防抖RedisKey{},targetMethod,redisKey);if(redisKey.isEmpty()){log.error(【自定义防抖切面】目标方法{}防抖Key生成失败空值拒绝执行,targetMethod);returnR.fail(防抖Key配置异常请检查Debounce注解);}// 3. 过期时间合法性校验longexpireSecondsdebounce.timeUnit().toSeconds(debounce.expireTime());if(expireSeconds0){log.error(【BladeX防抖切面】目标方法{}过期时间配置错误{}秒拒绝执行,targetMethod,expireSeconds);returnR.fail(限流配置异常请联系管理员);}// 4. 调用工具类获取防抖锁booleanacquireSuccessbladeDebounceUtil.tryAcquire(redisKey,debounce.expireTime(),debounce.timeUnit());log.info(【自定义防抖切面】目标方法{}Redis防抖校验结果{}true未重复false重复,targetMethod,acquireSuccess);// 5. 重复操作返回BladeX标准失败结果if(!acquireSuccess){log.warn(【自定义防抖切面】目标方法{}检测到重复操作RedisKey{}返回提示{},targetMethod,redisKey,debounce.message());returnR.fail(debounce.message());}// 6. 执行原业务方法log.info(【自定义防抖切面】目标方法{}防抖校验通过开始执行原方法,targetMethod);ObjectresultjoinPoint.proceed();log.info(【自定义防抖切面】目标方法{}原方法执行完成返回结果类型{},targetMethod,resultnull?null:result.getClass().getSimpleName());returnresult;}catch(Throwablee){// 异常抛出交给BladeX全局异常处理器处理log.error(【自定义防抖切面】目标方法{}执行过程中发生异常,targetMethod,e);throwe;}finally{log.info(【自定义防抖切面】目标方法{}切面执行结束,targetMethod);// 可选手动释放锁根据业务需求如操作成功后立即释放// if (redisKey ! null) {// bladeDebounceUtil.releaseLock(redisKey);// }}}// 生成Redis Key解析SpEL表达式支持动态参数privateStringgenerateRedisKey(ProceedingJoinPointjoinPoint,Debouncedebounce){MethodSignaturesignature(MethodSignature)joinPoint.getSignature();Methodmethodsignature.getMethod();Object[]argsjoinPoint.getArgs();StringtargetMethodmethod.getDeclaringClass().getSimpleName().method.getName();StringspelKeydebounce.key();// 未配置SpEL时使用默认Key前缀类名方法名if(spelKey.isEmpty()){StringdefaultKeydebounce.prefix()method.getDeclaringClass().getSimpleName()_method.getName();log.debug(【自定义防抖切面】目标方法{}未配置自定义SpEL Key使用默认Key{},targetMethod,defaultKey);returndefaultKey;}// 构建SpEL上下文解析方法参数名参数值StandardEvaluationContextcontextnewMethodBasedEvaluationContext(null,method,args,parameterNameDiscoverer);ObjectkeyObjnull;try{// 解析SpEL表达式获取动态KeykeyObjspelParser.parseExpression(spelKey).getValue(context);}catch(Exceptione){log.error(【自定义防抖切面】目标方法{}解析SpEL表达式失败表达式{},targetMethod,spelKey,e);}// 拼接最终Key前缀动态解析结果StringdynamicKeydebounce.prefix()(keyObjnull?:keyObj.toString());log.debug(【自定义防抖切面】目标方法{}SpEL表达式解析完成表达式{}解析结果{}最终Key{},targetMethod,spelKey,keyObj,dynamicKey);returndynamicKey;}}关键解析Pointcut精准匹配标注Debounce的方法无冗余拦截MethodBasedEvaluationContextBladeX兼容的SpEL上下文支持解析方法参数名如#miniUserIdProceedingJoinPoint.proceed()执行原业务方法保证AOP无侵入异常处理切面异常直接抛出交给BladeX全局异常处理器保证异常处理逻辑统一。4. 业务接口接入示例GetMapping(/getNameAndCardNum)ApiOperationSupport(order2)Operation(summary小程序-获取用户真实姓名和身份证号,description传入miniUserId)SwaggerVersion(versionSwaggerVersionEnum.V1_0)Debounce(prefixmini_user:getNameAndCardNum:,// 接口专属前缀便于区分key#miniUserId,// SpEL解析方法参数miniUserId实现“用户级”防抖expireTime20,// 防抖时间窗口20秒timeUnitTimeUnit.SECONDS,message查询过于频繁请5秒后再试// 前端提示语)publicRNameAndCardNumVOgetNameAndCardNum(RequestParamParameter(description小程序用户id)LongminiUserId){NameAndCardNumVOdetailminiUserService.getNameAndCardNum(miniUserId);returnR.data(detail);}关键解析prefix接口专属前缀避免不同接口Key冲突key #miniUserIdSpEL表达式解析方法参数miniUserId实现“同一个用户20秒内只能调用1次”而非“所有用户共享20秒窗口”注解参数完全自定义适配不同业务的防抖需求。四、重点难点总结面试高频1. 核心难点Redis原子操作与序列化问题问题RedisTemplate默认序列化器JdkSerializationRedisSerializer会将Long/String参数序列化为字节数组导致setIfAbsent的过期时间参数解析失败Key被设置为永久有效解决方案强制配置StringRedisSerializer保证参数以纯字符串传递给Redis面试回答思路实现分布式防抖的核心是Redis原子操作需注意两点① 使用setIfAbsentNXEX保证“判断-设置-过期”原子性避免并发问题② 必须统一Redis序列化器为String否则参数解析异常会导致Key永久有效反而引发更严重的限流问题。2. 重点SpEL表达式解析动态Key问题如何实现“接口参数”级别的精准防抖而非全局接口防抖解决方案通过MethodBasedEvaluationContext解析方法参数名结合SpelExpressionParser解析表达式动态拼接Key面试回答思路为了实现精细化防抖我们基于Spring SpEL表达式解析方法参数比如通过#miniUserId获取用户ID将Redis Key设置为前缀用户ID保证防抖粒度精准到“用户接口”既避免全局限流的粗粒度问题又能防止恶意用户高频调用。3. 鲁棒性难点Redis异常降级问题Redis宕机/网络异常时防抖功能不能影响主业务解决方案Redis未初始化/执行异常时直接放行请求核心业务优先面试回答思路分布式组件的降级策略是高可用设计的核心我们在防抖工具类中增加了Redis异常捕获和降级逻辑当Redis连接失败或执行异常时防抖功能自动降级为放行保证核心业务接口的可用性同时通过日志记录异常便于后续排查。4. 易错点永久Key清理问题序列化异常/参数解析失败会导致Key无过期时间第一次调用后永久限流解决方案检测到ttl-1永久Key时自动删除并重试获取锁面试回答思路实际生产中Redis Key可能因序列化、参数错误等原因被设置为永久有效我们通过getExpire方法检测Key的过期时间若发现永久Key则立即删除并重试避免因历史残留Key导致的误限流保证防抖功能的稳定性。5. 性能优化序列化器全局配置问题每次调用防抖工具类都重新设置序列化器增加性能开销解决方案通过PostConstruct在Bean初始化时仅配置1次序列化器面试回答思路性能优化的核心是减少重复操作我们将Redis序列化器的配置放在PostConstruct初始化方法中仅执行1次避免每次调用防抖方法都重复创建序列化器对象同时缓存ValueOperations减少RedisTemplate的重复调用提升接口响应速度。五、开箱即用使用指南1. 环境依赖确保BladeX项目中已引入Redis相关依赖BladeX默认已集成!-- BladeX Redis依赖 --dependencygroupIdorg.springblade/groupIdartifactIdblade-core-redis/artifactId/dependency2. 配置Redisapplication.ymlspring:redis:host:127.0.0.1port:6379password:123456database:0timeout:5000ms3. 快速接入步骤步骤1复制代码文件将Debounce、BladeDebounceUtil、BladeDebounceAspect三个类复制到项目对应包下步骤2业务接口添加注解Debounce(prefix业务前缀:,// 如order:submit:key#参数名,// 如#orderId、#user.idexpireTime10,// 防抖时间秒message操作过于频繁请10秒后再试)步骤3测试验证第一次调用接口正常返回业务结果Redis中生成Keyttl配置的过期时间过期时间内重复调用返回R.fail提示语过期时间后调用正常返回业务结果。4. 常见问题排查问题现象排查方案第一次调用就限流1. 执行redis-cli DEL Key清理残留Key2. 检查序列化器是否配置为StringSpEL解析失败1. 检查表达式是否正确如#miniUserId是否与方法参数名一致2. 查看日志中的解析异常信息Redis Key永久有效1. 检查序列化器配置2. 工具类已自动清理永久Key重启应用后重试防抖功能不生效1. 检查注解是否标注在方法上2. 检查AOP切面是否被Spring扫描Component六、扩展建议1. 功能扩展本地锁/分布式锁切换基于注解useDistributedLock参数实现本地锁ReentrantLock和分布式锁的切换适配单机/集群环境防抖时间动态配置整合Nacos/Apollo配置中心支持防抖时间、提示语动态修改无需重启应用批量防抖支持注解配置多个Key实现“多参数组合”防抖限流次数统计增加计数器统计接口被限流的次数对接监控平台如Prometheus/Grafana自定义返回结果支持注解配置返回码/返回体适配不同业务的返回格式。2. 性能扩展Redis连接池优化配置RedisTemplate的连接池参数提升高并发下的性能本地缓存预热热点Key的防抖结果本地缓存减少Redis调用异步释放锁操作成功后异步释放锁提升接口响应速度。3. 安全扩展Key前缀白名单限制可使用的Key前缀避免恶意拼接Key占用Redis空间防抖时间上限限制注解expireTime的最大值避免设置过长的防抖时间IP限流扩展结合用户IP生成Key防止单IP高频调用。七、总结本方案基于自定义注解AOP实现了BladeX框架下的高性能、高可用接口防抖功能核心解决了分布式环境下的重复调用问题同时兼顾了易用性、扩展性和鲁棒性。方案中的Redis原子操作、SpEL动态Key、序列化优化、异常降级等设计思路也是分布式系统开发中的高频考点既满足业务需求也适配面试场景的核心考点。完整版源码importjava.lang.annotation.*;importjava.util.concurrent.TimeUnit;/** * BladeX专属防抖注解 * 避免重复提交/重复操作 */Target({ElementType.METHOD})Retention(RetentionPolicy.RUNTIME)DocumentedpublicinterfaceDebounce{/** * 防抖Redis Key前缀默认blade:debounce: */Stringprefix()defaultblade:debounce:;/** * 防抖Key的拼接规则支持SpEL表达式 * 示例 * - #information.cardNo取方法参数information的cardNo属性 * - #userId取方法参数userId * - #root.args[0].id取第一个参数的id属性 */Stringkey()default;/** * 防抖过期时间默认5秒 */longexpireTime()default5;/** * 时间单位默认秒 */TimeUnittimeUnit()defaultTimeUnit.SECONDS;/** * 重复操作提示信息 */Stringmessage()default操作过于频繁请稍后再试;/** * 是否使用分布式锁BladeX集群环境默认开启 */booleanuseDistributedLock()defaulttrue;/** * 分布式锁等待时间默认0立即返回 */longlockWaitTime()default0;}/** * BladeX Redis防抖工具类 */Slf4jComponentpublicclassBladeDebounceUtil{// 配置常量 /** * 默认防抖value标识锁 */privatestaticfinalStringDEFAULT_DEBOUNCE_VALUE1;/** * 默认过期时间秒 */privatestaticfinallongDEFAULT_EXPIRE_SECONDS5;/** * 日志开关生产可关闭调试日志 */privatestaticfinalbooleanLOG_ENABLEtrue;ResourceprivateBladeRedisbladeRedis;/** * 全局RedisTemplate只初始化1次 */privateRedisTemplateString,ObjectredisTemplate;/** * 全局ValueOperations避免重复获取 */privateValueOperationsString,ObjectvalueOps;// 初始化序列化器只执行1次 PostConstructpublicvoidinit(){if(bladeRedis!null){this.redisTemplatebladeRedis.getRedisTemplate();// 序列化器全局配置仅初始化1次提升性能if(this.redisTemplate!null){StringRedisSerializerstringSerializernewStringRedisSerializer();this.redisTemplate.setKeySerializer(stringSerializer);this.redisTemplate.setValueSerializer(stringSerializer);this.redisTemplate.setHashKeySerializer(stringSerializer);this.redisTemplate.setHashValueSerializer(stringSerializer);this.valueOpsthis.redisTemplate.opsForValue();log.info(【防抖工具类】初始化完成Redis序列化器配置成功);}}else{log.warn(【防抖工具类】BladeRedis注入失败防抖功能降级为直接放行);}}// 重载方法简化调用提升易用性 /** * 重载使用默认过期时间5秒 */publicbooleantryAcquire(Stringkey){returntryAcquire(key,DEFAULT_EXPIRE_SECONDS,TimeUnit.SECONDS);}/** * 重载支持自定义value便于区分不同业务的锁 */publicbooleantryAcquire(Stringkey,Stringvalue,longexpireTime,TimeUnittimeUnit){returninnerTryAcquire(key,value,expireTime,timeUnit);}// 核心方法优化逻辑 /** * 尝试获取防抖锁强制配置序列化器双重保障过期时间 */publicbooleantryAcquire(Stringkey,longexpireTime,TimeUnittimeUnit){returninnerTryAcquire(key,DEFAULT_DEBOUNCE_VALUE,expireTime,timeUnit);}/** * 内部核心逻辑抽离通用逻辑便于维护 */privatebooleaninnerTryAcquire(Stringkey,Stringvalue,longexpireTime,TimeUnittimeUnit){// 1. 前置校验 降级逻辑Redis异常时直接放行避免影响主业务try{Assert.hasText(key,防抖Key不能为空);Assert.hasText(value,防抖Value不能为空);}catch(IllegalArgumentExceptione){log.error(【防抖工具类】参数非法{},e.getMessage());returnfalse;}// Redis未初始化降级为放行不影响主业务if(redisTemplatenull||valueOpsnull){if(LOG_ENABLE){log.warn(【防抖工具类】Redis未初始化防抖功能降级直接放行Key{},key);}returntrue;}// 2. 校验过期时间longexpireSecondstimeUnit.toSeconds(expireTime);if(expireSeconds0){if(LOG_ENABLE){log.error(【防抖工具类】过期时间非法Key{}时间{}秒,key,expireSeconds);}returnfalse;}booleanlockSuccessfalse;Longttl-1L;try{// 第一步原子判断并设置Key带过期时间BooleansetResultvalueOps.setIfAbsent(key,value,expireTime,timeUnit);lockSuccessBoolean.TRUE.equals(setResult);// 第二步双重保障强制设置过期时间防止第一步失效if(lockSuccess){booleanexpireSuccessBoolean.TRUE.equals(redisTemplate.expire(key,expireTime,timeUnit));if(LOG_ENABLE){log.info(【防抖工具类】获取锁成功Key{}过期时间{}秒过期设置{},key,expireSeconds,expireSuccess?成功:失败);}}else{// 检查Key是否永久有效若是则强制删除清理残留ttlredisTemplate.getExpire(key,TimeUnit.SECONDS);if(ttl-1){if(LOG_ENABLE){log.error(【防抖工具类】检测到永久有效Key强制删除{},key);}redisTemplate.delete(key);// 重新尝试获取锁setResultvalueOps.setIfAbsent(key,value,expireTime,timeUnit);lockSuccessBoolean.TRUE.equals(setResult);// 重新获取ttl日志更准确ttllockSuccess?Long.valueOf(expireSeconds):redisTemplate.getExpire(key,TimeUnit.SECONDS);if(lockSuccessLOG_ENABLE){log.info(【防抖工具类】清理永久Key后获取锁成功Key{},key);}}}}catch(Exceptione){// Redis异常时降级为放行避免影响主业务log.error(【防抖工具类】获取锁异常Key{}异常信息{},key,e.getMessage(),e);returntrue;}// 优化日志只在开启日志且获取锁失败时打印if(!lockSuccessLOG_ENABLE){StringttlDescswitch(ttl.intValue()){case-1-永久有效;case-2-Key不存在;default-ttl秒;};log.warn(【防抖工具类】获取锁失败Key{}剩余过期时间{},key,ttlDesc);}returnlockSuccess;}/** * 手动释放防抖锁增加异常捕获避免释放失败影响主业务 */publicvoidreleaseLock(Stringkey){if(bladeRedisnull||keynull||key.isEmpty()){return;}try{bladeRedis.del(key);if(LOG_ENABLE){log.info(【防抖工具类】释放锁成功Key{},key);}}catch(Exceptione){log.error(【防抖工具类】释放锁失败Key{}异常信息{},key,e.getMessage(),e);}}}javaimportjakarta.annotation.Resource;importlombok.extern.slf4j.Slf4j;importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.annotation.Around;importorg.aspectj.lang.annotation.Aspect;importorg.aspectj.lang.annotation.Pointcut;importorg.aspectj.lang.reflect.MethodSignature;importorg.springblade.business.aspect.annotation.Debounce;importorg.springblade.business.util.BladeDebounceUtil;importorg.springblade.core.tool.api.R;importorg.springframework.context.expression.MethodBasedEvaluationContext;importorg.springframework.core.DefaultParameterNameDiscoverer;importorg.springframework.core.ParameterNameDiscoverer;importorg.springframework.core.annotation.Order;importorg.springframework.expression.ExpressionParser;importorg.springframework.expression.spel.standard.SpelExpressionParser;importorg.springframework.expression.spel.support.StandardEvaluationContext;importorg.springframework.stereotype.Component;importjava.lang.reflect.Method;/** * BladeX防抖注解切面 * 适配框架原生返回结果R */Slf4jAspectComponentOrder(3)publicclassBladeDebounceAspect{ResourceprivateBladeDebounceUtilbladeDebounceUtil;// SpEL表达式解析器privatefinalExpressionParserspelParsernewSpelExpressionParser();// 参数名解析器privatefinalParameterNameDiscovererparameterNameDiscoverernewDefaultParameterNameDiscoverer();/** * 切入点匹配所有标注Debounce的方法 */Pointcut(annotation(org.springblade.business.aspect.annotation.Debounce))publicvoiddebouncePointcut(){}/** * 环绕通知实现防抖逻辑 */Around(debouncePointcut())publicObjectaround(ProceedingJoinPointjoinPoint)throwsThrowable{// 目标方法标识类名.方法名StringtargetMethodjoinPoint.getTarget().getClass().getSimpleName().joinPoint.getSignature().getName();log.info(【自定义防抖切面】开始执行目标方法{},targetMethod);try{// 1. 获取方法和注解信息MethodSignaturesignature(MethodSignature)joinPoint.getSignature();Methodmethodsignature.getMethod();Debouncedebouncemethod.getAnnotation(Debounce.class);if(debouncenull){log.warn(【自定义防抖切面】目标方法{}未解析到Debounce注解直接放行,targetMethod);returnjoinPoint.proceed();}log.info(【自定义防抖切面】目标方法{}解析防抖注解成功配置前缀{}过期时间{}{}提示语{},targetMethod,debounce.prefix(),debounce.expireTime(),debounce.timeUnit().name(),debounce.message());// 2. 生成防抖Key前缀动态KeyStringredisKeygenerateRedisKey(joinPoint,debounce);log.info(【自定义防抖切面】目标方法{}生成防抖RedisKey{},targetMethod,redisKey);if(redisKey.isEmpty()){log.error(【自定义防抖切面】目标方法{}防抖Key生成失败空值拒绝执行,targetMethod);returnR.fail(防抖Key配置异常请检查Debounce注解);}// 环绕通知中生成redisKey后新增longexpireSecondsdebounce.timeUnit().toSeconds(debounce.expireTime());if(expireSeconds0){log.error(【BladeX防抖切面】目标方法{}过期时间配置错误{}秒拒绝执行,targetMethod,expireSeconds);returnR.fail(限流配置异常请联系管理员);}// 3. 使用BladeRedis校验防抖原子操作booleanacquireSuccessbladeDebounceUtil.tryAcquire(redisKey,debounce.expireTime(),debounce.timeUnit());log.info(【自定义防抖切面】目标方法{}Redis防抖校验结果{}true未重复false重复,targetMethod,acquireSuccess);// 4. 重复操作返回BladeX原生R.fail结果if(!acquireSuccess){log.warn(【自定义防抖切面】目标方法{}检测到重复操作RedisKey{}返回提示{},targetMethod,redisKey,debounce.message());returnR.fail(debounce.message());}// 5. 执行原方法log.info(【自定义防抖切面】目标方法{}防抖校验通过开始执行原方法,targetMethod);ObjectresultjoinPoint.proceed();log.info(【自定义防抖切面】目标方法{}原方法执行完成返回结果类型{},targetMethod,resultnull?null:result.getClass().getSimpleName());returnresult;}catch(Throwablee){log.error(【自定义防抖切面】目标方法{}执行过程中发生异常,targetMethod,e);throwe;// 抛出异常交给BladeX全局异常处理器处理}finally{log.info(【自定义防抖切面】目标方法{}切面执行结束,targetMethod);//可选操作成功后手动释放锁根据业务需求//if (redisKey ! null) {// bladeDebounceUtil.releaseLock(redisKey);// log.info(【BladeX防抖切面】目标方法{}手动释放防抖锁RedisKey{}, targetMethod, redisKey);//}}}/** * 生成Redis Key */privateStringgenerateRedisKey(ProceedingJoinPointjoinPoint,Debouncedebounce){MethodSignaturesignature(MethodSignature)joinPoint.getSignature();Methodmethodsignature.getMethod();Object[]argsjoinPoint.getArgs();StringtargetMethodmethod.getDeclaringClass().getSimpleName().method.getName();StringspelKeydebounce.key();if(spelKey.isEmpty()){StringdefaultKeydebounce.prefix()method.getDeclaringClass().getSimpleName()_method.getName();log.debug(【自定义防抖切面】目标方法{}未配置自定义SpEL Key使用默认Key{},targetMethod,defaultKey);returndefaultKey;}// 创建SpEL上下文StandardEvaluationContextcontextnewMethodBasedEvaluationContext(null,method,args,parameterNameDiscoverer);ObjectkeyObjnull;try{keyObjspelParser.parseExpression(spelKey).getValue(context);}catch(Exceptione){log.error(【自定义防抖切面】目标方法{}解析SpEL表达式失败表达式{},targetMethod,spelKey,e);}StringdynamicKeydebounce.prefix()(keyObjnull?:keyObj.toString());log.debug(【自定义防抖切面】目标方法{}SpEL表达式解析完成表达式{}解析结果{}最终Key{},targetMethod,spelKey,keyObj,dynamicKey);returndynamicKey;}}关于这个自定义注解实现接口防抖的用法如下直接在我们的接口上添加注解可以自己指定参数如果不知道就用自定义注解的默认值测试如下第一次请求接口的时候接口可以正常响应返回数据给前端在20秒内再请求的话会显示如下响应而在20秒之后在请求又可以正常响应数据给前端接口防抖成功。