mirror of
https://github.com/continew-org/continew-starter.git
synced 2025-09-10 19:03:08 +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 TENANT = CONTINEW_STARTER + StringConstants.DOT + "tenant";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 幂等配置
|
||||||
|
*/
|
||||||
|
public static final String IDEMPOTENT = "idempotent";
|
||||||
|
|
||||||
private PropertiesConstants() {
|
private PropertiesConstants() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -13,45 +13,15 @@
|
|||||||
<description>ContiNew Starter 幂等模块</description>
|
<description>ContiNew Starter 幂等模块</description>
|
||||||
|
|
||||||
<dependencies>
|
<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>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-aop</artifactId>
|
<artifactId>spring-boot-starter-aop</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 缓存模块 - Redisson -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>top.continew</groupId>
|
||||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
<artifactId>continew-starter-cache-redisson</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>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
|
||||||
<plugins>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
|
||||||
</plugin>
|
|
||||||
</plugins>
|
|
||||||
</build>
|
|
||||||
|
|
||||||
</project>
|
</project>
|
@@ -20,27 +20,39 @@ import java.lang.annotation.*;
|
|||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 定义幂等注解
|
* 幂等注解
|
||||||
*
|
*
|
||||||
* @version 1.0
|
* @author loach
|
||||||
* @Author loach
|
* @author Charles7c
|
||||||
* @Date 2025-03-07 19:26
|
* @since 2.10.0
|
||||||
* @Package top.continew.starter.idempotent.annotation.Idempotent
|
|
||||||
*/
|
*/
|
||||||
|
@Documented
|
||||||
@Target({ElementType.METHOD})
|
@Target({ElementType.METHOD})
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Documented
|
|
||||||
public @interface Idempotent {
|
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 "请勿重复操作";
|
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.
|
* 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
|
||||||
* @Author loach
|
* @author Charles7c
|
||||||
* @Date 2025-03-07 19:48
|
* @since 2.10.0
|
||||||
* @Package top.continew.starter.idempotent.service.IdempotentService
|
|
||||||
*/
|
*/
|
||||||
public interface IdempotentService {
|
public interface IdempotentNameGenerator {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检验是否存在
|
* 生成幂等名称
|
||||||
*
|
*
|
||||||
* @param key 幂等key
|
* @param target 目标实例
|
||||||
* @param timeout 超时时间
|
* @param method 目标方法
|
||||||
* @return
|
* @param args 方法参数
|
||||||
|
* @return 幂等名称
|
||||||
*/
|
*/
|
||||||
boolean checkAndLock(String key, long timeout);
|
String generate(Object target, Method method, Object... args);
|
||||||
|
|
||||||
/**
|
|
||||||
* 释放对应key
|
|
||||||
*
|
|
||||||
* @param key 幂等key
|
|
||||||
*/
|
|
||||||
void unlock(String key);
|
|
||||||
}
|
}
|
@@ -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.autoconfigure.IdempotentAutoConfiguration
|
||||||
top.continew.starter.idempotent.config.MemoryIdempotentSchedulingConfig
|
|
Reference in New Issue
Block a user