diff --git a/CLAUDE.md b/CLAUDE.md
index 551e1950..55d2c51b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## 项目概述
-springboot-framework 是一个基于 Spring Boot 3.3.5(Java 17)的多模块企业级开发框架,由 CodingApi 团队维护。核心定位是为领域驱动设计(DDD)与事件风暴提供开箱即用的基础设施,当前版本为 `3.4.54`。
+springboot-framework 是一个基于 Spring Boot 3.3.5(Java 17)的多模块企业级开发框架,由 CodingApi 团队维护。核心定位是为领域驱动设计(DDD)与事件风暴提供开箱即用的基础设施,当前版本为 `3.4.55`。
**回复语言**:请使用中文进行回答。
diff --git a/example/example-app/example-app-cmd-domain/pom.xml b/example/example-app/example-app-cmd-domain/pom.xml
index fb233989..6fd7afd2 100644
--- a/example/example-app/example-app-cmd-domain/pom.xml
+++ b/example/example-app/example-app-cmd-domain/pom.xml
@@ -6,7 +6,7 @@
com.codingapi.springboot
example-app
- 3.4.54
+ 3.4.55
../pom.xml
diff --git a/example/example-app/example-app-cmd-meta/pom.xml b/example/example-app/example-app-cmd-meta/pom.xml
index 9ab44d9a..dc6b15fc 100644
--- a/example/example-app/example-app-cmd-meta/pom.xml
+++ b/example/example-app/example-app-cmd-meta/pom.xml
@@ -6,7 +6,7 @@
com.codingapi.springboot
example-app
- 3.4.54
+ 3.4.55
../pom.xml
diff --git a/example/example-app/example-app-query/pom.xml b/example/example-app/example-app-query/pom.xml
index f8f02e4d..982facb8 100644
--- a/example/example-app/example-app-query/pom.xml
+++ b/example/example-app/example-app-query/pom.xml
@@ -6,7 +6,7 @@
com.codingapi.springboot
example-app
- 3.4.54
+ 3.4.55
../pom.xml
diff --git a/example/example-app/pom.xml b/example/example-app/pom.xml
index 95f11dce..111b8a50 100644
--- a/example/example-app/pom.xml
+++ b/example/example-app/pom.xml
@@ -6,7 +6,7 @@
com.codingapi.springboot
springboot-example
- 3.4.54
+ 3.4.55
../pom.xml
pom
diff --git a/example/example-domain/example-domain-leave/pom.xml b/example/example-domain/example-domain-leave/pom.xml
index f5c1734e..6717974d 100644
--- a/example/example-domain/example-domain-leave/pom.xml
+++ b/example/example-domain/example-domain-leave/pom.xml
@@ -6,7 +6,7 @@
com.codingapi.springboot
example-domain
- 3.4.54
+ 3.4.55
../pom.xml
diff --git a/example/example-domain/example-domain-user/pom.xml b/example/example-domain/example-domain-user/pom.xml
index 2477be54..cd68cdab 100644
--- a/example/example-domain/example-domain-user/pom.xml
+++ b/example/example-domain/example-domain-user/pom.xml
@@ -6,7 +6,7 @@
com.codingapi.springboot
example-domain
- 3.4.54
+ 3.4.55
../pom.xml
diff --git a/example/example-domain/pom.xml b/example/example-domain/pom.xml
index c47712d3..085db923 100644
--- a/example/example-domain/pom.xml
+++ b/example/example-domain/pom.xml
@@ -5,7 +5,7 @@
com.codingapi.springboot
springboot-example
- 3.4.54
+ 3.4.55
../pom.xml
4.0.0
diff --git a/example/example-infra/example-infra-flow/pom.xml b/example/example-infra/example-infra-flow/pom.xml
index da43affa..6ca6db43 100644
--- a/example/example-infra/example-infra-flow/pom.xml
+++ b/example/example-infra/example-infra-flow/pom.xml
@@ -5,7 +5,7 @@
com.codingapi.springboot
example-infra
- 3.4.54
+ 3.4.55
../pom.xml
diff --git a/example/example-infra/example-infra-jpa/pom.xml b/example/example-infra/example-infra-jpa/pom.xml
index f5f1c4e2..3b0fe0c4 100644
--- a/example/example-infra/example-infra-jpa/pom.xml
+++ b/example/example-infra/example-infra-jpa/pom.xml
@@ -5,7 +5,7 @@
com.codingapi.springboot
example-infra
- 3.4.54
+ 3.4.55
../pom.xml
diff --git a/example/example-infra/example-infra-security/pom.xml b/example/example-infra/example-infra-security/pom.xml
index d0fdab13..6d13113e 100644
--- a/example/example-infra/example-infra-security/pom.xml
+++ b/example/example-infra/example-infra-security/pom.xml
@@ -6,7 +6,7 @@
com.codingapi.springboot
example-infra
- 3.4.54
+ 3.4.55
../pom.xml
diff --git a/example/example-infra/pom.xml b/example/example-infra/pom.xml
index 637e53ca..138f14a8 100644
--- a/example/example-infra/pom.xml
+++ b/example/example-infra/pom.xml
@@ -6,7 +6,7 @@
com.codingapi.springboot
springboot-example
- 3.4.54
+ 3.4.55
../pom.xml
pom
diff --git a/example/example-interface/pom.xml b/example/example-interface/pom.xml
index 5eafea99..9c99fbb0 100644
--- a/example/example-interface/pom.xml
+++ b/example/example-interface/pom.xml
@@ -6,7 +6,7 @@
com.codingapi.springboot
springboot-example
- 3.4.54
+ 3.4.55
example-interface
diff --git a/example/example-server/pom.xml b/example/example-server/pom.xml
index b5c8954c..73e58ece 100644
--- a/example/example-server/pom.xml
+++ b/example/example-server/pom.xml
@@ -5,7 +5,7 @@
springboot-example
com.codingapi.springboot
- 3.4.54
+ 3.4.55
4.0.0
diff --git a/example/pom.xml b/example/pom.xml
index f1281550..ea1257c2 100644
--- a/example/pom.xml
+++ b/example/pom.xml
@@ -14,12 +14,12 @@
com.codingapi.springboot
springboot-parent
- 3.3.5
+ 3.4.55
../pom.xml
springboot-example
- 3.4.54
+ 3.4.55
springboot-example
springboot-example project for Spring Boot
diff --git a/pom.xml b/pom.xml
index 82a3b015..a2d03bc9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -12,7 +12,7 @@
com.codingapi.springboot
springboot-parent
- 3.4.54
+ 3.4.55
https://github.com/codingapi/springboot-framewrok
springboot-parent
diff --git a/springboot-starter-data-authorization/pom.xml b/springboot-starter-data-authorization/pom.xml
index d673b106..97370691 100644
--- a/springboot-starter-data-authorization/pom.xml
+++ b/springboot-starter-data-authorization/pom.xml
@@ -6,7 +6,7 @@
com.codingapi.springboot
springboot-parent
- 3.4.54
+ 3.4.55
springboot-starter-data-authorization
diff --git a/springboot-starter-data-fast/pom.xml b/springboot-starter-data-fast/pom.xml
index e1e9666d..e94c4e69 100644
--- a/springboot-starter-data-fast/pom.xml
+++ b/springboot-starter-data-fast/pom.xml
@@ -5,7 +5,7 @@
springboot-parent
com.codingapi.springboot
- 3.4.54
+ 3.4.55
4.0.0
diff --git a/springboot-starter-flow/pom.xml b/springboot-starter-flow/pom.xml
index ba4a583f..0b58df48 100644
--- a/springboot-starter-flow/pom.xml
+++ b/springboot-starter-flow/pom.xml
@@ -6,7 +6,7 @@
springboot-parent
com.codingapi.springboot
- 3.4.54
+ 3.4.55
springboot-starter-flow
diff --git a/springboot-starter-script/pom.xml b/springboot-starter-script/pom.xml
index d9c4853b..07954d4e 100644
--- a/springboot-starter-script/pom.xml
+++ b/springboot-starter-script/pom.xml
@@ -6,7 +6,7 @@
com.codingapi.springboot
springboot-parent
- 3.4.54
+ 3.4.55
springboot-starter-script
diff --git a/springboot-starter-script/src/main/java/com/codingapi/springboot/script/temp/TempGroovyScriptContext.java b/springboot-starter-script/src/main/java/com/codingapi/springboot/script/temp/TempGroovyScriptContext.java
index 4cad69aa..e12ca04c 100644
--- a/springboot-starter-script/src/main/java/com/codingapi/springboot/script/temp/TempGroovyScriptContext.java
+++ b/springboot-starter-script/src/main/java/com/codingapi/springboot/script/temp/TempGroovyScriptContext.java
@@ -5,7 +5,13 @@
import com.codingapi.springboot.script.repository.TempGroovyScriptRepositoryContext;
import lombok.Getter;
-import java.util.*;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
/**
* 临时脚本数据上下文管理对象
@@ -15,10 +21,20 @@ public class TempGroovyScriptContext {
@Getter
private final static TempGroovyScriptContext instance = new TempGroovyScriptContext();
+ /**
+ * 共享的清理调度线程(daemon)
+ * 所有临时脚本共用一个调度线程,避免每个脚本创建原生线程导致线程泄漏
+ */
+ private final static ScheduledExecutorService SCHEDULER = Executors.newSingleThreadScheduledExecutor(r -> {
+ Thread thread = new Thread(r, "temp-groovy-script-clear");
+ thread.setDaemon(true);
+ return thread;
+ });
+
private final Map cache;
private TempGroovyScriptContext() {
- this.cache = new HashMap<>();
+ this.cache = new ConcurrentHashMap<>();
}
private static class ClearJob {
@@ -26,30 +42,37 @@ private static class ClearJob {
@Getter
private final TempGroovyScript tempGroovyScript;
- private final Timer timer;
+ private final ScheduledFuture> future;
public ClearJob(TempGroovyScript groovyScript) {
this.tempGroovyScript = groovyScript;
- this.timer = new Timer();
- this.initTimer();
+ // clearTime 为绝对到期时间戳,需换算为相对延迟
+ long delay = groovyScript.getClearTime() - System.currentTimeMillis();
+ this.future = SCHEDULER.schedule(this::expire, delay, TimeUnit.MILLISECONDS);
}
public GroovyScript getGroovyScript() {
return this.tempGroovyScript.getGroovyScript();
}
- private void initTimer() {
- this.timer.schedule(new TimerTask() {
- @Override
- public void run() {
- TempGroovyScriptContext.getInstance().remove(getKey());
- }
- }, tempGroovyScript.getClearTime());
- }
-
public String getKey() {
return tempGroovyScript.getKey();
}
+
+ /**
+ * 到期清理
+ * 仅当缓存中仍是当前任务时才删除,避免过期任务误删已被刷新的脚本
+ */
+ private void expire() {
+ TempGroovyScriptContext.getInstance().removeIfCurrent(getKey(), this);
+ }
+
+ /**
+ * 取消定时清理任务
+ */
+ private void cancel() {
+ this.future.cancel(false);
+ }
}
/**
@@ -60,7 +83,17 @@ public String getKey() {
public void save(GroovyScript script) {
if (script != null) {
long tempValidTime = PropertiesContext.getInstance().getTempValidTime();
- this.cache.put(script.getKey(), new ClearJob(new TempGroovyScript(script, tempValidTime + System.currentTimeMillis())));
+ this.put(script.getKey(), new ClearJob(new TempGroovyScript(script, tempValidTime + System.currentTimeMillis())));
+ }
+ }
+
+ /**
+ * 写入缓存,覆盖时取消旧任务的定时清理
+ */
+ private void put(String key, ClearJob job) {
+ ClearJob previous = this.cache.put(key, job);
+ if (previous != null) {
+ previous.cancel();
}
}
@@ -73,7 +106,7 @@ public void loadAll(List groovyScripts) {
if (groovyScript.isExpired()) {
this.remove(groovyScript.getKey());
} else {
- this.cache.put(groovyScript.getKey(), new ClearJob(groovyScript));
+ this.put(groovyScript.getKey(), new ClearJob(groovyScript));
}
}
}
@@ -93,10 +126,23 @@ public List findAll() {
* @param key 脚本key
*/
public void remove(String key) {
- this.cache.remove(key);
+ ClearJob job = this.cache.remove(key);
+ if (job != null) {
+ job.cancel();
+ }
TempGroovyScriptRepositoryContext.getInstance().delete(key);
}
+ /**
+ * 仅当缓存中的任务仍为当前任务时删除,防止过期任务误删已被刷新的脚本。
+ * 使用 ConcurrentHashMap.remove(key, value) 原子判断 + 删除,避免 check-then-act 竞态
+ */
+ private void removeIfCurrent(String key, ClearJob job) {
+ if (this.cache.remove(key, job)) {
+ job.cancel();
+ TempGroovyScriptRepositoryContext.getInstance().delete(key);
+ }
+ }
/**
* 脚本总数量
@@ -118,7 +164,11 @@ public GroovyScript getGroovyScript(String key) {
if (job == null) {
TempGroovyScript groovyScript = TempGroovyScriptRepositoryContext.getInstance().get(key);
if (groovyScript != null) {
- this.cache.put(key, new ClearJob(groovyScript));
+ if (groovyScript.isExpired()) {
+ this.remove(key);
+ return null;
+ }
+ this.put(key, new ClearJob(groovyScript));
return groovyScript.getGroovyScript();
}
return null;
@@ -131,6 +181,9 @@ public GroovyScript getGroovyScript(String key) {
* 清空脚本数据
*/
public void clear() {
+ for (ClearJob job : this.cache.values()) {
+ job.cancel();
+ }
this.cache.clear();
}
}
diff --git a/springboot-starter-script/src/test/java/com/codingapi/springboot/script/temp/TempGroovyScriptContextTest.java b/springboot-starter-script/src/test/java/com/codingapi/springboot/script/temp/TempGroovyScriptContextTest.java
new file mode 100644
index 00000000..d7fbd7c9
--- /dev/null
+++ b/springboot-starter-script/src/test/java/com/codingapi/springboot/script/temp/TempGroovyScriptContextTest.java
@@ -0,0 +1,84 @@
+package com.codingapi.springboot.script.temp;
+
+import com.codingapi.springboot.script.GroovyScript;
+import com.codingapi.springboot.script.properties.GroovyScriptProperties;
+import com.codingapi.springboot.script.properties.PropertiesContext;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * 回归测试:临时脚本不得为每个脚本创建原生线程(OOM 根因),
+ * 且脚本到期后应被自动清理。
+ */
+class TempGroovyScriptContextTest {
+
+ private TempGroovyScriptContext context;
+
+ @BeforeEach
+ void setUp() {
+ context = TempGroovyScriptContext.getInstance();
+ // 清理上一用例残留的定时任务
+ context.clear();
+ // 预热共享调度线程,避免首个任务创建线程影响基线统计
+ context.save(script("__warmup__"));
+ context.remove("__warmup__");
+ }
+
+ @AfterEach
+ void tearDown() {
+ // 清理定时任务并恢复默认配置,避免泄漏到同 JVM 的其他测试
+ context.clear();
+ PropertiesContext.getInstance().setProperties(new GroovyScriptProperties());
+ }
+
+ private GroovyScript script(String key) {
+ return GroovyScript.builder(key).script("return 1;").build();
+ }
+
+ @Test
+ void saveManyScriptsShouldNotCreateThreads() {
+ int threadsBefore = Thread.getAllStackTraces().size();
+
+ for (int i = 0; i < 200; i++) {
+ context.save(script("key-" + i));
+ }
+
+ assertEquals(200, context.count());
+ assertEquals(threadsBefore, Thread.getAllStackTraces().size(),
+ "注册 200 个临时脚本不应创建任何新线程");
+ }
+
+ @Test
+ void overwriteRemoveClearShouldNotCreateThreads() {
+ int threadsBefore = Thread.getAllStackTraces().size();
+
+ for (int i = 0; i < 100; i++) {
+ context.save(script("key-" + i));
+ context.save(script("key-" + i)); // 覆盖刷新
+ }
+ for (int i = 0; i < 100; i++) {
+ context.remove("key-" + i);
+ }
+ context.clear();
+
+ assertEquals(0, context.count());
+ assertEquals(threadsBefore, Thread.getAllStackTraces().size(),
+ "覆盖/删除/清空不应创建新线程");
+ }
+
+ @Test
+ void expiredScriptShouldBeRemovedAutomatically() throws InterruptedException {
+ GroovyScriptProperties properties = new GroovyScriptProperties();
+ properties.setTempValidTime(300);
+ PropertiesContext.getInstance().setProperties(properties);
+
+ context.save(script("expire-key"));
+ assertEquals(1, context.count());
+
+ Thread.sleep(1500);
+ assertEquals(0, context.count(), "脚本到期后应被自动清理");
+ }
+}
\ No newline at end of file
diff --git a/springboot-starter-security/pom.xml b/springboot-starter-security/pom.xml
index ba60d6d0..596f4df7 100644
--- a/springboot-starter-security/pom.xml
+++ b/springboot-starter-security/pom.xml
@@ -6,7 +6,7 @@
springboot-parent
com.codingapi.springboot
- 3.4.54
+ 3.4.55
springboot-starter-security
diff --git a/springboot-starter/pom.xml b/springboot-starter/pom.xml
index 8e3937d6..4aed573e 100644
--- a/springboot-starter/pom.xml
+++ b/springboot-starter/pom.xml
@@ -5,7 +5,7 @@
com.codingapi.springboot
springboot-parent
- 3.4.54
+ 3.4.55
springboot-starter