mirror of
https://github.com/continew-org/continew-starter.git
synced 2025-09-08 16:57:09 +08:00
refactor(idempotent): 重构幂等模块并支持 Redisson 缓存
This commit is contained in:
@@ -139,6 +139,11 @@ public class PropertiesConstants {
|
||||
*/
|
||||
public static final String TENANT = CONTINEW_STARTER + StringConstants.DOT + "tenant";
|
||||
|
||||
/**
|
||||
* 幂等配置
|
||||
*/
|
||||
public static final String IDEMPOTENT = "idempotent";
|
||||
|
||||
private PropertiesConstants() {
|
||||
}
|
||||
}
|
||||
|
@@ -13,45 +13,15 @@
|
||||
<description>ContiNew Starter 幂等模块</description>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- Spring Boot Starter(自动配置相关依赖) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 缓存模块 - Redisson -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<groupId>top.continew</groupId>
|
||||
<artifactId>continew-starter-cache-redisson</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
@@ -20,27 +20,39 @@ import java.lang.annotation.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 定义幂等注解
|
||||
* 幂等注解
|
||||
*
|
||||
* @version 1.0
|
||||
* @Author loach
|
||||
* @Date 2025-03-07 19:26
|
||||
* @Package top.continew.starter.idempotent.annotation.Idempotent
|
||||
* @author loach
|
||||
* @author Charles7c
|
||||
* @since 2.10.0
|
||||
*/
|
||||
@Documented
|
||||
@Target({ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface Idempotent {
|
||||
|
||||
// 幂等key前缀
|
||||
String prefix() default "idempotent:";
|
||||
/**
|
||||
* 名称
|
||||
*/
|
||||
String name() default "";
|
||||
|
||||
// key的过期时间
|
||||
long timeout() default 0;
|
||||
/**
|
||||
* 键(支持 Spring EL 表达式)
|
||||
*/
|
||||
String key() default "";
|
||||
|
||||
// 时间单位
|
||||
TimeUnit timeUnit() default TimeUnit.SECONDS;
|
||||
/**
|
||||
* 超时时间
|
||||
*/
|
||||
int timeout() default 1;
|
||||
|
||||
// 失败时的提示信息
|
||||
/**
|
||||
* 时间单位(默认:毫秒)
|
||||
*/
|
||||
TimeUnit unit() default TimeUnit.MILLISECONDS;
|
||||
|
||||
/**
|
||||
* 提示信息
|
||||
*/
|
||||
String message() default "请勿重复操作";
|
||||
}
|
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.idempotent.aop;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import top.continew.starter.cache.redisson.util.RedisUtils;
|
||||
import top.continew.starter.core.util.expression.ExpressionUtils;
|
||||
import top.continew.starter.idempotent.annotation.Idempotent;
|
||||
import top.continew.starter.idempotent.autoconfigure.IdempotentProperties;
|
||||
import top.continew.starter.idempotent.exception.IdempotentException;
|
||||
import top.continew.starter.idempotent.generator.IdempotentNameGenerator;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* 幂等切面
|
||||
*
|
||||
* @author loach
|
||||
* @author Charles7c
|
||||
* @since 2.10.0
|
||||
*/
|
||||
@Aspect
|
||||
public class IdempotentAspect {
|
||||
|
||||
private final IdempotentProperties properties;
|
||||
private final IdempotentNameGenerator nameGenerator;
|
||||
|
||||
public IdempotentAspect(IdempotentProperties properties, IdempotentNameGenerator nameGenerator) {
|
||||
this.properties = properties;
|
||||
this.nameGenerator = nameGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* 幂等处理
|
||||
*
|
||||
* @param joinPoint 切点
|
||||
* @param idempotent 幂等注解
|
||||
* @return 目标方法的执行结果
|
||||
* @throws Throwable /
|
||||
*/
|
||||
@Around("@annotation(idempotent)")
|
||||
public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {
|
||||
String cacheKey = this.getCacheKey(joinPoint, idempotent);
|
||||
// 如果键已存在,则抛出异常
|
||||
if (!RedisUtils.setIfAbsent(cacheKey, Duration.ofMillis(idempotent.unit().toMillis(idempotent.timeout())))) {
|
||||
throw new IdempotentException(idempotent.message());
|
||||
}
|
||||
// 执行目标方法
|
||||
try {
|
||||
return joinPoint.proceed();
|
||||
} catch (Throwable e) {
|
||||
// 删除键
|
||||
RedisUtils.delete(cacheKey);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存 Key
|
||||
*
|
||||
* @param joinPoint 切点
|
||||
* @param idempotent 幂等注解
|
||||
* @return 缓存 Key
|
||||
*/
|
||||
private String getCacheKey(ProceedingJoinPoint joinPoint, Idempotent idempotent) {
|
||||
Object target = joinPoint.getTarget();
|
||||
MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
|
||||
Method method = methodSignature.getMethod();
|
||||
Object[] args = joinPoint.getArgs();
|
||||
// 获取名称
|
||||
String name = idempotent.name();
|
||||
if (CharSequenceUtil.isBlank(name)) {
|
||||
name = nameGenerator.generate(target, method, args);
|
||||
}
|
||||
// 解析 Key
|
||||
String key = idempotent.key();
|
||||
if (CharSequenceUtil.isNotBlank(key)) {
|
||||
Object eval = ExpressionUtils.eval(key, target, method, args);
|
||||
if (ObjectUtil.isNull(eval)) {
|
||||
throw new IdempotentException("幂等 Key 解析错误");
|
||||
}
|
||||
key = Convert.toStr(eval);
|
||||
}
|
||||
return RedisUtils.formatKey(properties.getKeyPrefix(), name, key);
|
||||
}
|
||||
}
|
@@ -1,71 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.idempotent.aspect;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import top.continew.starter.idempotent.annotation.Idempotent;
|
||||
import top.continew.starter.idempotent.service.IdempotentService;
|
||||
import top.continew.starter.idempotent.util.IdempotentKeyGenerator;
|
||||
|
||||
/**
|
||||
* 注解切面
|
||||
*
|
||||
* @version 1.0
|
||||
* @Author loach
|
||||
* @Date 2025-03-07 19:27
|
||||
* @Package top.continew.starter.idempotent.aspect.IdempotentAspect
|
||||
*/
|
||||
@Aspect
|
||||
public class IdempotentAspect {
|
||||
|
||||
private final IdempotentService idempotentService;
|
||||
|
||||
public IdempotentAspect(IdempotentService idempotentService) {
|
||||
this.idempotentService = idempotentService;
|
||||
}
|
||||
|
||||
/**
|
||||
* AspectJ 环绕通知
|
||||
*
|
||||
* @param joinPoint 切点
|
||||
* @param idempotent 注解
|
||||
* @return
|
||||
* @throws Throwable
|
||||
*/
|
||||
@Around("@annotation(idempotent)")
|
||||
public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {
|
||||
HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes())
|
||||
.getRequest();
|
||||
String key = IdempotentKeyGenerator.generateKey(idempotent.prefix(), joinPoint, request);
|
||||
|
||||
try {
|
||||
if (!idempotentService.checkAndLock(key, idempotent.timeout())) {
|
||||
throw new RuntimeException(idempotent.message());
|
||||
}
|
||||
return joinPoint.proceed();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
idempotentService.unlock(key);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.idempotent.autoconfigure;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import top.continew.starter.core.constant.PropertiesConstants;
|
||||
import top.continew.starter.idempotent.aop.IdempotentAspect;
|
||||
import top.continew.starter.idempotent.generator.IdempotentNameGenerator;
|
||||
|
||||
/**
|
||||
* 幂等自动配置
|
||||
*
|
||||
* @author loach
|
||||
* @author Charles7c
|
||||
* @since 2.10.0
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@EnableConfigurationProperties(IdempotentProperties.class)
|
||||
@ConditionalOnProperty(prefix = PropertiesConstants.IDEMPOTENT, name = PropertiesConstants.ENABLED, havingValue = "true", matchIfMissing = true)
|
||||
public class IdempotentAutoConfiguration {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(IdempotentAutoConfiguration.class);
|
||||
|
||||
/**
|
||||
* 幂等切面
|
||||
*/
|
||||
@Bean
|
||||
public IdempotentAspect idempotentAspect(IdempotentProperties properties,
|
||||
IdempotentNameGenerator idempotentNameGenerator) {
|
||||
return new IdempotentAspect(properties, idempotentNameGenerator);
|
||||
}
|
||||
|
||||
/**
|
||||
* 幂等名称生成器
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public IdempotentNameGenerator idempotentNameGenerator() {
|
||||
if (log.isErrorEnabled()) {
|
||||
log.error("Consider defining a bean of type '{}' in your configuration.", ResolvableType
|
||||
.forClass(IdempotentNameGenerator.class));
|
||||
}
|
||||
throw new NoSuchBeanDefinitionException(IdempotentNameGenerator.class);
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void postConstruct() {
|
||||
log.debug("[ContiNew Starter] - Auto Configuration 'Idempotent' completed initialization.");
|
||||
}
|
||||
}
|
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.idempotent.autoconfigure;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import top.continew.starter.core.constant.PropertiesConstants;
|
||||
|
||||
/**
|
||||
* 幂等配置属性
|
||||
*
|
||||
* @author loach
|
||||
* @author Charles7c
|
||||
* @since 2.10.0
|
||||
*/
|
||||
@ConfigurationProperties(PropertiesConstants.IDEMPOTENT)
|
||||
public class IdempotentProperties {
|
||||
|
||||
/**
|
||||
* Key 前缀
|
||||
*/
|
||||
private String keyPrefix = "Idempotent";
|
||||
|
||||
public String getKeyPrefix() {
|
||||
return keyPrefix;
|
||||
}
|
||||
|
||||
public void setKeyPrefix(String keyPrefix) {
|
||||
this.keyPrefix = keyPrefix;
|
||||
}
|
||||
}
|
@@ -1,73 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.idempotent.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import top.continew.starter.idempotent.aspect.IdempotentAspect;
|
||||
import top.continew.starter.idempotent.service.IdempotentService;
|
||||
import top.continew.starter.idempotent.service.impl.MemoryIdempotentServiceImpl;
|
||||
import top.continew.starter.idempotent.service.impl.RedisIdempotentServiceImpl;
|
||||
|
||||
/**
|
||||
* 引用配置:暂定默认内存实现,扫描到启用redis 使用redis实现
|
||||
*
|
||||
* @version 1.0
|
||||
* @Author loach
|
||||
* @Date 2025-03-07 20:03
|
||||
* @Package top.continew.starter.idempotent.config.IdempotentAutoConfiguration
|
||||
*/
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(IdempotentProperties.class)
|
||||
public class IdempotentAutoConfiguration {
|
||||
|
||||
@Autowired
|
||||
private IdempotentService idempotentService;
|
||||
|
||||
private final IdempotentProperties properties;
|
||||
|
||||
public IdempotentAutoConfiguration(IdempotentProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IdempotentAspect idempotentAspect(IdempotentService idempotentService) {
|
||||
return new IdempotentAspect(idempotentService);
|
||||
}
|
||||
|
||||
@Bean(name = "redisIdempotentService")
|
||||
@ConditionalOnBean(StringRedisTemplate.class)
|
||||
public IdempotentService redisIdempotentService(StringRedisTemplate redisTemplate) {
|
||||
return new RedisIdempotentServiceImpl(redisTemplate, properties.getRedisTimeout());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(IdempotentService.class)
|
||||
public IdempotentService memoryIdempotentService() {
|
||||
return new MemoryIdempotentServiceImpl(properties.getCleanInterval());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IdempotentProperties idempotentProperties() {
|
||||
return new IdempotentProperties();
|
||||
}
|
||||
}
|
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.idempotent.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* 属性配置类
|
||||
*
|
||||
* @version 1.0
|
||||
* @Author loach
|
||||
* @Date 2025-03-07 19:27
|
||||
* @Package top.continew.starter.idempotent.config.IdempotentProperties
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "idempotent")
|
||||
public class IdempotentProperties {
|
||||
|
||||
// 内存实现清理过期 key 的间隔(毫秒)默认5分钟
|
||||
private long cleanInterval = 300000;
|
||||
|
||||
// redis实现过期 key 的间隔(毫秒)默认5分钟
|
||||
private long redisTimeout = 300000;
|
||||
|
||||
public long getCleanInterval() {
|
||||
return cleanInterval;
|
||||
}
|
||||
|
||||
public void setCleanInterval(long cleanInterval) {
|
||||
this.cleanInterval = cleanInterval;
|
||||
}
|
||||
|
||||
public long getRedisTimeout() {
|
||||
return redisTimeout;
|
||||
}
|
||||
|
||||
public void setRedisTimeout(long redisTimeout) {
|
||||
this.redisTimeout = redisTimeout;
|
||||
}
|
||||
}
|
@@ -1,49 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.idempotent.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import top.continew.starter.idempotent.service.IdempotentService;
|
||||
import top.continew.starter.idempotent.service.impl.MemoryIdempotentServiceImpl;
|
||||
|
||||
/**
|
||||
* 内存定时任务配置类
|
||||
*
|
||||
* @version 1.0
|
||||
* @Author loach
|
||||
* @Date 2025-03-07 22:26
|
||||
* @Package top.continew.starter.idempotent.config.MemoryIdempotentSchedulingConfig
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnMissingBean(name = "redisIdempotentService") // 当 Redis Bean 不存在时启用
|
||||
@EnableScheduling
|
||||
public class MemoryIdempotentSchedulingConfig {
|
||||
|
||||
@Autowired
|
||||
private IdempotentService idempotentService;
|
||||
|
||||
@Scheduled(fixedRateString = "${idempotent.clean-interval:60000}")
|
||||
public void cleanExpiredKeys() {
|
||||
if (idempotentService instanceof MemoryIdempotentServiceImpl) {
|
||||
((MemoryIdempotentServiceImpl)idempotentService).cleanExpiredKeys();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.idempotent.exception;
|
||||
|
||||
import top.continew.starter.core.exception.BaseException;
|
||||
|
||||
/**
|
||||
* 幂等异常
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2.10.0
|
||||
*/
|
||||
public class IdempotentException extends BaseException {
|
||||
|
||||
public IdempotentException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public IdempotentException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
@@ -14,31 +14,26 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.idempotent.service;
|
||||
package top.continew.starter.idempotent.generator;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* 服务接口
|
||||
* 幂等名称生成器
|
||||
*
|
||||
* @version 1.0
|
||||
* @Author loach
|
||||
* @Date 2025-03-07 19:48
|
||||
* @Package top.continew.starter.idempotent.service.IdempotentService
|
||||
* @author loach
|
||||
* @author Charles7c
|
||||
* @since 2.10.0
|
||||
*/
|
||||
public interface IdempotentService {
|
||||
public interface IdempotentNameGenerator {
|
||||
|
||||
/**
|
||||
* 检验是否存在
|
||||
* 生成幂等名称
|
||||
*
|
||||
* @param key 幂等key
|
||||
* @param timeout 超时时间
|
||||
* @return
|
||||
* @param target 目标实例
|
||||
* @param method 目标方法
|
||||
* @param args 方法参数
|
||||
* @return 幂等名称
|
||||
*/
|
||||
boolean checkAndLock(String key, long timeout);
|
||||
|
||||
/**
|
||||
* 释放对应key
|
||||
*
|
||||
* @param key 幂等key
|
||||
*/
|
||||
void unlock(String key);
|
||||
String generate(Object target, Method method, Object... args);
|
||||
}
|
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.idempotent.service.impl;
|
||||
|
||||
import top.continew.starter.idempotent.service.IdempotentService;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 内存实现幂等
|
||||
*
|
||||
* @version 1.0
|
||||
* @Author loach
|
||||
* @Date 2025-03-07 19:49
|
||||
* @Package top.continew.starter.idempotent.service.impl.MemoryIdempotentServiceImpl
|
||||
*/
|
||||
public class MemoryIdempotentServiceImpl implements IdempotentService {
|
||||
|
||||
private final ConcurrentHashMap<String, Long> lockMap = new ConcurrentHashMap<>();
|
||||
private final long cleanInterval;
|
||||
|
||||
public MemoryIdempotentServiceImpl(long cleanInterval) {
|
||||
this.cleanInterval = cleanInterval;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkAndLock(String key, long timeout) {
|
||||
if (key == null || key.isEmpty()) {
|
||||
throw new IllegalArgumentException("Key cannot be null or empty");
|
||||
}
|
||||
|
||||
long currentTime = System.currentTimeMillis();
|
||||
Long existingExpireTime = lockMap.get(key);
|
||||
|
||||
// 如果 key 已存在且未过期,返回 false
|
||||
if (existingExpireTime != null && currentTime < existingExpireTime) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 设置新的过期时间并放入 map
|
||||
long effectiveTimeout = timeout > 0 ? timeout : cleanInterval;
|
||||
long newExpireTime = currentTime + TimeUnit.SECONDS.toMillis(effectiveTimeout);
|
||||
lockMap.put(key, newExpireTime);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unlock(String key) {
|
||||
if (key != null && !key.isEmpty()) {
|
||||
lockMap.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
// 清理过期 key 的方法
|
||||
public void cleanExpiredKeys() {
|
||||
long currentTime = System.currentTimeMillis();
|
||||
lockMap.entrySet().removeIf(entry -> currentTime >= entry.getValue());
|
||||
}
|
||||
}
|
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.idempotent.service.impl;
|
||||
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import top.continew.starter.idempotent.service.IdempotentService;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* redis实现幂等
|
||||
*
|
||||
* @version 1.0
|
||||
* @Author loach
|
||||
* @Date 2025-03-07 19:49
|
||||
* @Package top.continew.starter.idempotent.service.impl.RedisIdempotentServiceImpl
|
||||
*/
|
||||
|
||||
public class RedisIdempotentServiceImpl implements IdempotentService {
|
||||
|
||||
private final StringRedisTemplate redisTemplate;
|
||||
private final long redisTimeout;
|
||||
|
||||
public RedisIdempotentServiceImpl(StringRedisTemplate redisTemplate, long redisTimeout) {
|
||||
this.redisTemplate = redisTemplate;
|
||||
this.redisTimeout = redisTimeout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkAndLock(String key, long timeout) {
|
||||
long effectiveTimeout = timeout > 0 ? timeout : redisTimeout;
|
||||
Boolean success = redisTemplate.opsForValue().setIfAbsent(key, "1", effectiveTimeout, TimeUnit.SECONDS);
|
||||
return success != null && success;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unlock(String key) {
|
||||
redisTemplate.delete(key);
|
||||
}
|
||||
}
|
@@ -1,57 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.idempotent.util;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.springframework.util.DigestUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 定义幂等key工具类
|
||||
*
|
||||
* @version 1.0
|
||||
* @Author loach
|
||||
* @Date 2025-03-07 20:13
|
||||
* @Package top.continew.starter.idempotent.util.IdempotentKeyGenerator
|
||||
*/
|
||||
public class IdempotentKeyGenerator {
|
||||
|
||||
/**
|
||||
* 创建key
|
||||
*
|
||||
* @param prefix 幂等key前缀
|
||||
* @param joinPoint 切面参数
|
||||
* @param request 请求头
|
||||
* @return
|
||||
*/
|
||||
public static String generateKey(String prefix, JoinPoint joinPoint, HttpServletRequest request) {
|
||||
// 可选使用header中的token
|
||||
String token = request.getHeader("Authorization");
|
||||
if (token == null || token.isEmpty()) {
|
||||
token = request.getParameter("Authorization");
|
||||
}
|
||||
|
||||
String methodSignature = joinPoint.getSignature().toString();
|
||||
String argsStr = Arrays.toString(joinPoint.getArgs());
|
||||
|
||||
// 如果没有token,只使用方法签名和参数生成key
|
||||
String rawKey = prefix + methodSignature + argsStr + (token != null ? token : "");
|
||||
return DigestUtils.md5DigestAsHex(rawKey.getBytes());
|
||||
}
|
||||
}
|
@@ -1,2 +1 @@
|
||||
top.continew.starter.idempotent.config.IdempotentAutoConfiguration
|
||||
top.continew.starter.idempotent.config.MemoryIdempotentSchedulingConfig
|
||||
top.continew.starter.idempotent.autoconfigure.IdempotentAutoConfiguration
|
Reference in New Issue
Block a user