fix(web): harden cache and API services

This commit is contained in:
yukaidi
2026-06-10 21:21:52 +08:00
parent 0103841fb5
commit cbb442ceb6
14 changed files with 935 additions and 315 deletions
@@ -3,9 +3,11 @@ package cn.qaiu.lz;
import cn.qaiu.WebClientVertxInit;
import cn.qaiu.db.pool.JDBCPoolInit;
import cn.qaiu.lz.common.cache.CacheConfigLoader;
import cn.qaiu.lz.common.cache.CacheManager;
import cn.qaiu.lz.common.interceptorImpl.RateLimiter;
import cn.qaiu.lz.web.config.PlaygroundConfig;
import cn.qaiu.lz.web.service.DbService;
import cn.qaiu.lz.web.service.impl.ShoutServiceImpl;
import cn.qaiu.parser.custom.CustomParserConfig;
import cn.qaiu.parser.custom.CustomParserRegistry;
import cn.qaiu.parser.customjs.JsScriptMetadataParser;
@@ -20,6 +22,7 @@ import io.vertx.core.json.jackson.DatabindCodec;
import io.vertx.core.shareddata.LocalMap;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.slf4j.LoggerFactory;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -38,22 +41,45 @@ import static cn.qaiu.vx.core.util.ConfigConstant.LOCAL;
public class AppMain {
public static void main(String[] args) {
// 先注册 ShutdownHook(JVM 逆序执行,先注册的后执行)
// 确保关闭顺序:Vert.x -> JDBCPoolInit -> JsParserExecutor
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
JDBCPoolInit.instance().close();
} catch (Exception e) {
// ignore
}
try {
cn.qaiu.parser.customjs.JsParserExecutor.shutdownExecutor();
} catch (Exception e) {
// ignore
}
}));
applyRuntimeLogLevelOverride();
Deploy deploy = Deploy.instance();
// 先阻断应用级定时任务,再让 Vert.x 停入口和 verticle。
deploy.addPreShutdownTask(CacheManager::cancelPeriodicCleanup);
deploy.addPreShutdownTask(ShoutServiceImpl::cancelCleanup);
// Vert.x 停完后再关数据库和解析器共享资源,避免请求还在路上就先关底层 client。
deploy.addPostShutdownTask(() -> JDBCPoolInit.instance().close());
deploy.addPostShutdownTask(cn.qaiu.parser.customjs.JsParserExecutor::shutdownExecutor);
deploy.addPostShutdownTask(cn.qaiu.parser.customjs.JsPlaygroundExecutor::shutdownPools);
deploy.addPostShutdownTask(cn.qaiu.parser.customjs.JsHttpClient::shutdownSharedClient);
deploy.addPostShutdownTask(cn.qaiu.parser.PanBase::shutdownSharedClients);
deploy.addPostShutdownTask(cn.qaiu.parser.IPanTool::shutdownCloseAfterScheduler);
deploy.addPostShutdownTask(cn.qaiu.parser.impl.PodTool::shutdownWorkerExecutor);
// start
Deploy.instance().start(args, AppMain::exec);
deploy.start(args, AppMain::exec);
}
private static void applyRuntimeLogLevelOverride() {
String levelName = System.getProperty("NFD_LOG_LEVEL");
if (levelName == null || levelName.isBlank()) {
levelName = System.getenv("NFD_LOG_LEVEL");
}
if (levelName == null || levelName.isBlank()) {
return;
}
try {
var level = ch.qos.logback.classic.Level.toLevel(levelName, null);
if (level == null) {
log.warn("忽略无效的 NFD_LOG_LEVEL: {}", levelName);
return;
}
var logger = LoggerFactory.getLogger("cn.qaiu");
if (logger instanceof ch.qos.logback.classic.Logger logbackLogger) {
logbackLogger.setLevel(level);
log.info("cn.qaiu 日志级别已覆盖为 {}", level);
}
} catch (Exception e) {
log.warn("覆盖 cn.qaiu 日志级别失败: {}", e.getMessage());
}
}
/**
@@ -65,6 +91,8 @@ public class AppMain {
private static void exec(JsonObject jsonObject) {
WebClientVertxInit.init(VertxHolder.getVertxInstance());
DatabindCodec.mapper().registerModule(new JavaTimeModule());
// 演练场配置要先加载,后续启动流程才能按开关决定是否注册动态解析器。
PlaygroundConfig.loadFromJson(jsonObject);
// 限流
if (jsonObject.containsKey("rateLimit")) {
JsonObject rateLimit = jsonObject.getJsonObject("rateLimit");
@@ -108,7 +136,12 @@ public class AppMain {
} catch (Exception e) {
log.warn("读取代理配置失败,使用默认页面地址: {}", e.getMessage());
}
loadPlaygroundParsers(pageAddr);
if (PlaygroundConfig.getInstance().isEnabled()) {
loadPlaygroundParsers(pageAddr);
} else {
log.info("演练场功能已禁用,跳过加载演练场解析器");
log.info("服务已启动,可通过 {} 访问页面", pageAddr);
}
System.out.println("启动成功: \n本地服务地址: " + addr);
});
});
@@ -142,9 +175,6 @@ public class AppMain {
JsonObject auths = jsonObject.getJsonObject(ConfigConstant.AUTHS);
localMap.put(ConfigConstant.AUTHS, auths);
}
// 演练场配置
PlaygroundConfig.loadFromJson(jsonObject);
}
/**
@@ -153,34 +183,31 @@ public class AppMain {
private static void loadPlaygroundParsers(String accessAddr) {
DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
dbService.getPlaygroundParserList().onSuccess(result -> {
dbService.getEnabledPlaygroundParsersForLoad().onSuccess(result -> {
JsonArray parsers = result.getJsonArray("data");
if (parsers != null) {
int loadedCount = 0;
for (int i = 0; i < parsers.size(); i++) {
JsonObject parser = parsers.getJsonObject(i);
// 只注册已启用的解析器
if (parser.getBoolean("enabled", false)) {
try {
String jsCode = parser.getString("jsCode");
if (jsCode == null || jsCode.trim().isEmpty()) {
log.error("加载演练场解析器失败: {} - JavaScript代码为空", parser.getString("name"));
continue;
}
CustomParserConfig config = JsScriptMetadataParser.parseScript(jsCode);
CustomParserRegistry.register(config);
loadedCount++;
log.info("已加载演练场解析器: {} ({})",
config.getDisplayName(), config.getType());
} catch (Exception e) {
String parserName = parser.getString("name");
String errorMsg = e.getMessage();
log.error("加载演练场解析器失败: {} - {}", parserName, errorMsg, e);
// 如果是require相关错误,提供更详细的提示
if (errorMsg != null && errorMsg.contains("require")) {
log.error("提示:演练场解析器不支持CommonJS模块系统(require),请确保代码使用ES5.1语法");
}
try {
String jsCode = parser.getString("jsCode");
if (jsCode == null || jsCode.trim().isEmpty()) {
log.error("加载演练场解析器失败: {} - JavaScript代码为空", parser.getString("name"));
continue;
}
CustomParserConfig config = JsScriptMetadataParser.parseScript(jsCode);
CustomParserRegistry.register(config);
loadedCount++;
log.info("已加载演练场解析器: {} ({})",
config.getDisplayName(), config.getType());
} catch (Exception e) {
String parserName = parser.getString("name");
String errorMsg = e.getMessage();
log.error("加载演练场解析器失败: {} - {}", parserName, errorMsg, e);
// 如果是require相关错误,提供更详细的提示
if (errorMsg != null && errorMsg.contains("require")) {
log.error("提示:演练场解析器不支持CommonJS模块系统(require),请确保代码使用ES5.1语法");
}
}
}