Skip to content

Commit 199df87

Browse files
AiMing317Charles7c
authored andcommitted
feat(idempotent): 新增幂等模块
实现内存/redis 模式幂等(默认内存模式)
1 parent 591a44d commit 199df87

File tree

12 files changed

+581
-0
lines changed

12 files changed

+581
-0
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
<parent>
7+
<groupId>top.continew</groupId>
8+
<artifactId>continew-starter</artifactId>
9+
<version>${revision}</version>
10+
</parent>
11+
12+
<artifactId>continew-starter-idempotent</artifactId>
13+
<description>ContiNew Starter 幂等模块</description>
14+
15+
<dependencies>
16+
17+
<!-- Spring Boot Starter(自动配置相关依赖) -->
18+
<dependency>
19+
<groupId>org.springframework.boot</groupId>
20+
<artifactId>spring-boot-starter</artifactId>
21+
</dependency>
22+
<dependency>
23+
<groupId>org.springframework.boot</groupId>
24+
<artifactId>spring-boot-configuration-processor</artifactId>
25+
</dependency>
26+
27+
<dependency>
28+
<groupId>org.springframework.boot</groupId>
29+
<artifactId>spring-boot-starter-aop</artifactId>
30+
</dependency>
31+
32+
<dependency>
33+
<groupId>org.springframework.boot</groupId>
34+
<artifactId>spring-boot-starter-data-redis</artifactId>
35+
<optional>true</optional>
36+
</dependency>
37+
38+
<dependency>
39+
<groupId>org.springframework.boot</groupId>
40+
<artifactId>spring-boot-starter-web</artifactId>
41+
</dependency>
42+
<dependency>
43+
<groupId>jakarta.servlet</groupId>
44+
<artifactId>jakarta.servlet-api</artifactId>
45+
</dependency>
46+
</dependencies>
47+
48+
<build>
49+
<plugins>
50+
<plugin>
51+
<groupId>org.springframework.boot</groupId>
52+
<artifactId>spring-boot-maven-plugin</artifactId>
53+
</plugin>
54+
</plugins>
55+
</build>
56+
57+
</project>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
3+
* <p>
4+
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* http://www.gnu.org/licenses/lgpl.html
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package top.continew.starter.idempotent.annotation;
18+
19+
import java.lang.annotation.*;
20+
import java.util.concurrent.TimeUnit;
21+
22+
/**
23+
* 定义幂等注解
24+
*
25+
* @version 1.0
26+
* @Author loach
27+
* @Date 2025-03-07 19:26
28+
* @Package top.continew.starter.idempotent.annotation.Idempotent
29+
*/
30+
@Target({ElementType.METHOD})
31+
@Retention(RetentionPolicy.RUNTIME)
32+
@Documented
33+
public @interface Idempotent {
34+
35+
// 幂等key前缀
36+
String prefix() default "idempotent:";
37+
38+
// key的过期时间
39+
long timeout() default 0;
40+
41+
// 时间单位
42+
TimeUnit timeUnit() default TimeUnit.SECONDS;
43+
44+
// 失败时的提示信息
45+
String message() default "请勿重复操作";
46+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
3+
* <p>
4+
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* http://www.gnu.org/licenses/lgpl.html
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package top.continew.starter.idempotent.aspect;
18+
19+
import jakarta.servlet.http.HttpServletRequest;
20+
import org.aspectj.lang.ProceedingJoinPoint;
21+
import org.aspectj.lang.annotation.Around;
22+
import org.aspectj.lang.annotation.Aspect;
23+
import org.springframework.web.context.request.RequestContextHolder;
24+
import org.springframework.web.context.request.ServletRequestAttributes;
25+
import top.continew.starter.idempotent.annotation.Idempotent;
26+
import top.continew.starter.idempotent.service.IdempotentService;
27+
import top.continew.starter.idempotent.util.IdempotentKeyGenerator;
28+
29+
/**
30+
* 注解切面
31+
*
32+
* @version 1.0
33+
* @Author loach
34+
* @Date 2025-03-07 19:27
35+
* @Package top.continew.starter.idempotent.aspect.IdempotentAspect
36+
*/
37+
@Aspect
38+
public class IdempotentAspect {
39+
40+
private final IdempotentService idempotentService;
41+
42+
public IdempotentAspect(IdempotentService idempotentService) {
43+
this.idempotentService = idempotentService;
44+
}
45+
46+
/**
47+
* AspectJ 环绕通知
48+
*
49+
* @param joinPoint 切点
50+
* @param idempotent 注解
51+
* @return
52+
* @throws Throwable
53+
*/
54+
@Around("@annotation(idempotent)")
55+
public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {
56+
HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes())
57+
.getRequest();
58+
String key = IdempotentKeyGenerator.generateKey(idempotent.prefix(), joinPoint, request);
59+
60+
try {
61+
if (!idempotentService.checkAndLock(key, idempotent.timeout())) {
62+
throw new RuntimeException(idempotent.message());
63+
}
64+
return joinPoint.proceed();
65+
} catch (Exception e) {
66+
throw new RuntimeException(e);
67+
} finally {
68+
idempotentService.unlock(key);
69+
}
70+
}
71+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
3+
* <p>
4+
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* http://www.gnu.org/licenses/lgpl.html
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package top.continew.starter.idempotent.config;
18+
19+
import org.springframework.beans.factory.annotation.Autowired;
20+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
21+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
22+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
23+
import org.springframework.context.annotation.Bean;
24+
import org.springframework.context.annotation.Configuration;
25+
import org.springframework.data.redis.core.StringRedisTemplate;
26+
import top.continew.starter.idempotent.aspect.IdempotentAspect;
27+
import top.continew.starter.idempotent.service.IdempotentService;
28+
import top.continew.starter.idempotent.service.impl.MemoryIdempotentServiceImpl;
29+
import top.continew.starter.idempotent.service.impl.RedisIdempotentServiceImpl;
30+
31+
/**
32+
* 引用配置:暂定默认内存实现,扫描到启用redis 使用redis实现
33+
*
34+
* @version 1.0
35+
* @Author loach
36+
* @Date 2025-03-07 20:03
37+
* @Package top.continew.starter.idempotent.config.IdempotentAutoConfiguration
38+
*/
39+
@Configuration
40+
@EnableConfigurationProperties(IdempotentProperties.class)
41+
public class IdempotentAutoConfiguration {
42+
43+
@Autowired
44+
private IdempotentService idempotentService;
45+
46+
private final IdempotentProperties properties;
47+
48+
public IdempotentAutoConfiguration(IdempotentProperties properties) {
49+
this.properties = properties;
50+
}
51+
52+
@Bean
53+
public IdempotentAspect idempotentAspect(IdempotentService idempotentService) {
54+
return new IdempotentAspect(idempotentService);
55+
}
56+
57+
@Bean(name = "redisIdempotentService")
58+
@ConditionalOnBean(StringRedisTemplate.class)
59+
public IdempotentService redisIdempotentService(StringRedisTemplate redisTemplate) {
60+
return new RedisIdempotentServiceImpl(redisTemplate, properties.getRedisTimeout());
61+
}
62+
63+
@Bean
64+
@ConditionalOnMissingBean(IdempotentService.class)
65+
public IdempotentService memoryIdempotentService() {
66+
return new MemoryIdempotentServiceImpl(properties.getCleanInterval());
67+
}
68+
69+
@Bean
70+
public IdempotentProperties idempotentProperties() {
71+
return new IdempotentProperties();
72+
}
73+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
3+
* <p>
4+
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* http://www.gnu.org/licenses/lgpl.html
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package top.continew.starter.idempotent.config;
18+
19+
import org.springframework.boot.context.properties.ConfigurationProperties;
20+
21+
/**
22+
* 属性配置类
23+
*
24+
* @version 1.0
25+
* @Author loach
26+
* @Date 2025-03-07 19:27
27+
* @Package top.continew.starter.idempotent.config.IdempotentProperties
28+
*/
29+
@ConfigurationProperties(prefix = "idempotent")
30+
public class IdempotentProperties {
31+
32+
// 内存实现清理过期 key 的间隔(毫秒)默认5分钟
33+
private long cleanInterval = 300000;
34+
35+
// redis实现过期 key 的间隔(毫秒)默认5分钟
36+
private long redisTimeout = 300000;
37+
38+
public long getCleanInterval() {
39+
return cleanInterval;
40+
}
41+
42+
public void setCleanInterval(long cleanInterval) {
43+
this.cleanInterval = cleanInterval;
44+
}
45+
46+
public long getRedisTimeout() {
47+
return redisTimeout;
48+
}
49+
50+
public void setRedisTimeout(long redisTimeout) {
51+
this.redisTimeout = redisTimeout;
52+
}
53+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
3+
* <p>
4+
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* http://www.gnu.org/licenses/lgpl.html
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package top.continew.starter.idempotent.config;
18+
19+
import org.springframework.beans.factory.annotation.Autowired;
20+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
21+
import org.springframework.context.annotation.Configuration;
22+
import org.springframework.scheduling.annotation.EnableScheduling;
23+
import org.springframework.scheduling.annotation.Scheduled;
24+
import top.continew.starter.idempotent.service.IdempotentService;
25+
import top.continew.starter.idempotent.service.impl.MemoryIdempotentServiceImpl;
26+
27+
/**
28+
* 内存定时任务配置类
29+
*
30+
* @version 1.0
31+
* @Author loach
32+
* @Date 2025-03-07 22:26
33+
* @Package top.continew.starter.idempotent.config.MemoryIdempotentSchedulingConfig
34+
*/
35+
@Configuration
36+
@ConditionalOnMissingBean(name = "redisIdempotentService") // 当 Redis Bean 不存在时启用
37+
@EnableScheduling
38+
public class MemoryIdempotentSchedulingConfig {
39+
40+
@Autowired
41+
private IdempotentService idempotentService;
42+
43+
@Scheduled(fixedRateString = "${idempotent.clean-interval:60000}")
44+
public void cleanExpiredKeys() {
45+
if (idempotentService instanceof MemoryIdempotentServiceImpl) {
46+
((MemoryIdempotentServiceImpl)idempotentService).cleanExpiredKeys();
47+
}
48+
}
49+
}

0 commit comments

Comments
 (0)