幂等性注解定义
importjava.lang.annotation.*; importjava.util.concurrent.TimeUnit; /*** @author jeckxu*/ElementType.METHOD) (value=RetentionPolicy.RUNTIME) (public@interfaceIdempotent { /*** 幂等操作的唯一标识,使用spring el表达式 用#来引用方法参数* @return Spring-EL expression*/Stringkey() default""; /*** 有效期 默认:1 有效期要大于程序执行时间,否则请求还是可能会进来* @return expireTime*/intexpireTime() default1; /*** 时间单位 默认:s* @return TimeUnit*/TimeUnittimeUnit() defaultTimeUnit.SECONDS; /*** 提示信息,可自定义* @return String*/Stringinfo() default"ROOT.IDEMPOTENT.DO_NOT_REPEAT_THE_OPERATION"; /*** 是否在业务完成后删除key true:删除 false:不删除* @return boolean*/booleandelKey() defaulttrue; }
幂等性配置类
importorg.springframework.boot.context.properties.ConfigurationProperties; /*** @author Jeckxu*/MagicalIdempotentProperties.PREFIX) (publicclassMagicalIdempotentProperties { publicstaticfinalStringPREFIX="magical.idempotent"; /*** 是否开启:默认为:false,便于生成配置提示。*/privateBooleanenabled=Boolean.FALSE; /*** 单机配置:redis 服务地址*/privateStringaddress="redis://127.0.0.1:6379"; /*** 密码配置*/privateStringpassword; /*** db*/privateIntegerdatabase=0; /*** 连接池大小*/privateIntegerpoolSize=20; /*** 最小空闲连接数*/privateIntegeridleSize=5; /*** 连接空闲超时,单位:毫秒*/privateIntegeridleTimeout=60000; /*** 连接超时,单位:毫秒*/privateIntegerconnectionTimeout=3000; /*** 命令等待超时,单位:毫秒*/privateIntegertimeout=10000; /*** 集群模式,单机:single,主从:master,哨兵模式:sentinel,集群模式:cluster*/privateModemode=Mode.single; /*** 主从模式,主地址*/privateStringmasterAddress; /*** 主从模式,从地址*/privateString[] slaveAddress; /*** 哨兵模式:主名称*/privateStringmasterName; /*** 哨兵模式地址*/privateString[] sentinelAddress; /*** 集群模式节点地址*/privateString[] nodeAddress; publicenumMode { /*** 集群模式,单机:single,主从:master,哨兵模式:sentinel,集群模式:cluster*/single, master, sentinel, cluster; } /*** 是否开启:默认为:false,便于生成配置提示。*/publicBooleangetEnabled() { returnthis.enabled; } /*** 单机配置:redis 服务地址*/publicStringgetAddress() { returnthis.address; } /*** 密码配置*/publicStringgetPassword() { returnthis.password; } /*** db*/publicIntegergetDatabase() { returnthis.database; } /*** 连接池大小*/publicIntegergetPoolSize() { returnthis.poolSize; } /*** 最小空闲连接数*/publicIntegergetIdleSize() { returnthis.idleSize; } /*** 连接空闲超时,单位:毫秒*/publicIntegergetIdleTimeout() { returnthis.idleTimeout; } /*** 连接超时,单位:毫秒*/publicIntegergetConnectionTimeout() { returnthis.connectionTimeout; } /*** 命令等待超时,单位:毫秒*/publicIntegergetTimeout() { returnthis.timeout; } /*** 集群模式,单机:single,主从:master,哨兵模式:sentinel,集群模式:cluster*/publicModegetMode() { returnthis.mode; } /*** 主从模式,主地址*/publicStringgetMasterAddress() { returnthis.masterAddress; } /*** 主从模式,从地址*/publicString[] getSlaveAddress() { returnthis.slaveAddress; } /*** 哨兵模式:主名称*/publicStringgetMasterName() { returnthis.masterName; } /*** 哨兵模式地址*/publicString[] getSentinelAddress() { returnthis.sentinelAddress; } /*** 集群模式节点地址*/publicString[] getNodeAddress() { returnthis.nodeAddress; } /*** 是否开启:默认为:false,便于生成配置提示。*/publicvoidsetEnabled(finalBooleanenabled) { this.enabled=enabled; } /*** 单机配置:redis 服务地址*/publicvoidsetAddress(finalStringaddress) { this.address=address; } /*** 密码配置*/publicvoidsetPassword(finalStringpassword) { this.password=password; } /*** db*/publicvoidsetDatabase(finalIntegerdatabase) { this.database=database; } /*** 连接池大小*/publicvoidsetPoolSize(finalIntegerpoolSize) { this.poolSize=poolSize; } /*** 最小空闲连接数*/publicvoidsetIdleSize(finalIntegeridleSize) { this.idleSize=idleSize; } /*** 连接空闲超时,单位:毫秒*/publicvoidsetIdleTimeout(finalIntegeridleTimeout) { this.idleTimeout=idleTimeout; } /*** 连接超时,单位:毫秒*/publicvoidsetConnectionTimeout(finalIntegerconnectionTimeout) { this.connectionTimeout=connectionTimeout; } /*** 命令等待超时,单位:毫秒*/publicvoidsetTimeout(finalIntegertimeout) { this.timeout=timeout; } /*** 集群模式,单机:single,主从:master,哨兵模式:sentinel,集群模式:cluster*/publicvoidsetMode(finalModemode) { this.mode=mode; } /*** 主从模式,主地址*/publicvoidsetMasterAddress(finalStringmasterAddress) { this.masterAddress=masterAddress; } /*** 主从模式,从地址*/publicvoidsetSlaveAddress(finalString[] slaveAddress) { this.slaveAddress=slaveAddress; } /*** 哨兵模式:主名称*/publicvoidsetMasterName(finalStringmasterName) { this.masterName=masterName; } /*** 哨兵模式地址*/publicvoidsetSentinelAddress(finalString[] sentinelAddress) { this.sentinelAddress=sentinelAddress; } /*** 集群模式节点地址*/publicvoidsetNodeAddress(finalString[] nodeAddress) { this.nodeAddress=nodeAddress; } }
幂等性切面类
importorg.aspectj.lang.JoinPoint; importorg.aspectj.lang.annotation.After; importorg.aspectj.lang.annotation.Aspect; importorg.aspectj.lang.annotation.Before; importorg.aspectj.lang.annotation.Pointcut; importorg.aspectj.lang.reflect.MethodSignature; importorg.jeckxu.magical.core.idempotent.annotation.Idempotent; importorg.jeckxu.magical.core.idempotent.exception.IdempotentException; importorg.jeckxu.magical.core.idempotent.expression.KeyResolver; importorg.redisson.api.RMapCache; importorg.redisson.api.RedissonClient; importorg.slf4j.Logger; importorg.slf4j.LoggerFactory; importorg.springframework.util.CollectionUtils; importorg.springframework.util.StringUtils; importorg.springframework.web.context.request.RequestContextHolder; importorg.springframework.web.context.request.ServletRequestAttributes; importjavax.servlet.http.HttpServletRequest; importjava.lang.reflect.Method; importjava.time.LocalDateTime; importjava.util.Arrays; importjava.util.HashMap; importjava.util.Map; importjava.util.concurrent.TimeUnit; /*** @author jeckxu*/publicclassIdempotentAspect { privatestaticfinalLoggerLOGGER=LoggerFactory.getLogger(IdempotentAspect.class); privateThreadLocal<Map<String, Object>>threadLocal=newThreadLocal(); privatestaticfinalStringRMAPCACHE_KEY="magical:idempotent"; privatestaticfinalStringKEY="key"; privatestaticfinalStringDELKEY="delKey"; privatefinalRedissonClientredissonClient; privatefinalKeyResolverkeyResolver; "@annotation(org.jeckxu.magical.core.idempotent.annotation.Idempotent)") (publicvoidpointCut() { } "pointCut()") (publicvoidbeforePointCut(JoinPointjoinPoint) throwsException { ServletRequestAttributesrequestAttributes= (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequestrequest=requestAttributes.getRequest(); MethodSignaturesignature= (MethodSignature) joinPoint.getSignature(); Methodmethod=signature.getMethod(); if (!method.isAnnotationPresent(Idempotent.class)) { return; } Idempotentidempotent=method.getAnnotation(Idempotent.class); Stringkey; // 若没有配置 幂等 标识编号,则使用 url + 参数列表作为区分if (StringUtils.isEmpty(idempotent.key())) { Stringurl=request.getRequestURL().toString(); StringargString=Arrays.asList(joinPoint.getArgs()).toString(); key=url+argString; } else { // 使用jstl 规则区分key=keyResolver.resolver(idempotent, joinPoint); } longexpireTime=idempotent.expireTime(); Stringinfo=idempotent.info(); TimeUnittimeUnit=idempotent.timeUnit(); booleandelKey=idempotent.delKey(); // do not need check nullRMapCache<String, Object>rMapCache=redissonClient.getMapCache(RMAPCACHE_KEY); Stringvalue=LocalDateTime.now().toString().replace("T", " "); Objectv1; if (null!=rMapCache.get(key)) { // had storedthrownewIdempotentException(info); } synchronized (this) { v1=rMapCache.putIfAbsent(key, value, expireTime, TimeUnit.SECONDS); if (null!=v1) { thrownewIdempotentException(info); } else { LOGGER.info("[idempotent]:has stored key={},value={},expireTime={}{},now={}", key, value, expireTime, timeUnit, LocalDateTime.now().toString()); } } Map<String, Object>map=CollectionUtils.isEmpty(threadLocal.get()) ?newHashMap<>(4) : threadLocal.get(); map.put(KEY, key); map.put(DELKEY, delKey); threadLocal.set(map); } "pointCut()") (publicvoidafterPointCut(JoinPointjoinPoint) { Map<String, Object>map=threadLocal.get(); if (CollectionUtils.isEmpty(map)) { return; } RMapCache<Object, Object>mapCache=redissonClient.getMapCache(RMAPCACHE_KEY); if (mapCache.size() ==0) { return; } Stringkey=map.get(KEY).toString(); booleandelKey= (boolean) map.get(DELKEY); if (delKey) { mapCache.fastRemove(key); LOGGER.info("[idempotent]:has removed key={}", key); } threadLocal.remove(); } publicIdempotentAspect(finalRedissonClientredissonClient, finalKeyResolverkeyResolver) { this.redissonClient=redissonClient; this.keyResolver=keyResolver; } }
幂等性异常定义
/*** Idempotent Exception If there is a custom global exception, you need to inherit the* custom global exception.** @author jeckxu*/publicclassIdempotentExceptionextendsException { publicIdempotentException() { super(); } publicIdempotentException(Stringmessage) { super(message); } publicIdempotentException(Stringmessage, Throwablecause) { super(message, cause); } publicIdempotentException(Throwablecause) { super(cause); } protectedIdempotentException(Stringmessage, Throwablecause, booleanenableSuppression, booleanwritableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } }
幂等性唯一标志处理器
importorg.aspectj.lang.JoinPoint; importorg.jeckxu.magical.core.idempotent.annotation.Idempotent; /*** @author jeckxu* @date 2020/11/10* <p>* 唯一标志处理器*/publicinterfaceKeyResolver { /*** 解析处理 key* @param idempotent 接口注解标识* @param point 接口切点信息* @return 处理结果*/Stringresolver(Idempotentidempotent, JoinPointpoint); }
importorg.aspectj.lang.JoinPoint; importorg.aspectj.lang.reflect.MethodSignature; importorg.jeckxu.magical.core.idempotent.annotation.Idempotent; importorg.springframework.core.LocalVariableTableParameterNameDiscoverer; importorg.springframework.expression.Expression; importorg.springframework.expression.spel.standard.SpelExpressionParser; importorg.springframework.expression.spel.support.StandardEvaluationContext; importjava.lang.reflect.Method; /*** @author jeckxu*/publicclassExpressionResolverimplementsKeyResolver { privatestaticfinalSpelExpressionParserPARSER=newSpelExpressionParser(); privatestaticfinalLocalVariableTableParameterNameDiscovererDISCOVERER=newLocalVariableTableParameterNameDiscoverer(); publicStringresolver(Idempotentidempotent, JoinPointpoint) { Object[] arguments=point.getArgs(); String[] params=DISCOVERER.getParameterNames(getMethod(point)); StandardEvaluationContextcontext=newStandardEvaluationContext(); for (intlen=0; len<params.length; len++) { context.setVariable(params[len], arguments[len]); } Expressionexpression=PARSER.parseExpression(idempotent.key()); returnexpression.getValue(context, String.class); } /*** 根据切点解析方法信息* @param joinPoint 切点信息* @return Method 原信息*/privateMethodgetMethod(JoinPointjoinPoint) { MethodSignaturesignature= (MethodSignature) joinPoint.getSignature(); Methodmethod=signature.getMethod(); if (method.getDeclaringClass().isInterface()) { try { method=joinPoint.getTarget().getClass().getDeclaredMethod(joinPoint.getSignature().getName(), method.getParameterTypes()); } catch (SecurityException|NoSuchMethodExceptione) { thrownewRuntimeException(e); } } returnmethod; } }
幂等性异常统一处理
importorg.jeckxu.magical.core.idempotent.exception.IdempotentException; importorg.jeckxu.magical.core.launch.destroy.MagicalDestroying; importorg.jeckxu.magical.core.launch.utils.I18nMessageUtils; importorg.jeckxu.magical.core.tool.api.R; importorg.springframework.boot.autoconfigure.condition.ConditionalOnClass; importorg.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; importorg.springframework.context.annotation.Configuration; importorg.springframework.core.annotation.Order; importorg.springframework.http.HttpStatus; importorg.springframework.web.bind.annotation.ExceptionHandler; importorg.springframework.web.bind.annotation.ResponseStatus; importorg.springframework.web.bind.annotation.RestControllerAdvice; importorg.springframework.web.servlet.DispatcherServlet; importjavax.servlet.Servlet; 3) (proxyBeanMethods=false) (type=ConditionalOnWebApplication.Type.SERVLET) (Servlet.class, DispatcherServlet.class}) ({publicclassMagicalIdempotentExceptionTranslatorimplementsMagicalDestroying { privatestaticfinalorg.slf4j.Loggerlog=org.slf4j.LoggerFactory.getLogger(MagicalIdempotentExceptionTranslator.class); IdempotentException.class) (HttpStatus.INTERNAL_SERVER_ERROR) (publicR<Object>handleError(IdempotentExceptione) { e.printStackTrace(); returnR.fail(1999, I18nMessageUtils.get(e.getMessage())); } }
幂等插件自动配置【初始化】
importorg.jeckxu.magical.core.idempotent.aspect.IdempotentAspect; importorg.jeckxu.magical.core.idempotent.expression.ExpressionResolver; importorg.jeckxu.magical.core.idempotent.expression.KeyResolver; importorg.jeckxu.magical.core.idempotent.prop.MagicalIdempotentProperties; importorg.jeckxu.magical.core.launch.destroy.MagicalDestroying; importorg.jeckxu.magical.core.tool.utils.StringUtil; importorg.redisson.Redisson; importorg.redisson.api.RedissonClient; importorg.redisson.config.*; importorg.springframework.boot.autoconfigure.AutoConfigureAfter; importorg.springframework.boot.autoconfigure.condition.ConditionalOnClass; importorg.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; importorg.springframework.boot.autoconfigure.condition.ConditionalOnProperty; importorg.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; importorg.springframework.boot.context.properties.EnableConfigurationProperties; importorg.springframework.context.annotation.Bean; importorg.springframework.context.annotation.Configuration; /*** 幂等插件初始化* @author jeckxu*/proxyBeanMethods=false) (RedissonClient.class) (MagicalIdempotentProperties.class) (RedisAutoConfiguration.class) (value="magical.idempotent.enabled", havingValue="true") (publicclassIdempotentAutoConfigurationimplementsMagicalDestroying { privatestaticConfigsingleConfig(MagicalIdempotentPropertiesproperties) { Configconfig=newConfig(); SingleServerConfigserversConfig=config.useSingleServer(); serversConfig.setAddress(properties.getAddress()); Stringpassword=properties.getPassword(); if (StringUtil.isNotBlank(password)) { serversConfig.setPassword(password); } serversConfig.setDatabase(properties.getDatabase()); serversConfig.setConnectionPoolSize(properties.getPoolSize()); serversConfig.setConnectionMinimumIdleSize(properties.getIdleSize()); serversConfig.setIdleConnectionTimeout(properties.getConnectionTimeout()); serversConfig.setConnectTimeout(properties.getConnectionTimeout()); serversConfig.setTimeout(properties.getTimeout()); returnconfig; } privatestaticConfigmasterSlaveConfig(MagicalIdempotentPropertiesproperties) { Configconfig=newConfig(); MasterSlaveServersConfigserversConfig=config.useMasterSlaveServers(); serversConfig.setMasterAddress(properties.getMasterAddress()); serversConfig.addSlaveAddress(properties.getSlaveAddress()); Stringpassword=properties.getPassword(); if (StringUtil.isNotBlank(password)) { serversConfig.setPassword(password); } serversConfig.setDatabase(properties.getDatabase()); serversConfig.setMasterConnectionPoolSize(properties.getPoolSize()); serversConfig.setMasterConnectionMinimumIdleSize(properties.getIdleSize()); serversConfig.setSlaveConnectionPoolSize(properties.getPoolSize()); serversConfig.setSlaveConnectionMinimumIdleSize(properties.getIdleSize()); serversConfig.setIdleConnectionTimeout(properties.getConnectionTimeout()); serversConfig.setConnectTimeout(properties.getConnectionTimeout()); serversConfig.setTimeout(properties.getTimeout()); returnconfig; } privatestaticConfigsentinelConfig(MagicalIdempotentPropertiesproperties) { Configconfig=newConfig(); SentinelServersConfigserversConfig=config.useSentinelServers(); serversConfig.setMasterName(properties.getMasterName()); serversConfig.addSentinelAddress(properties.getSentinelAddress()); Stringpassword=properties.getPassword(); if (StringUtil.isNotBlank(password)) { serversConfig.setPassword(password); } serversConfig.setDatabase(properties.getDatabase()); serversConfig.setMasterConnectionPoolSize(properties.getPoolSize()); serversConfig.setMasterConnectionMinimumIdleSize(properties.getIdleSize()); serversConfig.setSlaveConnectionPoolSize(properties.getPoolSize()); serversConfig.setSlaveConnectionMinimumIdleSize(properties.getIdleSize()); serversConfig.setIdleConnectionTimeout(properties.getConnectionTimeout()); serversConfig.setConnectTimeout(properties.getConnectionTimeout()); serversConfig.setTimeout(properties.getTimeout()); returnconfig; } privatestaticConfigclusterConfig(MagicalIdempotentPropertiesproperties) { Configconfig=newConfig(); ClusterServersConfigserversConfig=config.useClusterServers(); serversConfig.addNodeAddress(properties.getNodeAddress()); Stringpassword=properties.getPassword(); if (StringUtil.isNotBlank(password)) { serversConfig.setPassword(password); } serversConfig.setMasterConnectionPoolSize(properties.getPoolSize()); serversConfig.setMasterConnectionMinimumIdleSize(properties.getIdleSize()); serversConfig.setSlaveConnectionPoolSize(properties.getPoolSize()); serversConfig.setSlaveConnectionMinimumIdleSize(properties.getIdleSize()); serversConfig.setIdleConnectionTimeout(properties.getConnectionTimeout()); serversConfig.setConnectTimeout(properties.getConnectionTimeout()); serversConfig.setTimeout(properties.getTimeout()); returnconfig; } /*** 切面 拦截处理所有 @Idempotent* @return Aspect*/publicIdempotentAspectidempotentAspect(MagicalIdempotentPropertiesproperties) { returnnewIdempotentAspect(redissonClient(properties), newExpressionResolver()); } /*** key 解析器* @return KeyResolver*/KeyResolver.class) (publicKeyResolverkeyResolver() { returnnewExpressionResolver(); } privatestaticRedissonClientredissonClient(MagicalIdempotentPropertiesproperties) { MagicalIdempotentProperties.Modemode=properties.getMode(); Configconfig; switch (mode) { casesentinel: config=sentinelConfig(properties); break; casecluster: config=clusterConfig(properties); break; casemaster: config=masterSlaveConfig(properties); break; casesingle: config=singleConfig(properties); break; default: config=newConfig(); break; } returnRedisson.create(config); } }
yml配置项
magical idempotent enabledfalse address redis //192.168.0.126379 password $ YOUR_PASSWORD