Skip to content

Commit 7e558f1

Browse files
committed
Java:MultiDataSource 新增每日自动跑定时任务触发接口自动化回归测试并邮箱通知报告
1 parent f404187 commit 7e558f1

File tree

6 files changed

+823
-1
lines changed

6 files changed

+823
-1
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@
1212
APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/resources/static/cv/js/package-lock.json
1313
APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/resources/static/cv/js/package.json
1414
APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/resources/static/cv/package-lock.json
15+
APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/resources/static/api/package-lock.json

APIJSON-Java-Server/APIJSONBoot-MultiDataSource/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,11 @@
323323
<!-- </exclusion>-->
324324
</exclusions>
325325
</dependency>
326+
<dependency>
327+
<groupId>org.springframework.boot</groupId>
328+
<artifactId>spring-boot-starter-mail</artifactId>
329+
<version>3.2.5</version>
330+
</dependency>
326331

327332

328333
<!-- DemoController /delegate 代理转发请求 HTTP PATCH 方法需要,使用 PATCH 可以取消注释 -->
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
package apijson.boot;
2+
3+
import apijson.Log;
4+
import jakarta.annotation.PreDestroy;
5+
import org.springframework.beans.factory.annotation.Value;
6+
import org.springframework.boot.context.event.ApplicationReadyEvent;
7+
import org.springframework.context.event.EventListener;
8+
import org.springframework.http.HttpEntity;
9+
import org.springframework.http.HttpMethod;
10+
import org.springframework.http.ResponseEntity;
11+
import org.springframework.scheduling.annotation.Scheduled;
12+
import org.springframework.stereotype.Service;
13+
14+
import java.io.IOException;
15+
import java.nio.file.Files;
16+
import java.nio.file.Path;
17+
import java.nio.file.Paths;
18+
19+
@Service
20+
public class ApiAutoNodeProcessSupervisor {
21+
private static final String TAG = "ApiAutoNodeSupervisor";
22+
23+
private final Object lock = new Object();
24+
private volatile Process process;
25+
private volatile Path logFilePath;
26+
27+
@Value("${apijson.auto-test.enabled:true}")
28+
private boolean autoTestEnabled;
29+
@Value("${apijson.auto-test.node.enabled:true}")
30+
private boolean enabled;
31+
@Value("${apijson.auto-test.node.command:node}")
32+
private String nodeCommand;
33+
@Value("${apijson.auto-test.node.script:js/server.js}")
34+
private String nodeScript;
35+
@Value("${apijson.auto-test.node.working-directory:src/main/resources/static/api}")
36+
private String nodeWorkingDirectory;
37+
@Value("${apijson.auto-test.node.startup-timeout-millis:60000}")
38+
private long startupTimeoutMillis;
39+
@Value("${apijson.auto-test.status-url:http://localhost:3000/test/status}")
40+
private String statusUrl;
41+
42+
@EventListener(ApplicationReadyEvent.class)
43+
public void startWhenApplicationReady() {
44+
if (!autoTestEnabled || !enabled) {
45+
return;
46+
}
47+
try {
48+
ensureRunning();
49+
} catch (Throwable e) {
50+
e.printStackTrace();
51+
Log.e(TAG, "启动 APIAuto Node 守护进程失败: " + e.getMessage());
52+
}
53+
}
54+
55+
@Scheduled(
56+
fixedDelayString = "${apijson.auto-test.node.health-check-interval-millis:30000}",
57+
initialDelayString = "${apijson.auto-test.node.health-check-interval-millis:30000}"
58+
)
59+
public void keepAlive() {
60+
if (!autoTestEnabled || !enabled) {
61+
return;
62+
}
63+
try {
64+
ensureRunning();
65+
} catch (Throwable e) {
66+
e.printStackTrace();
67+
Log.e(TAG, "检查 APIAuto Node 守护进程失败: " + e.getMessage());
68+
}
69+
}
70+
71+
public void ensureRunning() {
72+
if (!autoTestEnabled || !enabled || isNodeServiceReachable()) {
73+
return;
74+
}
75+
76+
synchronized (lock) {
77+
if (isNodeServiceReachable()) {
78+
return;
79+
}
80+
81+
Process current = process;
82+
if (current != null && current.isAlive()) {
83+
waitUntilReachable();
84+
return;
85+
}
86+
87+
startProcess();
88+
waitUntilReachable();
89+
}
90+
}
91+
92+
public boolean isNodeServiceReachable() {
93+
try {
94+
ResponseEntity<String> response = DemoController.CLIENT.exchange(statusUrl, HttpMethod.GET, HttpEntity.EMPTY, String.class);
95+
return response.getStatusCode().is2xxSuccessful();
96+
} catch (Throwable e) {
97+
return false;
98+
}
99+
}
100+
101+
private void startProcess() {
102+
Path workingDirectory = resolveWorkingDirectory();
103+
if (!Files.isDirectory(workingDirectory)) {
104+
throw new IllegalStateException("Node 工作目录不存在: " + workingDirectory);
105+
}
106+
107+
Path scriptPath = workingDirectory.resolve(nodeScript).normalize();
108+
if (!Files.exists(scriptPath)) {
109+
throw new IllegalStateException("Node 启动脚本不存在: " + scriptPath);
110+
}
111+
112+
if (!Files.isDirectory(workingDirectory.resolve("node_modules"))) {
113+
Log.w(TAG, "Node 依赖目录不存在,若启动失败请先在 " + workingDirectory + " 下执行 npm install");
114+
}
115+
116+
logFilePath = prepareLogFile(workingDirectory);
117+
118+
ProcessBuilder builder = new ProcessBuilder(nodeCommand, nodeScript);
119+
builder.directory(workingDirectory.toFile());
120+
builder.redirectErrorStream(true);
121+
builder.redirectOutput(ProcessBuilder.Redirect.appendTo(logFilePath.toFile()));
122+
123+
try {
124+
process = builder.start();
125+
Log.d(TAG, "已启动 APIAuto Node 守护进程,pid = " + process.pid());
126+
} catch (IOException e) {
127+
throw new IllegalStateException("启动 Node 守护进程失败,请确认已安装 node 并可执行 `" + nodeCommand + " " + nodeScript + "`", e);
128+
}
129+
}
130+
131+
private void waitUntilReachable() {
132+
long deadline = System.currentTimeMillis() + Math.max(startupTimeoutMillis, 1000L);
133+
while (System.currentTimeMillis() < deadline) {
134+
if (isNodeServiceReachable()) {
135+
return;
136+
}
137+
138+
Process current = process;
139+
if (current != null && !current.isAlive()) {
140+
process = null;
141+
throw new IllegalStateException("Node 守护进程启动后立即退出,请检查日志: " + logFilePath);
142+
}
143+
144+
sleep(1000L);
145+
}
146+
147+
Process current = process;
148+
if (current != null && current.isAlive()) {
149+
current.destroy();
150+
}
151+
process = null;
152+
throw new IllegalStateException("等待 Node 守护进程就绪超时,请检查日志: " + logFilePath);
153+
}
154+
155+
private Path resolveWorkingDirectory() {
156+
Path workingDirectory = Paths.get(nodeWorkingDirectory);
157+
if (!workingDirectory.isAbsolute()) {
158+
workingDirectory = Paths.get(System.getProperty("user.dir", ".")).resolve(workingDirectory);
159+
}
160+
return workingDirectory.normalize();
161+
}
162+
163+
private Path prepareLogFile(Path workingDirectory) {
164+
try {
165+
Path logDirectory = workingDirectory.resolve("logs");
166+
Files.createDirectories(logDirectory);
167+
return logDirectory.resolve("api-auto-node.log");
168+
} catch (IOException e) {
169+
throw new IllegalStateException("创建 Node 日志目录失败: " + workingDirectory.resolve("logs"), e);
170+
}
171+
}
172+
173+
private void sleep(long millis) {
174+
try {
175+
Thread.sleep(millis);
176+
} catch (InterruptedException e) {
177+
Thread.currentThread().interrupt();
178+
throw new IllegalStateException("等待 Node 守护进程启动时被中断", e);
179+
}
180+
}
181+
182+
@PreDestroy
183+
public void stopManagedProcess() {
184+
synchronized (lock) {
185+
Process current = process;
186+
if (current != null && current.isAlive()) {
187+
current.destroy();
188+
}
189+
process = null;
190+
}
191+
}
192+
}

0 commit comments

Comments
 (0)