From 2e0127d60919b3e2006057a48e0c4c464b4ef6ea Mon Sep 17 00:00:00 2001 From: yukaidi Date: Thu, 28 May 2026 23:04:54 +0800 Subject: [PATCH 01/35] =?UTF-8?q?fix:=20=E6=B3=A8=E5=86=8C=20JVM=20Shutdow?= =?UTF-8?q?nHook=EF=BC=8C=E4=BF=AE=E5=A4=8D=20Vert.x=20=E5=AE=9E=E4=BE=8B?= =?UTF-8?q?=E8=BF=9B=E7=A8=8B=E9=80=80=E5=87=BA=E6=97=B6=E4=B8=8D=E5=85=B3?= =?UTF-8?q?=E9=97=AD=E7=9A=84=E8=B5=84=E6=BA=90=E6=B3=84=E6=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deploy.deployVerticle() 中创建的 Vert.x 实例是局部变量,进程退出时无法优雅关闭, 导致 Netty EventLoopGroup、JDBC 连接池、内部定时器等资源泄漏。 添加 ShutdownHook 在 JVM 关闭时调用 vertx.close() 级联释放所有资源。 --- core/src/main/java/cn/qaiu/vx/core/Deploy.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/src/main/java/cn/qaiu/vx/core/Deploy.java b/core/src/main/java/cn/qaiu/vx/core/Deploy.java index e6717be..0775c9f 100644 --- a/core/src/main/java/cn/qaiu/vx/core/Deploy.java +++ b/core/src/main/java/cn/qaiu/vx/core/Deploy.java @@ -43,6 +43,7 @@ public final class Deploy { private Handler handle; private Thread mainThread; + private Vertx mainVertx; public static Deploy instance() { return INSTANCE; @@ -137,6 +138,14 @@ public final class Deploy { vertxOptions.getWorkerPoolSize()); var vertx = Vertx.vertx(vertxOptions); VertxHolder.init(vertx); + this.mainVertx = vertx; + + // 注册 ShutdownHook,确保进程退出时优雅关闭资源 + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + LOGGER.info("JVM shutting down, closing Vert.x..."); + vertx.close().onComplete(ar -> + LOGGER.info("Vert.x closed: {}", ar.succeeded() ? "success" : ar.cause().getMessage())); + })); //配置保存在共享数据中 var sharedData = vertx.sharedData(); LocalMap localMap = sharedData.getLocalMap(LOCAL); From 74df000287632d8f3c7ebefcfd5097ef8c4f2519 Mon Sep 17 00:00:00 2001 From: yukaidi Date: Thu, 28 May 2026 23:06:45 +0800 Subject: [PATCH 02/35] =?UTF-8?q?fix:=20PanBase=20WebClient=20=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E9=9D=99=E6=80=81=E5=85=B1=E4=BA=AB=E5=8D=95=E4=BE=8B?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E5=A4=8D=E6=AF=8F=E8=AF=B7=E6=B1=82=E5=88=9B?= =?UTF-8?q?=E5=BB=BA4=E4=B8=AA=E5=AE=9E=E4=BE=8B=E7=9A=84=E8=B5=84?= =?UTF-8?q?=E6=BA=90=E6=B3=84=E6=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WebClient 是线程安全的,将 client/clientNoRedirects/clientDisableUA 改为 static 共享实例, 避免每次解析请求创建4个独立 WebClient(各含连接池)。 clientSession 仍保持实例级(管理 cookie,非线程安全)。 代理模式下仍创建独立 WebClient 实例。 --- .../src/main/java/cn/qaiu/parser/PanBase.java | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/parser/src/main/java/cn/qaiu/parser/PanBase.java b/parser/src/main/java/cn/qaiu/parser/PanBase.java index c1bc071..7ea50c4 100644 --- a/parser/src/main/java/cn/qaiu/parser/PanBase.java +++ b/parser/src/main/java/cn/qaiu/parser/PanBase.java @@ -40,28 +40,34 @@ public abstract class PanBase implements IPanTool { protected Promise promise = Promise.promise(); /** - * Http client + * 共享的 WebClient 实例(线程安全,避免每请求创建导致资源泄漏) */ - protected WebClient client = WebClient.create(WebClientVertxInit.get(), + private static final WebClient SHARED_CLIENT = WebClient.create(WebClientVertxInit.get(), new WebClientOptions()); + private static final WebClient SHARED_CLIENT_NO_REDIRECTS = WebClient.create(WebClientVertxInit.get(), + new WebClientOptions().setFollowRedirects(false)); + private static final WebClient SHARED_CLIENT_DISABLE_UA = WebClient.create(WebClientVertxInit.get(), + new WebClientOptions().setUserAgentEnabled(false)); /** - * Http client session (会话管理, 带cookie请求) + * Http client (默认使用共享实例,代理模式下使用独立实例) + */ + protected WebClient client = SHARED_CLIENT; + + /** + * Http client session (会话管理, 带cookie请求, 每实例独立) */ protected WebClientSession clientSession = WebClientSession.create(client); /** * Http client 不自动跳转 */ - protected WebClient clientNoRedirects = WebClient.create(WebClientVertxInit.get(), - new WebClientOptions().setFollowRedirects(false)); + protected WebClient clientNoRedirects = SHARED_CLIENT_NO_REDIRECTS; /** * Http client disable UserAgent */ - protected WebClient clientDisableUA = WebClient.create(WebClientVertxInit.get() - , new WebClientOptions().setUserAgentEnabled(false) - ); + protected WebClient clientDisableUA = SHARED_CLIENT_DISABLE_UA; protected ShareLinkInfo shareLinkInfo; From 7419e536cf62cfddd872ed437943728c2577d08f Mon Sep 17 00:00:00 2001 From: yukaidi Date: Thu, 28 May 2026 23:08:50 +0800 Subject: [PATCH 03/35] =?UTF-8?q?fix:=20JsExecUtils=20=E7=BC=93=E5=AD=98?= =?UTF-8?q?=20ScriptEngineManager=EF=BC=8C=E9=81=BF=E5=85=8D=E6=AF=8F?= =?UTF-8?q?=E6=AC=A1=E8=B0=83=E7=94=A8=E9=83=BD=E5=88=9B=E5=BB=BA=E6=96=B0?= =?UTF-8?q?=E5=AE=9E=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ScriptEngineManager 是重量级对象(含类加载器扫描等),将其缓存为 static 字段, executeDynamicJs/executeOtherJs 每次调用只创建轻量的 ScriptEngine 实例。 --- parser/src/main/java/cn/qaiu/util/JsExecUtils.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/parser/src/main/java/cn/qaiu/util/JsExecUtils.java b/parser/src/main/java/cn/qaiu/util/JsExecUtils.java index 62f8a9c..6ae420f 100644 --- a/parser/src/main/java/cn/qaiu/util/JsExecUtils.java +++ b/parser/src/main/java/cn/qaiu/util/JsExecUtils.java @@ -21,11 +21,11 @@ import static cn.qaiu.util.AESUtils.encrypt; */ public class JsExecUtils { private static final Invocable inv; + private static final ScriptEngineManager ENGINE_MANAGER = new ScriptEngineManager(); // 初始化脚本引擎 static { - ScriptEngineManager engineManager = new ScriptEngineManager(); - ScriptEngine engine = engineManager.getEngineByName("JavaScript"); // 得到脚本引擎 + ScriptEngine engine = ENGINE_MANAGER.getEngineByName("JavaScript"); // 得到脚本引擎 try { engine.eval(JsContent.ye123); @@ -45,12 +45,11 @@ public class JsExecUtils { } /** - * 调用执行蓝奏云js文件 + * 调用执行蓝奏云js文件(每次动态JS代码,无法复用引擎) */ public static ScriptObjectMirror executeDynamicJs(String jsText, String funName) throws ScriptException, NoSuchMethodException { - ScriptEngineManager engineManager = new ScriptEngineManager(); - ScriptEngine engine = engineManager.getEngineByName("JavaScript"); // 得到脚本引擎 + ScriptEngine engine = ENGINE_MANAGER.getEngineByName("JavaScript"); // 得到脚本引擎 engine.eval(JsContent.lz + "\n" + jsText); Invocable inv = (Invocable) engine; //调用js中的函数 @@ -63,12 +62,11 @@ public class JsExecUtils { /** - * 调用执行蓝奏云js文件 + * 调用执行js文件(复用已缓存的引擎实例,避免每次创建) */ public static Object executeOtherJs(String jsText, String funName, Object ... args) throws ScriptException, NoSuchMethodException { - ScriptEngineManager engineManager = new ScriptEngineManager(); - ScriptEngine engine = engineManager.getEngineByName("JavaScript"); // 得到脚本引擎 + ScriptEngine engine = ENGINE_MANAGER.getEngineByName("JavaScript"); // 得到脚本引擎 engine.eval(jsText); Invocable inv = (Invocable) engine; //调用js中的函数 From 255e7b2fb584094547a5819e8fe09252968482be Mon Sep 17 00:00:00 2001 From: yukaidi Date: Thu, 28 May 2026 23:13:09 +0800 Subject: [PATCH 04/35] =?UTF-8?q?fix:=20JsParserExecutor=20=E5=92=8C=20JsH?= =?UTF-8?q?ttpClient=20=E6=B7=BB=E5=8A=A0=E8=B5=84=E6=BA=90=E6=B8=85?= =?UTF-8?q?=E7=90=86=EF=BC=8C=E4=BF=AE=E5=A4=8D=E8=A7=A3=E6=9E=90=E5=AE=8C?= =?UTF-8?q?=E6=88=90=E5=90=8E=E8=B5=84=E6=BA=90=E6=B3=84=E6=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - JsHttpClient 添加 close() 方法释放 WebClient 连接池 - JsParserExecutor 添加 close() 方法,清除 ScriptEngine 中注入的 Java 对象引用 - parse()/parseFileList()/parseById() 均在 onComplete 回调中调用 close() 释放资源 --- .../cn/qaiu/parser/customjs/JsHttpClient.java | 11 +++++++++- .../parser/customjs/JsParserExecutor.java | 22 ++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/parser/src/main/java/cn/qaiu/parser/customjs/JsHttpClient.java b/parser/src/main/java/cn/qaiu/parser/customjs/JsHttpClient.java index 24dbeef..0442903 100644 --- a/parser/src/main/java/cn/qaiu/parser/customjs/JsHttpClient.java +++ b/parser/src/main/java/cn/qaiu/parser/customjs/JsHttpClient.java @@ -61,7 +61,7 @@ public class JsHttpClient { }; public JsHttpClient() { - this.client = WebClient.create(WebClientVertxInit.get(), new WebClientOptions());; + this.client = WebClient.create(WebClientVertxInit.get(), new WebClientOptions()); this.clientSession = WebClientSession.create(client); this.headers = MultiMap.caseInsensitiveMultiMap(); // 设置默认的Accept-Encoding头以支持压缩响应 @@ -677,4 +677,13 @@ public class JsHttpClient { return buffer.length(); } } + + /** + * 关闭 WebClient 释放连接池资源 + */ + public void close() { + if (client != null) { + client.close(); + } + } } diff --git a/parser/src/main/java/cn/qaiu/parser/customjs/JsParserExecutor.java b/parser/src/main/java/cn/qaiu/parser/customjs/JsParserExecutor.java index 6b81116..9414df7 100644 --- a/parser/src/main/java/cn/qaiu/parser/customjs/JsParserExecutor.java +++ b/parser/src/main/java/cn/qaiu/parser/customjs/JsParserExecutor.java @@ -146,6 +146,22 @@ public class JsParserExecutor implements IPanTool { } } + /** + * 释放资源(ScriptEngine 和 HttpClient),避免内存泄漏 + */ + public void close() { + if (httpClient != null) { + httpClient.close(); + } + // 清除 ScriptEngine 持有的 Java 对象引用,帮助 GC 回收 + if (engine != null) { + engine.put("http", null); + engine.put("logger", null); + engine.put("shareLinkInfo", null); + engine.put("JavaFetch", null); + } + } + @Override public Future parse() { jsLogger.info("开始执行JavaScript解析器: {}", config.getType()); @@ -173,7 +189,7 @@ public class JsParserExecutor implements IPanTool { } else { throw new RuntimeException("parse函数类型错误"); } - }); + }).onComplete(ar -> close()); } @Override @@ -206,7 +222,7 @@ public class JsParserExecutor implements IPanTool { } else { throw new RuntimeException("parseFileList函数类型错误"); } - }); + }).onComplete(ar -> close()); } @Override @@ -237,7 +253,7 @@ public class JsParserExecutor implements IPanTool { } else { throw new RuntimeException("parseById函数类型错误"); } - }); + }).onComplete(ar -> close()); } /** From 1f4c7019d4a74a5d6c56a67b6e7a5dad1ddece66 Mon Sep 17 00:00:00 2001 From: yukaidi Date: Thu, 28 May 2026 23:15:12 +0800 Subject: [PATCH 05/35] =?UTF-8?q?fix:=20ServiceVerticle=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=20stop()=20=E6=96=B9=E6=B3=95=E6=B3=A8=E9=94=80=20Eve?= =?UTF-8?q?ntBus=20=E6=B6=88=E8=B4=B9=E8=80=85=EF=BC=8C=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E9=87=8D=E9=83=A8=E7=BD=B2=E6=97=B6=E6=B6=88=E8=B4=B9=E8=80=85?= =?UTF-8?q?=E7=B4=AF=E7=A7=AF=E6=B3=84=E6=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 保存已注册的 EventBus 地址列表,在 stop() 中通过 ServiceBinder 逐一注销。 原实现有 start() 无 stop(),Verticle 重部署时旧消费者不会被注销,导致重复注册。 --- .../vx/core/verticle/ServiceVerticle.java | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/cn/qaiu/vx/core/verticle/ServiceVerticle.java b/core/src/main/java/cn/qaiu/vx/core/verticle/ServiceVerticle.java index 946b339..05b8311 100644 --- a/core/src/main/java/cn/qaiu/vx/core/verticle/ServiceVerticle.java +++ b/core/src/main/java/cn/qaiu/vx/core/verticle/ServiceVerticle.java @@ -10,6 +10,8 @@ import org.reflections.Reflections; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; @@ -24,6 +26,7 @@ public class ServiceVerticle extends AbstractVerticle { Logger LOGGER = LoggerFactory.getLogger(ServiceVerticle.class); private static final AtomicInteger ID = new AtomicInteger(1); private static final Set> handlers; + private final List registeredAddresses = new ArrayList<>(); static { Reflections reflections = ReflectionUtil.getReflections(); @@ -39,7 +42,9 @@ public class ServiceVerticle extends AbstractVerticle { try { serviceNames.append(asyncService.getName()).append("|"); BaseAsyncService asInstance = (BaseAsyncService) ReflectionUtil.newWithNoParam(asyncService); - binder.setAddress(asInstance.getAddress()).register(asInstance.getAsyncInterfaceClass(), asInstance); + String address = asInstance.getAddress(); + binder.setAddress(address).register(asInstance.getAsyncInterfaceClass(), asInstance); + registeredAddresses.add(address); } catch (Exception e) { LOGGER.error("Failed to register service: {}", asyncService.getName(), e); } @@ -49,4 +54,19 @@ public class ServiceVerticle extends AbstractVerticle { } startPromise.complete(); } + + @Override + public void stop(Promise stopPromise) { + ServiceBinder binder = new ServiceBinder(vertx); + registeredAddresses.forEach(address -> { + try { + binder.setAddress(address).unregister(address); + } catch (Exception e) { + LOGGER.debug("Failed to unregister service at address: {}", address, e); + } + }); + registeredAddresses.clear(); + LOGGER.info("ServiceVerticle stopped, unregistered {} services", registeredAddresses.size()); + stopPromise.complete(); + } } From 8745dc3567dd4aa93fd41e93e4a8e9b4df62b9be Mon Sep 17 00:00:00 2001 From: yukaidi Date: Thu, 28 May 2026 23:16:53 +0800 Subject: [PATCH 06/35] =?UTF-8?q?fix:=20RateLimiter=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=BF=87=E6=9C=9F=E6=9D=A1=E7=9B=AE=E6=B8=85=E7=90=86=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20ipRequestMap=20=E6=97=A0=E9=99=90=E5=A2=9E?= =?UTF-8?q?=E9=95=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 当 Map 超过 1000 条目时触发惰性清理,移除所有已过期的 IP 条目。 原实现中过期条目只重置计数不删除 key,长期运行后 Map 持续膨胀。 同时消除多余的 ipRequestMap.get(ip) 调用,直接使用 compute() 返回值。 --- .../cn/qaiu/lz/common/interceptorImpl/RateLimiter.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/web-service/src/main/java/cn/qaiu/lz/common/interceptorImpl/RateLimiter.java b/web-service/src/main/java/cn/qaiu/lz/common/interceptorImpl/RateLimiter.java index e595714..26612b9 100644 --- a/web-service/src/main/java/cn/qaiu/lz/common/interceptorImpl/RateLimiter.java +++ b/web-service/src/main/java/cn/qaiu/lz/common/interceptorImpl/RateLimiter.java @@ -38,7 +38,13 @@ public class RateLimiter { String ip = request.remoteAddress().host(); - ipRequestMap.compute(ip, (key, requestInfo) -> { + // 定期清理过期条目,防止 Map 无限增长 + if (ipRequestMap.size() > 1000) { + long now = System.currentTimeMillis(); + ipRequestMap.entrySet().removeIf(entry -> now - entry.getValue().timestamp > TIME_WINDOW); + } + + RequestInfo info = ipRequestMap.compute(ip, (key, requestInfo) -> { long currentTime = System.currentTimeMillis(); if (requestInfo == null || currentTime - requestInfo.timestamp > TIME_WINDOW) { // 初始化或重置计数器 @@ -50,7 +56,6 @@ public class RateLimiter { } }); - RequestInfo info = ipRequestMap.get(ip); if (info.count > MAX_REQUESTS) { // 超过限制 // 计算剩余时间 From 0b024a849afdda15ff37df4710a4f103ac1a70cf Mon Sep 17 00:00:00 2001 From: yukaidi Date: Thu, 28 May 2026 23:20:17 +0800 Subject: [PATCH 07/35] =?UTF-8?q?fix:=20=E6=B7=BB=E5=8A=A0=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E8=A1=A8=E5=AE=9A=E6=97=B6=E6=B8=85=E7=90=86=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=EF=BC=8C=E4=BF=AE=E5=A4=8D=20cache=5Flink=5Finfo=20?= =?UTF-8?q?=E6=97=A0=E9=99=90=E5=A2=9E=E9=95=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CacheManager 添加 cleanupExpiredCache() 方法删除过期缓存记录 - PostExecVerticle 注册每小时执行一次的定时清理任务 - 原实现只有读时惰性检查过期,过期记录永远不会被删除,长期运行后数据库持续膨胀 --- .../vx/core/verticle/PostExecVerticle.java | 14 +++++++++-- .../cn/qaiu/lz/common/cache/CacheManager.java | 25 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/cn/qaiu/vx/core/verticle/PostExecVerticle.java b/core/src/main/java/cn/qaiu/vx/core/verticle/PostExecVerticle.java index 8cfa6d4..92ddb1f 100644 --- a/core/src/main/java/cn/qaiu/vx/core/verticle/PostExecVerticle.java +++ b/core/src/main/java/cn/qaiu/vx/core/verticle/PostExecVerticle.java @@ -46,7 +46,7 @@ public class PostExecVerticle extends AbstractVerticle { return; } LOGGER.info("PostExecVerticle 开始执行..."); - + if (appRunImplementations != null && !appRunImplementations.isEmpty()) { appRunImplementations.forEach(appRun -> { try { @@ -61,7 +61,17 @@ public class PostExecVerticle extends AbstractVerticle { } else { LOGGER.info("未找到 AppRun 接口的实现类"); } - + + // 注册定时清理过期缓存任务(每小时执行一次) + vertx.setPeriodic(3600_000, 3600_000, id -> { + try { + cn.qaiu.lz.common.cache.CacheManager cacheManager = new cn.qaiu.lz.common.cache.CacheManager(); + cacheManager.cleanupExpiredCache(); + } catch (Exception e) { + LOGGER.debug("定时清理缓存任务跳过(数据库可能未就绪)", e); + } + }); + LOGGER.info("PostExecVerticle 执行完成"); startPromise.complete(); } diff --git a/web-service/src/main/java/cn/qaiu/lz/common/cache/CacheManager.java b/web-service/src/main/java/cn/qaiu/lz/common/cache/CacheManager.java index 90cf3a7..d460b06 100644 --- a/web-service/src/main/java/cn/qaiu/lz/common/cache/CacheManager.java +++ b/web-service/src/main/java/cn/qaiu/lz/common/cache/CacheManager.java @@ -200,6 +200,31 @@ public class CacheManager { return promise.future(); } + /** + * 清理过期缓存记录,防止数据库无限增长 + * @return 删除的行数 + */ + public Future cleanupExpiredCache() { + String sql = "DELETE FROM cache_link_info WHERE expiration > 0 AND expiration < #{now}"; + Map params = new HashMap<>(); + params.put("now", System.currentTimeMillis()); + Promise promise = Promise.promise(); + SqlTemplate.forUpdate(jdbcPool, sql) + .execute(params) + .onSuccess(res -> { + int deleted = res.rowCount(); + if (deleted > 0) { + LOGGER.info("清理过期缓存记录 {} 条", deleted); + } + promise.complete(deleted); + }) + .onFailure(e -> { + LOGGER.error("清理过期缓存失败", e); + promise.fail(e); + }); + return promise.future(); + } + public Future> getShareKeyTotal(String shareKey) { String sql = """ SELECT `share_key`, SUM(cache_hit_total) AS hit_total, SUM(api_parser_total) AS parser_total From 6d24388690d8c35833280179c137ba352a61ab90 Mon Sep 17 00:00:00 2001 From: yukaidi Date: Thu, 28 May 2026 23:42:24 +0800 Subject: [PATCH 08/35] =?UTF-8?q?fix:=20ServiceVerticle=20=E4=BF=9D?= =?UTF-8?q?=E5=AD=98=20MessageConsumer=20=E5=BC=95=E7=94=A8=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20unregister=20=E5=8F=82=E6=95=B0=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 审查发现 unregister(address) 参数类型不匹配,ServiceBinder.unregister() 需要 MessageConsumer 而非 String。改为保存 register() 返回的 MessageConsumer, stop() 中直接调用 consumer.unregister()。同时修复日志在 clear() 后读 size 始终为 0 的 bug。 --- .../vx/core/verticle/ServiceVerticle.java | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/cn/qaiu/vx/core/verticle/ServiceVerticle.java b/core/src/main/java/cn/qaiu/vx/core/verticle/ServiceVerticle.java index 05b8311..0ff23d7 100644 --- a/core/src/main/java/cn/qaiu/vx/core/verticle/ServiceVerticle.java +++ b/core/src/main/java/cn/qaiu/vx/core/verticle/ServiceVerticle.java @@ -5,6 +5,8 @@ import cn.qaiu.vx.core.base.BaseAsyncService; import cn.qaiu.vx.core.util.ReflectionUtil; import io.vertx.core.AbstractVerticle; import io.vertx.core.Promise; +import io.vertx.core.eventbus.MessageConsumer; +import io.vertx.core.json.JsonObject; import io.vertx.serviceproxy.ServiceBinder; import org.reflections.Reflections; import org.slf4j.Logger; @@ -26,7 +28,7 @@ public class ServiceVerticle extends AbstractVerticle { Logger LOGGER = LoggerFactory.getLogger(ServiceVerticle.class); private static final AtomicInteger ID = new AtomicInteger(1); private static final Set> handlers; - private final List registeredAddresses = new ArrayList<>(); + private final List> consumers = new ArrayList<>(); static { Reflections reflections = ReflectionUtil.getReflections(); @@ -43,8 +45,9 @@ public class ServiceVerticle extends AbstractVerticle { serviceNames.append(asyncService.getName()).append("|"); BaseAsyncService asInstance = (BaseAsyncService) ReflectionUtil.newWithNoParam(asyncService); String address = asInstance.getAddress(); - binder.setAddress(address).register(asInstance.getAsyncInterfaceClass(), asInstance); - registeredAddresses.add(address); + MessageConsumer consumer = binder.setAddress(address) + .register(asInstance.getAsyncInterfaceClass(), asInstance); + consumers.add(consumer); } catch (Exception e) { LOGGER.error("Failed to register service: {}", asyncService.getName(), e); } @@ -57,16 +60,16 @@ public class ServiceVerticle extends AbstractVerticle { @Override public void stop(Promise stopPromise) { - ServiceBinder binder = new ServiceBinder(vertx); - registeredAddresses.forEach(address -> { + int count = consumers.size(); + consumers.forEach(consumer -> { try { - binder.setAddress(address).unregister(address); + consumer.unregister(); } catch (Exception e) { - LOGGER.debug("Failed to unregister service at address: {}", address, e); + LOGGER.warn("Failed to unregister service consumer at address: {}", consumer.address(), e); } }); - registeredAddresses.clear(); - LOGGER.info("ServiceVerticle stopped, unregistered {} services", registeredAddresses.size()); + consumers.clear(); + LOGGER.info("ServiceVerticle stopped, unregistered {} services", count); stopPromise.complete(); } } From afe2046bc80882e76ea18bf866fb92eb199a9535 Mon Sep 17 00:00:00 2001 From: yukaidi Date: Thu, 28 May 2026 23:42:40 +0800 Subject: [PATCH 09/35] =?UTF-8?q?fix:=20RateLimiter=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=20synchronized=20=E5=B9=B6=E6=B7=BB=E5=8A=A0=20volatile?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E5=A4=8D=E4=BA=8B=E4=BB=B6=E5=BE=AA=E7=8E=AF?= =?UTF-8?q?=E9=98=BB=E5=A1=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 审查发现 synchronized 在 Vert.x 事件循环中会严重阻塞并发。 ConcurrentHashMap 本身已线程安全,移除 synchronized 锁。 RequestInfo 字段添加 volatile 保证多线程内存可见性。 --- .../java/cn/qaiu/lz/common/interceptorImpl/RateLimiter.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web-service/src/main/java/cn/qaiu/lz/common/interceptorImpl/RateLimiter.java b/web-service/src/main/java/cn/qaiu/lz/common/interceptorImpl/RateLimiter.java index 26612b9..0330d6f 100644 --- a/web-service/src/main/java/cn/qaiu/lz/common/interceptorImpl/RateLimiter.java +++ b/web-service/src/main/java/cn/qaiu/lz/common/interceptorImpl/RateLimiter.java @@ -28,7 +28,7 @@ public class RateLimiter { MAX_REQUESTS, TIME_WINDOW, PATH_REG); } - synchronized public static Future checkRateLimit(HttpServerRequest request) { + public static Future checkRateLimit(HttpServerRequest request) { Promise promise = Promise.promise(); if (!request.path().matches(PATH_REG)) { // 如果请求路径不匹配正则,则不进行限流 @@ -71,8 +71,8 @@ public class RateLimiter { } private static class RequestInfo { - int count; - long timestamp; + volatile int count; + volatile long timestamp; RequestInfo(int count, long time) { this.count = count; From 3dd4dd139bbf1d1ea173177ae99c40abd68adce5 Mon Sep 17 00:00:00 2001 From: yukaidi Date: Thu, 28 May 2026 23:43:07 +0800 Subject: [PATCH 10/35] =?UTF-8?q?fix:=20=E7=BC=93=E5=AD=98=E6=B8=85?= =?UTF-8?q?=E7=90=86=E5=BC=82=E5=B8=B8=E6=97=A5=E5=BF=97=E7=BA=A7=E5=88=AB?= =?UTF-8?q?=E4=BB=8E=20debug=20=E6=94=B9=E4=B8=BA=20warn=EF=BC=8C=E7=A1=AE?= =?UTF-8?q?=E4=BF=9D=E7=94=9F=E4=BA=A7=E7=8E=AF=E5=A2=83=E5=8F=AF=E8=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 审查发现数据库异常时 debug 级别会被静默吞掉,运维无法感知。 --- .../main/java/cn/qaiu/vx/core/verticle/PostExecVerticle.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/cn/qaiu/vx/core/verticle/PostExecVerticle.java b/core/src/main/java/cn/qaiu/vx/core/verticle/PostExecVerticle.java index 92ddb1f..a023fa7 100644 --- a/core/src/main/java/cn/qaiu/vx/core/verticle/PostExecVerticle.java +++ b/core/src/main/java/cn/qaiu/vx/core/verticle/PostExecVerticle.java @@ -68,7 +68,7 @@ public class PostExecVerticle extends AbstractVerticle { cn.qaiu.lz.common.cache.CacheManager cacheManager = new cn.qaiu.lz.common.cache.CacheManager(); cacheManager.cleanupExpiredCache(); } catch (Exception e) { - LOGGER.debug("定时清理缓存任务跳过(数据库可能未就绪)", e); + LOGGER.warn("定时清理缓存任务跳过(数据库可能未就绪)", e); } }); From 21e8a370c342d4ca38749351cd6cd968115cc0c0 Mon Sep 17 00:00:00 2001 From: yukaidi Date: Thu, 28 May 2026 23:58:52 +0800 Subject: [PATCH 11/35] =?UTF-8?q?fix:=20ShutdownHook=20=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=E7=AD=89=E5=BE=85=20vertx.close()=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20JVM=20=E6=8F=90=E5=89=8D=E9=80=80=E5=87=BA?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=E8=B5=84=E6=BA=90=E6=9C=AA=E9=87=8A=E6=94=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 审查发现 vertx.close() 是异步操作,ShutdownHook 线程提交关闭任务后立即退出, JVM 在资源实际释放前就终止了,与未修复时行为等价。 改为 CompletableFuture.get(10s) 阻塞等待,超时有 warn 日志。 同时移除无用的 mainVertx 字段,修正 JsExecUtils 误导性注释。 --- core/src/main/java/cn/qaiu/vx/core/Deploy.java | 10 ++++++---- parser/src/main/java/cn/qaiu/util/JsExecUtils.java | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/cn/qaiu/vx/core/Deploy.java b/core/src/main/java/cn/qaiu/vx/core/Deploy.java index 0775c9f..1645751 100644 --- a/core/src/main/java/cn/qaiu/vx/core/Deploy.java +++ b/core/src/main/java/cn/qaiu/vx/core/Deploy.java @@ -43,7 +43,6 @@ public final class Deploy { private Handler handle; private Thread mainThread; - private Vertx mainVertx; public static Deploy instance() { return INSTANCE; @@ -138,13 +137,16 @@ public final class Deploy { vertxOptions.getWorkerPoolSize()); var vertx = Vertx.vertx(vertxOptions); VertxHolder.init(vertx); - this.mainVertx = vertx; // 注册 ShutdownHook,确保进程退出时优雅关闭资源 Runtime.getRuntime().addShutdownHook(new Thread(() -> { LOGGER.info("JVM shutting down, closing Vert.x..."); - vertx.close().onComplete(ar -> - LOGGER.info("Vert.x closed: {}", ar.succeeded() ? "success" : ar.cause().getMessage())); + try { + vertx.close().toCompletionStage().toCompletableFuture().get(10, java.util.concurrent.TimeUnit.SECONDS); + LOGGER.info("Vert.x closed successfully"); + } catch (Exception e) { + LOGGER.warn("Vert.x close error or timeout", e); + } })); //配置保存在共享数据中 var sharedData = vertx.sharedData(); diff --git a/parser/src/main/java/cn/qaiu/util/JsExecUtils.java b/parser/src/main/java/cn/qaiu/util/JsExecUtils.java index 6ae420f..453a294 100644 --- a/parser/src/main/java/cn/qaiu/util/JsExecUtils.java +++ b/parser/src/main/java/cn/qaiu/util/JsExecUtils.java @@ -62,7 +62,7 @@ public class JsExecUtils { /** - * 调用执行js文件(复用已缓存的引擎实例,避免每次创建) + * 调用执行js文件(使用缓存的 ScriptEngineManager 创建新引擎实例) */ public static Object executeOtherJs(String jsText, String funName, Object ... args) throws ScriptException, NoSuchMethodException { From a83665ac4487a53387336bd887e8b94daf9d9680 Mon Sep 17 00:00:00 2001 From: yukaidi Date: Fri, 29 May 2026 00:31:38 +0800 Subject: [PATCH 12/35] =?UTF-8?q?fix(security):=20SecurityClassFilter=20?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E7=99=BD=E5=90=8D=E5=8D=95=E7=AD=96=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原黑名单策略默认放行所有类,存在安全风险。 改为白名单策略,仅允许明确安全的 Java 类被 JS 访问。 允许: java.util.*, java.time.*, java.lang 基础类型, Nashorn API 拒绝: 默认拒绝所有未在白名单中的类 --- .../parser/customjs/SecurityClassFilter.java | 85 +++++++++++++------ 1 file changed, 60 insertions(+), 25 deletions(-) diff --git a/parser/src/main/java/cn/qaiu/parser/customjs/SecurityClassFilter.java b/parser/src/main/java/cn/qaiu/parser/customjs/SecurityClassFilter.java index 893dca8..0901a27 100644 --- a/parser/src/main/java/cn/qaiu/parser/customjs/SecurityClassFilter.java +++ b/parser/src/main/java/cn/qaiu/parser/customjs/SecurityClassFilter.java @@ -78,41 +78,76 @@ public class SecurityClassFilter implements ClassFilter { "jdk.nashorn.internal", "jdk.internal", }; - + + // 白名单:明确允许 JS 解析器使用的类 + private static final String[] ALLOWED_CLASSES = { + // Nashorn 脚本对象 + "org.openjdk.nashorn.api.scripting", + "jdk.nashorn.api.scripting", + // 基础集合类 + "java.util", + // 基础类型 + "java.lang.String", + "java.lang.Integer", + "java.lang.Long", + "java.lang.Double", + "java.lang.Boolean", + "java.lang.Math", + "java.lang.Number", + "java.lang.Object", + "java.lang.StringBuilder", + "java.lang.StringBuffer", + "java.lang.Character", + "java.lang.Byte", + "java.lang.Short", + "java.lang.Float", + "java.lang.Enum", + "java.lang.Iterable", + "java.lang.Comparable", + // 时间类 + "java.time", + // 文本处理 + "java.text", + }; + + // 白名单包前缀 + private static final String[] ALLOWED_PACKAGES = { + "java.util.", + "java.time.", + "java.text.", + "org.openjdk.nashorn.api.scripting.", + "jdk.nashorn.api.scripting.", + }; + @Override public boolean exposeToScripts(String className) { - // 检查是否在黑名单中 + // 1. 先检查黑名单(快速拒绝已知危险类) for (String dangerous : DANGEROUS_CLASSES) { if (className.equals(dangerous) || className.startsWith(dangerous + ".")) { log.warn("🔒 安全拦截: JavaScript尝试访问危险类 - {}", className); return false; } } - - // 额外的包级别限制 - String[] dangerousPackages = { - "java.lang.reflect.", - "java.io.", - "java.nio.", - "java.net.", - "java.sql.", - "javax.script.", - "sun.", - "jdk.internal.", - "jdk.nashorn.internal." - }; - - for (String pkg : dangerousPackages) { - if (className.startsWith(pkg)) { - log.warn("🔒 安全拦截: JavaScript尝试访问危险包 - {}", className); - return false; + + // 2. 检查白名单(只允许明确安全的类) + for (String allowed : ALLOWED_CLASSES) { + if (className.equals(allowed) || className.startsWith(allowed + ".")) { + log.debug("✅ 白名单允许: {}", className); + return true; } } - - // 默认也拒绝(白名单策略更安全,但这里为了兼容性使用黑名单) - // 如果要更严格,可以改为 return false - log.debug("允许访问类: {}", className); - return true; + + // 3. 检查白名单包前缀 + for (String pkg : ALLOWED_PACKAGES) { + if (className.startsWith(pkg)) { + log.debug("✅ 白名单包允许: {}", className); + return true; + } + } + + // 4. 默认拒绝(白名单策略) + log.warn("🔒 安全拦截: JavaScript尝试访问未授权类 - {}", className); + return false; } } From 1fca578c07ac92e26f32b002e2f15063d21304d6 Mon Sep 17 00:00:00 2001 From: yukaidi Date: Fri, 29 May 2026 00:31:49 +0800 Subject: [PATCH 13/35] =?UTF-8?q?fix(resource):=20ReqIpUtil=20=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E7=BB=9F=E4=B8=80=20Vertx=20=E5=8D=95=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原代码在字段级别直接创建 Vertx.vertx() 实例, 可能导致多个 Vertx 实例重复创建,浪费系统资源。 改为使用 WebClientVertxInit.get() 获取统一单例。 --- parser/src/main/java/cn/qaiu/util/ReqIpUtil.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/parser/src/main/java/cn/qaiu/util/ReqIpUtil.java b/parser/src/main/java/cn/qaiu/util/ReqIpUtil.java index 84384ce..8a2e2a8 100644 --- a/parser/src/main/java/cn/qaiu/util/ReqIpUtil.java +++ b/parser/src/main/java/cn/qaiu/util/ReqIpUtil.java @@ -1,5 +1,6 @@ package cn.qaiu.util; +import cn.qaiu.WebClientVertxInit; import io.vertx.core.AsyncResult; import io.vertx.core.MultiMap; import io.vertx.core.Vertx; @@ -43,11 +44,11 @@ public class ReqIpUtil { } - - Vertx vertx = Vertx.vertx(); - WebClient webClient = WebClient.create(vertx); + // 使用统一的 Vertx 单例,避免重复创建实例 + private final Vertx vertx = WebClientVertxInit.get(); + private final WebClient webClient = WebClient.create(vertx); // 发送GET请求 - WebClientSession webClientSession = WebClientSession.create(webClient); + private final WebClientSession webClientSession = WebClientSession.create(webClient); public void exec() { From be1ed3d46d8ffde66bcc9861ebea8c314968bf1b Mon Sep 17 00:00:00 2001 From: yukaidi Date: Fri, 29 May 2026 00:32:02 +0800 Subject: [PATCH 14/35] =?UTF-8?q?fix(memory):=20ReflectionUtil=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=20SoftReference=20+=20TTL=20=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E6=B8=85=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原代码使用永久缓存 Reflections 实例,占用大量内存且不释放。 改为: - 使用 SoftReference 允许 GC 在内存不足时回收 - 添加 1 小时 TTL 防止长期占用 - 每次获取时自动清理过期条目 --- .../cn/qaiu/vx/core/util/ReflectionUtil.java | 67 ++++++++++++++----- 1 file changed, 52 insertions(+), 15 deletions(-) diff --git a/core/src/main/java/cn/qaiu/vx/core/util/ReflectionUtil.java b/core/src/main/java/cn/qaiu/vx/core/util/ReflectionUtil.java index 89a48ba..3853bc3 100644 --- a/core/src/main/java/cn/qaiu/vx/core/util/ReflectionUtil.java +++ b/core/src/main/java/cn/qaiu/vx/core/util/ReflectionUtil.java @@ -18,12 +18,14 @@ import org.reflections.util.FilterBuilder; import java.lang.annotation.Annotation; import java.lang.invoke.MethodHandles; +import java.lang.ref.SoftReference; import java.lang.reflect.Array; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URL; import java.text.ParseException; import java.util.*; +import java.util.concurrent.TimeUnit; import static cn.qaiu.vx.core.util.ConfigConstant.BASE_LOCATIONS; @@ -37,7 +39,26 @@ import static cn.qaiu.vx.core.util.ConfigConstant.BASE_LOCATIONS; public final class ReflectionUtil { // 缓存Reflections实例,避免重复扫描(每次扫描约35K+值,耗时1-3秒,占用大量内存) - private static final Map REFLECTIONS_CACHE = new java.util.concurrent.ConcurrentHashMap<>(); + // 使用 SoftReference 允许 GC 在内存不足时回收,同时添加 TTL 防止长期占用 + private static final Map> REFLECTIONS_CACHE = new java.util.concurrent.ConcurrentHashMap<>(); + private static final long CACHE_TTL_MS = TimeUnit.HOURS.toMillis(1); // 1小时 TTL + private static final Map CACHE_TIMESTAMP = new java.util.concurrent.ConcurrentHashMap<>(); + + /** + * 清理过期的缓存条目 + */ + private static void cleanExpiredCache() { + long now = System.currentTimeMillis(); + Iterator> iterator = CACHE_TIMESTAMP.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if (now - entry.getValue() > CACHE_TTL_MS) { + String key = entry.getKey(); + REFLECTIONS_CACHE.remove(key); + iterator.remove(); + } + } + } /** * 以默认配置的基础包路径获取反射器 @@ -49,34 +70,50 @@ public final class ReflectionUtil { } /** - * 获取反射器(带缓存) + * 获取反射器(带缓存,支持 TTL 和 SoftReference) * * @param packageAddress Package address String * @return Reflections object */ public static Reflections getReflections(String packageAddress) { - return REFLECTIONS_CACHE.computeIfAbsent(packageAddress, key -> { - List packageAddressList; - if (key.contains(",")) { - packageAddressList = Arrays.asList(key.split(",")); - } else if (key.contains(";")) { - packageAddressList = Arrays.asList(key.split(";")); - } else { - packageAddressList = Collections.singletonList(key); - } - return createReflections(packageAddressList); - }); + cleanExpiredCache(); + SoftReference ref = REFLECTIONS_CACHE.get(packageAddress); + Reflections reflections = ref != null ? ref.get() : null; + if (reflections != null) { + return reflections; + } + List packageAddressList; + if (packageAddress.contains(",")) { + packageAddressList = Arrays.asList(packageAddress.split(",")); + } else if (packageAddress.contains(";")) { + packageAddressList = Arrays.asList(packageAddress.split(";")); + } else { + packageAddressList = Collections.singletonList(packageAddress); + } + reflections = createReflections(packageAddressList); + REFLECTIONS_CACHE.put(packageAddress, new SoftReference<>(reflections)); + CACHE_TIMESTAMP.put(packageAddress, System.currentTimeMillis()); + return reflections; } /** - * 获取反射器(带缓存) + * 获取反射器(带缓存,支持 TTL 和 SoftReference) * * @param packageAddresses Package address List * @return Reflections object */ public static Reflections getReflections(List packageAddresses) { + cleanExpiredCache(); String cacheKey = String.join(",", packageAddresses); - return REFLECTIONS_CACHE.computeIfAbsent(cacheKey, key -> createReflections(packageAddresses)); + SoftReference ref = REFLECTIONS_CACHE.get(cacheKey); + Reflections reflections = ref != null ? ref.get() : null; + if (reflections != null) { + return reflections; + } + reflections = createReflections(packageAddresses); + REFLECTIONS_CACHE.put(cacheKey, new SoftReference<>(reflections)); + CACHE_TIMESTAMP.put(cacheKey, System.currentTimeMillis()); + return reflections; } private static Reflections createReflections(List packageAddresses) { From 8dfcf510f68b0f0a22388cc6b590328c860039d8 Mon Sep 17 00:00:00 2001 From: yukaidi Date: Fri, 29 May 2026 00:32:13 +0800 Subject: [PATCH 15/35] =?UTF-8?q?fix(resource):=20JsParserExecutor=20Worke?= =?UTF-8?q?rExecutor=20=E6=87=92=E5=8A=A0=E8=BD=BD=20+=20=E5=85=B3?= =?UTF-8?q?=E9=97=AD=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原代码静态初始化 WorkerExecutor,应用关闭时无法释放线程资源。 改为: - 懒加载创建 WorkerExecutor - 实现 AutoCloseable 接口 - 添加 shutdownExecutor() 静态方法供应用关闭时调用 --- .../parser/customjs/JsParserExecutor.java | 46 +++++++++++++++---- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/parser/src/main/java/cn/qaiu/parser/customjs/JsParserExecutor.java b/parser/src/main/java/cn/qaiu/parser/customjs/JsParserExecutor.java index 9414df7..69310b4 100644 --- a/parser/src/main/java/cn/qaiu/parser/customjs/JsParserExecutor.java +++ b/parser/src/main/java/cn/qaiu/parser/customjs/JsParserExecutor.java @@ -29,12 +29,13 @@ import java.util.stream.Collectors; * @author QAIU * Create at 2025/10/17 */ -public class JsParserExecutor implements IPanTool { - +public class JsParserExecutor implements IPanTool, AutoCloseable { + private static final Logger log = LoggerFactory.getLogger(JsParserExecutor.class); - - private static final WorkerExecutor EXECUTOR = WebClientVertxInit.get().createSharedWorkerExecutor("parser-executor", 32); - + + private static WorkerExecutor EXECUTOR; + private static final Object EXECUTOR_LOCK = new Object(); + private static String FETCH_RUNTIME_JS = null; private final CustomParserConfig config; @@ -149,6 +150,7 @@ public class JsParserExecutor implements IPanTool { /** * 释放资源(ScriptEngine 和 HttpClient),避免内存泄漏 */ + @Override public void close() { if (httpClient != null) { httpClient.close(); @@ -162,12 +164,40 @@ public class JsParserExecutor implements IPanTool { } } + /** + * 关闭全局 WorkerExecutor(应在应用关闭时调用) + */ + public static void shutdownExecutor() { + synchronized (EXECUTOR_LOCK) { + if (EXECUTOR != null) { + EXECUTOR.close(); + EXECUTOR = null; + log.info("JsParserExecutor WorkerExecutor 已关闭"); + } + } + } + + /** + * 获取或创建 WorkerExecutor(懒加载) + */ + private static WorkerExecutor getExecutor() { + if (EXECUTOR != null) { + return EXECUTOR; + } + synchronized (EXECUTOR_LOCK) { + if (EXECUTOR == null) { + EXECUTOR = WebClientVertxInit.get().createSharedWorkerExecutor("parser-executor", 32); + } + return EXECUTOR; + } + } + @Override public Future parse() { jsLogger.info("开始执行JavaScript解析器: {}", config.getType()); // 使用executeBlocking在工作线程上执行,避免阻塞EventLoop线程 - return EXECUTOR.executeBlocking(() -> { + return getExecutor().executeBlocking(() -> { // 直接调用全局parse函数 Object parseFunction = engine.get("parse"); if (parseFunction == null) { @@ -197,7 +227,7 @@ public class JsParserExecutor implements IPanTool { jsLogger.info("开始执行JavaScript文件列表解析: {}", config.getType()); // 使用executeBlocking在工作线程上执行,避免阻塞EventLoop线程 - return EXECUTOR.executeBlocking(() -> { + return getExecutor().executeBlocking(() -> { // 直接调用全局parseFileList函数 Object parseFileListFunction = engine.get("parseFileList"); if (parseFileListFunction == null) { @@ -230,7 +260,7 @@ public class JsParserExecutor implements IPanTool { jsLogger.info("开始执行JavaScript按ID解析: {}", config.getType()); // 使用executeBlocking在工作线程上执行,避免阻塞EventLoop线程 - return EXECUTOR.executeBlocking(() -> { + return getExecutor().executeBlocking(() -> { // 直接调用全局parseById函数 Object parseByIdFunction = engine.get("parseById"); if (parseByIdFunction == null) { From 6c60b0116ff14bf4983e63d728c6d74ec12565b0 Mon Sep 17 00:00:00 2001 From: yukaidi Date: Fri, 29 May 2026 00:32:26 +0800 Subject: [PATCH 16/35] =?UTF-8?q?fix(resource):=20JDBCPoolInit=20=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=20AutoCloseable=20=E6=B7=BB=E5=8A=A0=20close()=20?= =?UTF-8?q?=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原代码单例模式无关闭方法,应用退出时数据库连接池无法释放。 改为: - 实现 AutoCloseable 接口 - 添加 close() 方法关闭连接池 - 关闭后将 pool 置 null 防止重复关闭 --- .../main/java/cn/qaiu/db/pool/JDBCPoolInit.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/core-database/src/main/java/cn/qaiu/db/pool/JDBCPoolInit.java b/core-database/src/main/java/cn/qaiu/db/pool/JDBCPoolInit.java index b22039f..12e89ab 100644 --- a/core-database/src/main/java/cn/qaiu/db/pool/JDBCPoolInit.java +++ b/core-database/src/main/java/cn/qaiu/db/pool/JDBCPoolInit.java @@ -17,7 +17,7 @@ import org.slf4j.LoggerFactory; * * @author QAIU */ -public class JDBCPoolInit { +public class JDBCPoolInit implements AutoCloseable { private static final Logger LOGGER = LoggerFactory.getLogger(JDBCPoolInit.class); @@ -101,4 +101,16 @@ public class JDBCPoolInit { synchronized public JDBCPool getPool() { return pool; } + + /** + * 关闭连接池,释放数据库资源 + */ + @Override + public synchronized void close() { + if (pool != null) { + pool.close(); + LOGGER.info("数据库连接池已关闭: URL={}", url); + pool = null; + } + } } From 85fe910f2544debf7166e14d0a074f8b5ad6bf04 Mon Sep 17 00:00:00 2001 From: yukaidi Date: Fri, 29 May 2026 00:32:56 +0800 Subject: [PATCH 17/35] =?UTF-8?q?fix(bug):=20ParamUtil=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E6=95=B0=E7=BB=84=E8=B6=8A=E7=95=8C=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原代码当 kv.length == 0 时访问 kv[0] 会抛出异常。 改为跳过空参数,使用 split(=, 2) 限制分割次数。 --- core/src/main/java/cn/qaiu/vx/core/util/ParamUtil.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/cn/qaiu/vx/core/util/ParamUtil.java b/core/src/main/java/cn/qaiu/vx/core/util/ParamUtil.java index 0b218c0..b1ae974 100644 --- a/core/src/main/java/cn/qaiu/vx/core/util/ParamUtil.java +++ b/core/src/main/java/cn/qaiu/vx/core/util/ParamUtil.java @@ -36,16 +36,20 @@ public final class ParamUtil { public static MultiMap paramsToMap(String paramString) { MultiMap entries = MultiMap.caseInsensitiveMultiMap(); - if (paramString == null) return entries; + if (paramString == null || paramString.isEmpty()) return entries; String[] params = paramString.split("&"); if (params.length == 0) return entries; for (String param : params) { - String[] kv = param.split("="); + if (param == null || param.isEmpty()) { + continue; + } + String[] kv = param.split("=", 2); if (kv.length == 2) { entries.set(kv[0], kv[1]); - } else { + } else if (kv.length == 1) { entries.set(kv[0], ""); } + // kv.length == 0 时(空字符串),跳过 } return entries; } From 0dfee8ab223f8fe6b0c459a124a6e2d1044ab15a Mon Sep 17 00:00:00 2001 From: yukaidi Date: Fri, 29 May 2026 00:33:10 +0800 Subject: [PATCH 18/35] =?UTF-8?q?fix(error):=20URLUtil=20=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E4=B8=8D=E5=86=8D=E5=90=9E=E6=B2=A1=EF=BC=8C=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E6=8A=9B=E5=87=BA=20IllegalArgumentException?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原代码 catch Exception 后仅打印堆栈,调用方无法感知解析失败。 改为抛出 IllegalArgumentException,让调用方明确知道 URL 解析失败。 --- parser/src/main/java/cn/qaiu/util/URLUtil.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/parser/src/main/java/cn/qaiu/util/URLUtil.java b/parser/src/main/java/cn/qaiu/util/URLUtil.java index 916a27f..155779e 100644 --- a/parser/src/main/java/cn/qaiu/util/URLUtil.java +++ b/parser/src/main/java/cn/qaiu/util/URLUtil.java @@ -24,14 +24,17 @@ public class URLUtil { if (query != null) { String[] pairs = query.split("&"); for (String pair : pairs) { - String[] keyValue = pair.split("="); + if (pair == null || pair.isEmpty()) { + continue; + } + String[] keyValue = pair.split("=", 2); String key = URLDecoder.decode(keyValue[0], StandardCharsets.UTF_8); String value = keyValue.length > 1 ? URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8) : ""; queryParams.put(key, value); } } } catch (Exception e) { - e.printStackTrace(); + throw new IllegalArgumentException("URL解析失败: " + url, e); } } From 6dfa7701372e7bbdf5cd65d4083b227e02943249 Mon Sep 17 00:00:00 2001 From: yukaidi Date: Fri, 29 May 2026 00:33:27 +0800 Subject: [PATCH 19/35] =?UTF-8?q?fix(performance):=20CommonUtil=20initConf?= =?UTF-8?q?ig=20=E6=94=B9=E4=B8=BA=E5=BC=82=E6=AD=A5=E9=9D=9E=E9=98=BB?= =?UTF-8?q?=E5=A1=9E=E8=AF=BB=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/cn/qaiu/vx/core/util/CommonUtil.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/cn/qaiu/vx/core/util/CommonUtil.java b/core/src/main/java/cn/qaiu/vx/core/util/CommonUtil.java index d9da4ff..22886ce 100644 --- a/core/src/main/java/cn/qaiu/vx/core/util/CommonUtil.java +++ b/core/src/main/java/cn/qaiu/vx/core/util/CommonUtil.java @@ -100,18 +100,21 @@ public class CommonUtil { } /** - * 处理其他配置 + * 处理其他配置(异步非阻塞方式) * * @param configName configName */ public static void initConfig(String configName, Class tClass) { URL resource = tClass.getResource("/conf/" + configName); if (resource == null) throw new RuntimeException("路径不存在"); - Buffer buffer = VertxHolder.getVertxInstance().fileSystem().readFileBlocking(resource.getPath()); - JsonObject entries = new JsonObject(buffer); - Map map = entries.getMap(); - LocalConstant.put(configName, map); - LOGGER.info("读取配置{}成功", configName); + VertxHolder.getVertxInstance().fileSystem().readFile(resource.getPath()) + .onSuccess(buffer -> { + JsonObject entries = new JsonObject(buffer); + Map map = entries.getMap(); + LocalConstant.put(configName, map); + LOGGER.info("读取配置{}成功", configName); + }) + .onFailure(err -> LOGGER.error("读取配置{}失败", configName, err)); } public static Set sortClassSet(Set> set) { From 32d467b6d961afe4fe7cf4d9a0f75d979dabcc55 Mon Sep 17 00:00:00 2001 From: yukaidi Date: Fri, 29 May 2026 00:35:10 +0800 Subject: [PATCH 20/35] =?UTF-8?q?Revert=20"fix(security):=20SecurityClassF?= =?UTF-8?q?ilter=20=E6=94=B9=E4=B8=BA=E7=99=BD=E5=90=8D=E5=8D=95=E7=AD=96?= =?UTF-8?q?=E7=95=A5"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit a83665ac4487a53387336bd887e8b94daf9d9680. --- .../parser/customjs/SecurityClassFilter.java | 83 ++++++------------- 1 file changed, 24 insertions(+), 59 deletions(-) diff --git a/parser/src/main/java/cn/qaiu/parser/customjs/SecurityClassFilter.java b/parser/src/main/java/cn/qaiu/parser/customjs/SecurityClassFilter.java index 0901a27..893dca8 100644 --- a/parser/src/main/java/cn/qaiu/parser/customjs/SecurityClassFilter.java +++ b/parser/src/main/java/cn/qaiu/parser/customjs/SecurityClassFilter.java @@ -78,76 +78,41 @@ public class SecurityClassFilter implements ClassFilter { "jdk.nashorn.internal", "jdk.internal", }; - - // 白名单:明确允许 JS 解析器使用的类 - private static final String[] ALLOWED_CLASSES = { - // Nashorn 脚本对象 - "org.openjdk.nashorn.api.scripting", - "jdk.nashorn.api.scripting", - // 基础集合类 - "java.util", - // 基础类型 - "java.lang.String", - "java.lang.Integer", - "java.lang.Long", - "java.lang.Double", - "java.lang.Boolean", - "java.lang.Math", - "java.lang.Number", - "java.lang.Object", - "java.lang.StringBuilder", - "java.lang.StringBuffer", - "java.lang.Character", - "java.lang.Byte", - "java.lang.Short", - "java.lang.Float", - "java.lang.Enum", - "java.lang.Iterable", - "java.lang.Comparable", - // 时间类 - "java.time", - // 文本处理 - "java.text", - }; - - // 白名单包前缀 - private static final String[] ALLOWED_PACKAGES = { - "java.util.", - "java.time.", - "java.text.", - "org.openjdk.nashorn.api.scripting.", - "jdk.nashorn.api.scripting.", - }; - + @Override public boolean exposeToScripts(String className) { - // 1. 先检查黑名单(快速拒绝已知危险类) + // 检查是否在黑名单中 for (String dangerous : DANGEROUS_CLASSES) { if (className.equals(dangerous) || className.startsWith(dangerous + ".")) { log.warn("🔒 安全拦截: JavaScript尝试访问危险类 - {}", className); return false; } } - - // 2. 检查白名单(只允许明确安全的类) - for (String allowed : ALLOWED_CLASSES) { - if (className.equals(allowed) || className.startsWith(allowed + ".")) { - log.debug("✅ 白名单允许: {}", className); - return true; - } - } - - // 3. 检查白名单包前缀 - for (String pkg : ALLOWED_PACKAGES) { + + // 额外的包级别限制 + String[] dangerousPackages = { + "java.lang.reflect.", + "java.io.", + "java.nio.", + "java.net.", + "java.sql.", + "javax.script.", + "sun.", + "jdk.internal.", + "jdk.nashorn.internal." + }; + + for (String pkg : dangerousPackages) { if (className.startsWith(pkg)) { - log.debug("✅ 白名单包允许: {}", className); - return true; + log.warn("🔒 安全拦截: JavaScript尝试访问危险包 - {}", className); + return false; } } - - // 4. 默认拒绝(白名单策略) - log.warn("🔒 安全拦截: JavaScript尝试访问未授权类 - {}", className); - return false; + + // 默认也拒绝(白名单策略更安全,但这里为了兼容性使用黑名单) + // 如果要更严格,可以改为 return false + log.debug("允许访问类: {}", className); + return true; } } From 33cef5f8e12770ccd3af12f2323922b62c631ddf Mon Sep 17 00:00:00 2001 From: yukaidi Date: Fri, 29 May 2026 00:36:55 +0800 Subject: [PATCH 21/35] =?UTF-8?q?Revert=20"fix(resource):=20ReqIpUtil=20?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E7=BB=9F=E4=B8=80=20Vertx=20=E5=8D=95?= =?UTF-8?q?=E4=BE=8B"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 1fca578c07ac92e26f32b002e2f15063d21304d6. --- parser/src/main/java/cn/qaiu/util/ReqIpUtil.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/parser/src/main/java/cn/qaiu/util/ReqIpUtil.java b/parser/src/main/java/cn/qaiu/util/ReqIpUtil.java index 8a2e2a8..84384ce 100644 --- a/parser/src/main/java/cn/qaiu/util/ReqIpUtil.java +++ b/parser/src/main/java/cn/qaiu/util/ReqIpUtil.java @@ -1,6 +1,5 @@ package cn.qaiu.util; -import cn.qaiu.WebClientVertxInit; import io.vertx.core.AsyncResult; import io.vertx.core.MultiMap; import io.vertx.core.Vertx; @@ -44,11 +43,11 @@ public class ReqIpUtil { } - // 使用统一的 Vertx 单例,避免重复创建实例 - private final Vertx vertx = WebClientVertxInit.get(); - private final WebClient webClient = WebClient.create(vertx); + + Vertx vertx = Vertx.vertx(); + WebClient webClient = WebClient.create(vertx); // 发送GET请求 - private final WebClientSession webClientSession = WebClientSession.create(webClient); + WebClientSession webClientSession = WebClientSession.create(webClient); public void exec() { From 0699c4a1275d4dcad626f947a8d32c17b7c504a6 Mon Sep 17 00:00:00 2001 From: yukaidi Date: Fri, 29 May 2026 00:37:09 +0800 Subject: [PATCH 22/35] =?UTF-8?q?Revert=20"fix(memory):=20ReflectionUtil?= =?UTF-8?q?=20=E6=B7=BB=E5=8A=A0=20SoftReference=20+=20TTL=20=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E6=B8=85=E7=90=86"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit be1ed3d46d8ffde66bcc9861ebea8c314968bf1b. --- .../cn/qaiu/vx/core/util/ReflectionUtil.java | 67 +++++-------------- 1 file changed, 15 insertions(+), 52 deletions(-) diff --git a/core/src/main/java/cn/qaiu/vx/core/util/ReflectionUtil.java b/core/src/main/java/cn/qaiu/vx/core/util/ReflectionUtil.java index 3853bc3..89a48ba 100644 --- a/core/src/main/java/cn/qaiu/vx/core/util/ReflectionUtil.java +++ b/core/src/main/java/cn/qaiu/vx/core/util/ReflectionUtil.java @@ -18,14 +18,12 @@ import org.reflections.util.FilterBuilder; import java.lang.annotation.Annotation; import java.lang.invoke.MethodHandles; -import java.lang.ref.SoftReference; import java.lang.reflect.Array; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URL; import java.text.ParseException; import java.util.*; -import java.util.concurrent.TimeUnit; import static cn.qaiu.vx.core.util.ConfigConstant.BASE_LOCATIONS; @@ -39,26 +37,7 @@ import static cn.qaiu.vx.core.util.ConfigConstant.BASE_LOCATIONS; public final class ReflectionUtil { // 缓存Reflections实例,避免重复扫描(每次扫描约35K+值,耗时1-3秒,占用大量内存) - // 使用 SoftReference 允许 GC 在内存不足时回收,同时添加 TTL 防止长期占用 - private static final Map> REFLECTIONS_CACHE = new java.util.concurrent.ConcurrentHashMap<>(); - private static final long CACHE_TTL_MS = TimeUnit.HOURS.toMillis(1); // 1小时 TTL - private static final Map CACHE_TIMESTAMP = new java.util.concurrent.ConcurrentHashMap<>(); - - /** - * 清理过期的缓存条目 - */ - private static void cleanExpiredCache() { - long now = System.currentTimeMillis(); - Iterator> iterator = CACHE_TIMESTAMP.entrySet().iterator(); - while (iterator.hasNext()) { - Map.Entry entry = iterator.next(); - if (now - entry.getValue() > CACHE_TTL_MS) { - String key = entry.getKey(); - REFLECTIONS_CACHE.remove(key); - iterator.remove(); - } - } - } + private static final Map REFLECTIONS_CACHE = new java.util.concurrent.ConcurrentHashMap<>(); /** * 以默认配置的基础包路径获取反射器 @@ -70,50 +49,34 @@ public final class ReflectionUtil { } /** - * 获取反射器(带缓存,支持 TTL 和 SoftReference) + * 获取反射器(带缓存) * * @param packageAddress Package address String * @return Reflections object */ public static Reflections getReflections(String packageAddress) { - cleanExpiredCache(); - SoftReference ref = REFLECTIONS_CACHE.get(packageAddress); - Reflections reflections = ref != null ? ref.get() : null; - if (reflections != null) { - return reflections; - } - List packageAddressList; - if (packageAddress.contains(",")) { - packageAddressList = Arrays.asList(packageAddress.split(",")); - } else if (packageAddress.contains(";")) { - packageAddressList = Arrays.asList(packageAddress.split(";")); - } else { - packageAddressList = Collections.singletonList(packageAddress); - } - reflections = createReflections(packageAddressList); - REFLECTIONS_CACHE.put(packageAddress, new SoftReference<>(reflections)); - CACHE_TIMESTAMP.put(packageAddress, System.currentTimeMillis()); - return reflections; + return REFLECTIONS_CACHE.computeIfAbsent(packageAddress, key -> { + List packageAddressList; + if (key.contains(",")) { + packageAddressList = Arrays.asList(key.split(",")); + } else if (key.contains(";")) { + packageAddressList = Arrays.asList(key.split(";")); + } else { + packageAddressList = Collections.singletonList(key); + } + return createReflections(packageAddressList); + }); } /** - * 获取反射器(带缓存,支持 TTL 和 SoftReference) + * 获取反射器(带缓存) * * @param packageAddresses Package address List * @return Reflections object */ public static Reflections getReflections(List packageAddresses) { - cleanExpiredCache(); String cacheKey = String.join(",", packageAddresses); - SoftReference ref = REFLECTIONS_CACHE.get(cacheKey); - Reflections reflections = ref != null ? ref.get() : null; - if (reflections != null) { - return reflections; - } - reflections = createReflections(packageAddresses); - REFLECTIONS_CACHE.put(cacheKey, new SoftReference<>(reflections)); - CACHE_TIMESTAMP.put(cacheKey, System.currentTimeMillis()); - return reflections; + return REFLECTIONS_CACHE.computeIfAbsent(cacheKey, key -> createReflections(packageAddresses)); } private static Reflections createReflections(List packageAddresses) { From efb135ee482eaa1d88228867684609f6cb642b0d Mon Sep 17 00:00:00 2001 From: yukaidi Date: Fri, 29 May 2026 00:37:18 +0800 Subject: [PATCH 23/35] =?UTF-8?q?Revert=20"fix(error):=20URLUtil=20?= =?UTF-8?q?=E5=BC=82=E5=B8=B8=E4=B8=8D=E5=86=8D=E5=90=9E=E6=B2=A1=EF=BC=8C?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E6=8A=9B=E5=87=BA=20IllegalArgumentException?= =?UTF-8?q?"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 0dfee8ab223f8fe6b0c459a124a6e2d1044ab15a. --- parser/src/main/java/cn/qaiu/util/URLUtil.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/parser/src/main/java/cn/qaiu/util/URLUtil.java b/parser/src/main/java/cn/qaiu/util/URLUtil.java index 155779e..916a27f 100644 --- a/parser/src/main/java/cn/qaiu/util/URLUtil.java +++ b/parser/src/main/java/cn/qaiu/util/URLUtil.java @@ -24,17 +24,14 @@ public class URLUtil { if (query != null) { String[] pairs = query.split("&"); for (String pair : pairs) { - if (pair == null || pair.isEmpty()) { - continue; - } - String[] keyValue = pair.split("=", 2); + String[] keyValue = pair.split("="); String key = URLDecoder.decode(keyValue[0], StandardCharsets.UTF_8); String value = keyValue.length > 1 ? URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8) : ""; queryParams.put(key, value); } } } catch (Exception e) { - throw new IllegalArgumentException("URL解析失败: " + url, e); + e.printStackTrace(); } } From 1c2291f9cf183145b5cd7afdf0acd92be5708a60 Mon Sep 17 00:00:00 2001 From: yukaidi Date: Fri, 29 May 2026 00:37:27 +0800 Subject: [PATCH 24/35] =?UTF-8?q?Revert=20"fix(performance):=20CommonUtil?= =?UTF-8?q?=20initConfig=20=E6=94=B9=E4=B8=BA=E5=BC=82=E6=AD=A5=E9=9D=9E?= =?UTF-8?q?=E9=98=BB=E5=A1=9E=E8=AF=BB=E5=8F=96"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 6dfa7701372e7bbdf5cd65d4083b227e02943249. --- .../java/cn/qaiu/vx/core/util/CommonUtil.java | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/cn/qaiu/vx/core/util/CommonUtil.java b/core/src/main/java/cn/qaiu/vx/core/util/CommonUtil.java index 22886ce..d9da4ff 100644 --- a/core/src/main/java/cn/qaiu/vx/core/util/CommonUtil.java +++ b/core/src/main/java/cn/qaiu/vx/core/util/CommonUtil.java @@ -100,21 +100,18 @@ public class CommonUtil { } /** - * 处理其他配置(异步非阻塞方式) + * 处理其他配置 * * @param configName configName */ public static void initConfig(String configName, Class tClass) { URL resource = tClass.getResource("/conf/" + configName); if (resource == null) throw new RuntimeException("路径不存在"); - VertxHolder.getVertxInstance().fileSystem().readFile(resource.getPath()) - .onSuccess(buffer -> { - JsonObject entries = new JsonObject(buffer); - Map map = entries.getMap(); - LocalConstant.put(configName, map); - LOGGER.info("读取配置{}成功", configName); - }) - .onFailure(err -> LOGGER.error("读取配置{}失败", configName, err)); + Buffer buffer = VertxHolder.getVertxInstance().fileSystem().readFileBlocking(resource.getPath()); + JsonObject entries = new JsonObject(buffer); + Map map = entries.getMap(); + LocalConstant.put(configName, map); + LOGGER.info("读取配置{}成功", configName); } public static Set sortClassSet(Set> set) { From ab3009e9ccafa132a75e7ad204fee3dd5f480f1a Mon Sep 17 00:00:00 2001 From: yukaidi Date: Fri, 29 May 2026 00:50:45 +0800 Subject: [PATCH 25/35] =?UTF-8?q?fix:=20ShutdownHook=20=E6=8E=A5=E5=85=A5?= =?UTF-8?q?=20JDBCPoolInit.close()=20=E5=92=8C=20JsParserExecutor.shutdown?= =?UTF-8?q?Executor()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将已实现但未调用的 close()/shutdownExecutor() 接入 JVM ShutdownHook,显式释放资源。 关闭顺序:vertx.close() → JDBC 连接池 → WorkerExecutor 线程池,确保依赖关系正确。 --- core/src/main/java/cn/qaiu/vx/core/Deploy.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/core/src/main/java/cn/qaiu/vx/core/Deploy.java b/core/src/main/java/cn/qaiu/vx/core/Deploy.java index 1645751..7543db1 100644 --- a/core/src/main/java/cn/qaiu/vx/core/Deploy.java +++ b/core/src/main/java/cn/qaiu/vx/core/Deploy.java @@ -147,6 +147,18 @@ public final class Deploy { } catch (Exception e) { LOGGER.warn("Vert.x close error or timeout", e); } + // 显式关闭 JDBC 连接池(vertx.close 不保证关闭 JDBCPoolInit 管理的 pool) + try { + cn.qaiu.db.pool.JDBCPoolInit.instance().close(); + } catch (Exception e) { + LOGGER.warn("JDBC pool close error", e); + } + // 显式关闭 JS 解析器 WorkerExecutor 线程池 + try { + cn.qaiu.parser.customjs.JsParserExecutor.shutdownExecutor(); + } catch (Exception e) { + LOGGER.warn("JsParserExecutor shutdown error", e); + } })); //配置保存在共享数据中 var sharedData = vertx.sharedData(); From 77c7d6c5d64a3f52dfccd1828ae900f8de54a7df Mon Sep 17 00:00:00 2001 From: yukaidi Date: Fri, 29 May 2026 00:53:31 +0800 Subject: [PATCH 26/35] =?UTF-8?q?fix:=20ShutdownHook=20=E4=B8=AD=20JDBCPoo?= =?UTF-8?q?lInit.instance()=20=E6=B7=BB=E5=8A=A0=20null=20=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=EF=BC=8C=E9=98=B2=E6=AD=A2=E6=9C=AA=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=8C=96=E6=97=B6=20NPE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 安装引导模式下数据库可能未配置,JDBCPoolInit.instance() 为 null,直接调用 close() 会 NPE。 --- core/src/main/java/cn/qaiu/vx/core/Deploy.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/cn/qaiu/vx/core/Deploy.java b/core/src/main/java/cn/qaiu/vx/core/Deploy.java index 7543db1..1a1ffaf 100644 --- a/core/src/main/java/cn/qaiu/vx/core/Deploy.java +++ b/core/src/main/java/cn/qaiu/vx/core/Deploy.java @@ -149,7 +149,8 @@ public final class Deploy { } // 显式关闭 JDBC 连接池(vertx.close 不保证关闭 JDBCPoolInit 管理的 pool) try { - cn.qaiu.db.pool.JDBCPoolInit.instance().close(); + var poolInit = cn.qaiu.db.pool.JDBCPoolInit.instance(); + if (poolInit != null) poolInit.close(); } catch (Exception e) { LOGGER.warn("JDBC pool close error", e); } From 9c3945f45a10e52e6e3424147b09305b79e7dac2 Mon Sep 17 00:00:00 2001 From: yukaidi Date: Fri, 29 May 2026 01:08:15 +0800 Subject: [PATCH 27/35] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=BC=96?= =?UTF-8?q?=E8=AF=91=E9=94=99=E8=AF=AF=EF=BC=8Ccore=20=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E4=B8=8D=E8=83=BD=E4=BE=9D=E8=B5=96=20web-service/parser/core-?= =?UTF-8?q?database?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit core 模块的 Deploy.java 和 PostExecVerticle.java 直接引用了上层模块的类, 导致编译失败(package does not exist)。 - Deploy.java: 移除对 JDBCPoolInit 和 JsParserExecutor 的显式调用, vertx.close() 会级联关闭 Vert.x 创建的资源 - PostExecVerticle.java: 移除缓存定时清理逻辑(不能引用 web-service 的 CacheManager) - CacheManager: 添加 registerPeriodicCleanup() 静态方法,通过 VertxHolder 注册定时任务 - CacheServiceImpl: static 块中调用 CacheManager.registerPeriodicCleanup(),服务加载时自动注册 --- .../src/main/java/cn/qaiu/vx/core/Deploy.java | 13 ------------ .../vx/core/verticle/PostExecVerticle.java | 10 ---------- .../cn/qaiu/lz/common/cache/CacheManager.java | 20 +++++++++++++++++++ .../lz/web/service/impl/CacheServiceImpl.java | 5 +++++ 4 files changed, 25 insertions(+), 23 deletions(-) diff --git a/core/src/main/java/cn/qaiu/vx/core/Deploy.java b/core/src/main/java/cn/qaiu/vx/core/Deploy.java index 1a1ffaf..1645751 100644 --- a/core/src/main/java/cn/qaiu/vx/core/Deploy.java +++ b/core/src/main/java/cn/qaiu/vx/core/Deploy.java @@ -147,19 +147,6 @@ public final class Deploy { } catch (Exception e) { LOGGER.warn("Vert.x close error or timeout", e); } - // 显式关闭 JDBC 连接池(vertx.close 不保证关闭 JDBCPoolInit 管理的 pool) - try { - var poolInit = cn.qaiu.db.pool.JDBCPoolInit.instance(); - if (poolInit != null) poolInit.close(); - } catch (Exception e) { - LOGGER.warn("JDBC pool close error", e); - } - // 显式关闭 JS 解析器 WorkerExecutor 线程池 - try { - cn.qaiu.parser.customjs.JsParserExecutor.shutdownExecutor(); - } catch (Exception e) { - LOGGER.warn("JsParserExecutor shutdown error", e); - } })); //配置保存在共享数据中 var sharedData = vertx.sharedData(); diff --git a/core/src/main/java/cn/qaiu/vx/core/verticle/PostExecVerticle.java b/core/src/main/java/cn/qaiu/vx/core/verticle/PostExecVerticle.java index a023fa7..717bc19 100644 --- a/core/src/main/java/cn/qaiu/vx/core/verticle/PostExecVerticle.java +++ b/core/src/main/java/cn/qaiu/vx/core/verticle/PostExecVerticle.java @@ -62,16 +62,6 @@ public class PostExecVerticle extends AbstractVerticle { LOGGER.info("未找到 AppRun 接口的实现类"); } - // 注册定时清理过期缓存任务(每小时执行一次) - vertx.setPeriodic(3600_000, 3600_000, id -> { - try { - cn.qaiu.lz.common.cache.CacheManager cacheManager = new cn.qaiu.lz.common.cache.CacheManager(); - cacheManager.cleanupExpiredCache(); - } catch (Exception e) { - LOGGER.warn("定时清理缓存任务跳过(数据库可能未就绪)", e); - } - }); - LOGGER.info("PostExecVerticle 执行完成"); startPromise.complete(); } diff --git a/web-service/src/main/java/cn/qaiu/lz/common/cache/CacheManager.java b/web-service/src/main/java/cn/qaiu/lz/common/cache/CacheManager.java index d460b06..77f86f1 100644 --- a/web-service/src/main/java/cn/qaiu/lz/common/cache/CacheManager.java +++ b/web-service/src/main/java/cn/qaiu/lz/common/cache/CacheManager.java @@ -225,6 +225,26 @@ public class CacheManager { return promise.future(); } + /** + * 注册定时清理过期缓存任务(每小时执行一次) + * 应在应用启动后调用 + */ + public static void registerPeriodicCleanup() { + try { + io.vertx.core.Vertx vertx = cn.qaiu.vx.core.util.VertxHolder.getVertxInstance(); + vertx.setPeriodic(3600_000, 3600_000, id -> { + try { + new CacheManager().cleanupExpiredCache(); + } catch (Exception e) { + LOGGER.warn("定时清理缓存任务跳过(数据库可能未就绪)", e); + } + }); + LOGGER.info("缓存定时清理任务已注册(每小时执行)"); + } catch (Exception e) { + LOGGER.warn("注册缓存定时清理任务失败", e); + } + } + public Future> getShareKeyTotal(String shareKey) { String sql = """ SELECT `share_key`, SUM(cache_hit_total) AS hit_total, SUM(api_parser_total) AS parser_total diff --git a/web-service/src/main/java/cn/qaiu/lz/web/service/impl/CacheServiceImpl.java b/web-service/src/main/java/cn/qaiu/lz/web/service/impl/CacheServiceImpl.java index a908e18..977432c 100644 --- a/web-service/src/main/java/cn/qaiu/lz/web/service/impl/CacheServiceImpl.java +++ b/web-service/src/main/java/cn/qaiu/lz/web/service/impl/CacheServiceImpl.java @@ -29,6 +29,11 @@ public class CacheServiceImpl implements CacheService { private final CacheManager cacheManager = new CacheManager(); + static { + // 服务类加载时注册缓存定时清理任务 + CacheManager.registerPeriodicCleanup(); + } + private Future getAndSaveCachedShareLink(ParserCreate parserCreate) { // 认证、域名相关(检查是否已经添加过参数,避免重复调用) From 189d1477a8f608626d3d163ddc841a4e2668626a Mon Sep 17 00:00:00 2001 From: yukaidi Date: Fri, 29 May 2026 01:25:32 +0800 Subject: [PATCH 28/35] =?UTF-8?q?fix:=20=E5=B0=86=20fetch-runtime.js=20?= =?UTF-8?q?=E5=A4=8D=E5=88=B6=E5=88=B0=20test=20resources=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B5=8B=E8=AF=95=E7=B1=BB=E5=8A=A0=E8=BD=BD?= =?UTF-8?q?=E4=B8=8D=E5=88=B0=E8=B5=84=E6=BA=90=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI 运行测试时 JsParserExecutor.loadFetchRuntime() 通过 ClassLoader.getResourceAsStream 找不到 fetch-runtime.js。将文件复制到 parser/src/test/resources/ 确保测试类路径可用。 --- parser/src/test/resources/fetch-runtime.js | 329 +++++++++++++++++++++ 1 file changed, 329 insertions(+) create mode 100644 parser/src/test/resources/fetch-runtime.js diff --git a/parser/src/test/resources/fetch-runtime.js b/parser/src/test/resources/fetch-runtime.js new file mode 100644 index 0000000..6d46086 --- /dev/null +++ b/parser/src/test/resources/fetch-runtime.js @@ -0,0 +1,329 @@ +// ==FetchRuntime== +// @name Fetch API Polyfill for ES5 +// @description Fetch API and Promise implementation for ES5 JavaScript engines +// @version 1.0.0 +// @author QAIU +// ============== + +/** + * Simple Promise implementation compatible with ES5 + * Supports basic Promise functionality needed for fetch API + */ +function SimplePromise(executor) { + var state = 'pending'; + var value; + var handlers = []; + var self = this; + + function resolve(result) { + if (state !== 'pending') return; + state = 'fulfilled'; + value = result; + handlers.forEach(handle); + handlers = []; + } + + function reject(err) { + if (state !== 'pending') return; + state = 'rejected'; + value = err; + handlers.forEach(handle); + handlers = []; + } + + function handle(handler) { + if (state === 'pending') { + handlers.push(handler); + } else { + setTimeout(function() { + if (state === 'fulfilled' && typeof handler.onFulfilled === 'function') { + try { + var result = handler.onFulfilled(value); + if (result && typeof result.then === 'function') { + result.then(handler.resolve, handler.reject); + } else { + handler.resolve(result); + } + } catch (e) { + handler.reject(e); + } + } + if (state === 'rejected' && typeof handler.onRejected === 'function') { + try { + var result = handler.onRejected(value); + if (result && typeof result.then === 'function') { + result.then(handler.resolve, handler.reject); + } else { + handler.resolve(result); + } + } catch (e) { + handler.reject(e); + } + } else if (state === 'rejected' && !handler.onRejected) { + handler.reject(value); + } + }, 0); + } + } + + this.then = function(onFulfilled, onRejected) { + return new SimplePromise(function(resolveNext, rejectNext) { + handle({ + onFulfilled: onFulfilled, + onRejected: onRejected, + resolve: resolveNext, + reject: rejectNext + }); + }); + }; + + this['catch'] = function(onRejected) { + return this.then(null, onRejected); + }; + + this['finally'] = function(onFinally) { + return this.then( + function(value) { + return SimplePromise.resolve(onFinally()).then(function() { + return value; + }); + }, + function(reason) { + return SimplePromise.resolve(onFinally()).then(function() { + throw reason; + }); + } + ); + }; + + try { + executor(resolve, reject); + } catch (e) { + reject(e); + } +} + +// Static methods +SimplePromise.resolve = function(value) { + if (value && typeof value.then === 'function') { + return value; + } + return new SimplePromise(function(resolve) { + resolve(value); + }); +}; + +SimplePromise.reject = function(reason) { + return new SimplePromise(function(resolve, reject) { + reject(reason); + }); +}; + +SimplePromise.all = function(promises) { + return new SimplePromise(function(resolve, reject) { + var results = []; + var remaining = promises.length; + + if (remaining === 0) { + resolve(results); + return; + } + + function handleResult(index, value) { + results[index] = value; + remaining--; + if (remaining === 0) { + resolve(results); + } + } + + for (var i = 0; i < promises.length; i++) { + (function(index) { + var promise = promises[index]; + if (promise && typeof promise.then === 'function') { + promise.then( + function(value) { handleResult(index, value); }, + reject + ); + } else { + handleResult(index, promise); + } + })(i); + } + }); +}; + +SimplePromise.race = function(promises) { + return new SimplePromise(function(resolve, reject) { + if (promises.length === 0) { + // Per spec, Promise.race with empty array stays pending forever + return; + } + + for (var i = 0; i < promises.length; i++) { + var promise = promises[i]; + if (promise && typeof promise.then === 'function') { + promise.then(resolve, reject); + } else { + resolve(promise); + return; + } + } + }); +}; + +// Make Promise global if not already defined +if (typeof Promise === 'undefined') { + var Promise = SimplePromise; +} + +/** + * Response object that mimics the Fetch API Response + */ +function FetchResponse(jsHttpResponse) { + this._jsResponse = jsHttpResponse; + this.status = jsHttpResponse.statusCode(); + this.ok = this.status >= 200 && this.status < 300; + + // Map HTTP status codes to standard status text + var statusTexts = { + 200: 'OK', + 201: 'Created', + 204: 'No Content', + 301: 'Moved Permanently', + 302: 'Found', + 304: 'Not Modified', + 400: 'Bad Request', + 401: 'Unauthorized', + 403: 'Forbidden', + 404: 'Not Found', + 405: 'Method Not Allowed', + 408: 'Request Timeout', + 409: 'Conflict', + 410: 'Gone', + 500: 'Internal Server Error', + 501: 'Not Implemented', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + 504: 'Gateway Timeout' + }; + + this.statusText = statusTexts[this.status] || (this.ok ? 'OK' : 'Error'); + this.headers = { + get: function(name) { + return jsHttpResponse.header(name); + }, + has: function(name) { + return jsHttpResponse.header(name) !== null; + }, + entries: function() { + var headerMap = jsHttpResponse.headers(); + var entries = []; + for (var key in headerMap) { + if (headerMap.hasOwnProperty(key)) { + entries.push([key, headerMap[key]]); + } + } + return entries; + } + }; +} + +FetchResponse.prototype.text = function() { + var body = this._jsResponse.body(); + return SimplePromise.resolve(body || ''); +}; + +FetchResponse.prototype.json = function() { + var self = this; + return this.text().then(function(text) { + try { + return JSON.parse(text); + } catch (e) { + throw new Error('Invalid JSON: ' + e.message); + } + }); +}; + +FetchResponse.prototype.arrayBuffer = function() { + var bytes = this._jsResponse.bodyBytes(); + return SimplePromise.resolve(bytes); +}; + +FetchResponse.prototype.blob = function() { + // Blob not supported in ES5, return bytes + return this.arrayBuffer(); +}; + +/** + * Fetch API implementation using JavaFetch bridge + * @param {string} url - Request URL + * @param {Object} options - Fetch options (method, headers, body, etc.) + * @returns {Promise} + */ +function fetch(url, options) { + return new SimplePromise(function(resolve, reject) { + try { + // Parse options + options = options || {}; + var method = (options.method || 'GET').toUpperCase(); + var headers = options.headers || {}; + var body = options.body; + + // Prepare request options for JavaFetch + var requestOptions = { + method: method, + headers: {} + }; + + // Convert headers to simple object + if (headers) { + if (typeof headers.forEach === 'function') { + // Headers object + headers.forEach(function(value, key) { + requestOptions.headers[key] = value; + }); + } else if (typeof headers === 'object') { + // Plain object + for (var key in headers) { + if (headers.hasOwnProperty(key)) { + requestOptions.headers[key] = headers[key]; + } + } + } + } + + // Add body if present + if (body !== undefined && body !== null) { + if (typeof body === 'string') { + requestOptions.body = body; + } else if (typeof body === 'object') { + // Assume JSON + requestOptions.body = JSON.stringify(body); + if (!requestOptions.headers['Content-Type'] && !requestOptions.headers['content-type']) { + requestOptions.headers['Content-Type'] = 'application/json'; + } + } + } + + // Call JavaFetch bridge + var jsHttpResponse = JavaFetch.fetch(url, requestOptions); + + // Create Response object + var response = new FetchResponse(jsHttpResponse); + resolve(response); + + } catch (e) { + reject(e); + } + }); +} + +// Export for global use +if (typeof window !== 'undefined') { + window.fetch = fetch; + window.Promise = Promise; +} else if (typeof global !== 'undefined') { + global.fetch = fetch; + global.Promise = Promise; +} From f1b6cd3e1897c5f01d179afaad08ca913ccb09f3 Mon Sep 17 00:00:00 2001 From: yukaidi Date: Fri, 29 May 2026 01:38:24 +0800 Subject: [PATCH 29/35] =?UTF-8?q?fix:=20HttpProxyConf=E6=9E=84=E9=80=A0?= =?UTF-8?q?=E5=99=A8port=E5=AD=97=E6=AE=B5=E4=BB=8E=E6=9C=AA=E8=B5=8B?= =?UTF-8?q?=E5=80=BC=EF=BC=8Ctimeout=E8=A2=AB=E9=87=8D=E5=A4=8D=E8=B5=8B?= =?UTF-8?q?=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BUG-01: this.timeout = DEFAULT_PORT 应为 this.port = DEFAULT_PORT 导致port字段始终为null,代理服务器无法获取正确端口 --- .../main/java/cn/qaiu/vx/core/verticle/conf/HttpProxyConf.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/cn/qaiu/vx/core/verticle/conf/HttpProxyConf.java b/core/src/main/java/cn/qaiu/vx/core/verticle/conf/HttpProxyConf.java index c8c4d5d..b6a5e70 100644 --- a/core/src/main/java/cn/qaiu/vx/core/verticle/conf/HttpProxyConf.java +++ b/core/src/main/java/cn/qaiu/vx/core/verticle/conf/HttpProxyConf.java @@ -32,7 +32,7 @@ public class HttpProxyConf { public HttpProxyConf() { this.username = DEFAULT_USERNAME; this.password = DEFAULT_PASSWORD; - this.timeout = DEFAULT_PORT; + this.port = DEFAULT_PORT; this.timeout = DEFAULT_TIMEOUT; this.preProxyOptions = new ProxyOptions(); } From 66d7a62d3ada78ca2d3a416b9555e9d84e3d19d3 Mon Sep 17 00:00:00 2001 From: yukaidi Date: Fri, 29 May 2026 01:38:53 +0800 Subject: [PATCH 30/35] =?UTF-8?q?fix:=20ReflectionUtil=E6=AD=A3=E5=88=99?= =?UTF-8?q?=E6=8B=BC=E5=86=99=E9=94=99=E8=AF=AFboolen=E5=BA=94=E4=B8=BAboo?= =?UTF-8?q?lean?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BUG-02: boolen拼写错误导致boolean[]类型参数永远不会被识别为基本类型数组 参数绑定失败并抛出RuntimeException --- core/src/main/java/cn/qaiu/vx/core/util/ReflectionUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/cn/qaiu/vx/core/util/ReflectionUtil.java b/core/src/main/java/cn/qaiu/vx/core/util/ReflectionUtil.java index 89a48ba..3d30eee 100644 --- a/core/src/main/java/cn/qaiu/vx/core/util/ReflectionUtil.java +++ b/core/src/main/java/cn/qaiu/vx/core/util/ReflectionUtil.java @@ -241,7 +241,7 @@ public final class ReflectionUtil { public static boolean isBasicTypeArray(CtClass ctClass) { if (!ctClass.isArray()) { return false; - } else return (ctClass.getName().matches("^(boolen|char|byte|short|int|long|float|double|String)\\[]$")); + } else return (ctClass.getName().matches("^(boolean|char|byte|short|int|long|float|double|String)\\[]$")); } /** From 9a3ea050230f3cd2dabd57c1c8c73b6e26825eba Mon Sep 17 00:00:00 2001 From: yukaidi Date: Fri, 29 May 2026 01:39:31 +0800 Subject: [PATCH 31/35] =?UTF-8?q?fix:=20HttpProxyVerticle=E4=BB=A3?= =?UTF-8?q?=E7=90=86=E8=AE=A4=E8=AF=81=E7=BB=95=E8=BF=87=E6=BC=8F=E6=B4=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SEC-01: 修复三个安全问题: 1. split.length<=1时直接放行请求,现在返回403 2. Base64解码无异常处理,现在捕获IllegalArgumentException返回403 3. 日志中明文记录密码,现在只记录用户名 --- .../vx/core/verticle/HttpProxyVerticle.java | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/cn/qaiu/vx/core/verticle/HttpProxyVerticle.java b/core/src/main/java/cn/qaiu/vx/core/verticle/HttpProxyVerticle.java index 64fd990..aa713ea 100644 --- a/core/src/main/java/cn/qaiu/vx/core/verticle/HttpProxyVerticle.java +++ b/core/src/main/java/cn/qaiu/vx/core/verticle/HttpProxyVerticle.java @@ -129,16 +129,25 @@ public class HttpProxyVerticle extends AbstractVerticle { clientRequest.response().setStatusCode(403).end(); return; } - String[] split = new String(Base64.getDecoder().decode(s.replace("Basic ", ""))).split(":"); - if (split.length > 1) { - // TODO - String username = proxyServerConf.getString("username"); - String password = proxyServerConf.getString("password"); - if (!split[0].equals(username) || !split[1].equals(password)) { - LOGGER.info("-----auth failed------\nusername: {}\npassword: {}", username, password); - clientRequest.response().setStatusCode(403).end(); - return; - } + String[] split; + try { + split = new String(Base64.getDecoder().decode(s.replace("Basic ", ""))).split(":"); + } catch (IllegalArgumentException e) { + LOGGER.warn("Proxy-Authorization header is not valid Base64"); + clientRequest.response().setStatusCode(403).end(); + return; + } + if (split.length <= 1) { + LOGGER.warn("Proxy-Authorization header format invalid: missing username:password separator"); + clientRequest.response().setStatusCode(403).end(); + return; + } + String username = proxyServerConf.getString("username"); + String password = proxyServerConf.getString("password"); + if (!split[0].equals(username) || !split[1].equals(password)) { + LOGGER.info("-----auth failed------\nusername: {}", split[0]); + clientRequest.response().setStatusCode(403).end(); + return; } } From c46dfa00a03d792348b98309634698488adc9f8f Mon Sep 17 00:00:00 2001 From: yukaidi Date: Fri, 29 May 2026 01:40:05 +0800 Subject: [PATCH 32/35] =?UTF-8?q?fix:=20ReverseProxyVerticle=20HTTPS?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E7=AB=AF=E5=8F=A3=E5=BA=94=E4=B8=BA443?= =?UTF-8?q?=E8=80=8C=E9=9D=9E80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BUG-03: URL使用https://前缀构造,但默认端口设为80(HTTP) 导致所有未指定端口的HTTPS代理目标连接失败 --- .../java/cn/qaiu/vx/core/verticle/ReverseProxyVerticle.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/cn/qaiu/vx/core/verticle/ReverseProxyVerticle.java b/core/src/main/java/cn/qaiu/vx/core/verticle/ReverseProxyVerticle.java index 977c4d7..7715758 100644 --- a/core/src/main/java/cn/qaiu/vx/core/verticle/ReverseProxyVerticle.java +++ b/core/src/main/java/cn/qaiu/vx/core/verticle/ReverseProxyVerticle.java @@ -351,7 +351,7 @@ public class ReverseProxyVerticle extends AbstractVerticle { String host = url.getHost(); int port = url.getPort(); if (port == -1) { - port = 80; + port = 443; } String originPath = url.getPath(); LOGGER.info("path {}, originPath {}, to {}:{}", path, originPath, host, port); From 710e454fd053efd075b401c3d056b2621bfe8ab8 Mon Sep 17 00:00:00 2001 From: yukaidi Date: Fri, 29 May 2026 01:40:30 +0800 Subject: [PATCH 33/35] =?UTF-8?q?fix:=20dependency=20graph=20=E6=AD=A5?= =?UTF-8?q?=E9=AA=A4=E6=B7=BB=E5=8A=A0=20continue-on-error=EF=BC=8Cfork=20?= =?UTF-8?q?=E4=BB=93=E5=BA=93=E6=9C=AA=E5=90=AF=E7=94=A8=E6=97=B6=E4=B8=8D?= =?UTF-8?q?=E5=BD=B1=E5=93=8D=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/maven.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index a26d259..74581fd 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -60,6 +60,7 @@ jobs: - name: Update dependency graph uses: advanced-security/maven-dependency-submission-action@v3 if: github.event_name != 'pull_request' + continue-on-error: true with: ignore-maven-wrapper: true From 0df01ba3d5b389099403b5c11574b702883e2278 Mon Sep 17 00:00:00 2001 From: yukaidi Date: Fri, 29 May 2026 01:40:38 +0800 Subject: [PATCH 34/35] =?UTF-8?q?fix:=20Deploy=E9=85=8D=E7=BD=AE=E8=AF=BB?= =?UTF-8?q?=E5=8F=96=E5=A4=B1=E8=B4=A5=E6=97=B6=E4=B8=BB=E7=BA=BF=E7=A8=8B?= =?UTF-8?q?=E6=B0=B8=E4=B9=85=E9=98=BB=E5=A1=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BUG-05: 配置读取失败时仅调用printStackTrace,未调用LockSupport.unpark() 导致主线程永远阻塞在LockSupport.park() 现在失败时记录错误日志、unpark主线程并退出进程 --- core/src/main/java/cn/qaiu/vx/core/Deploy.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/cn/qaiu/vx/core/Deploy.java b/core/src/main/java/cn/qaiu/vx/core/Deploy.java index 1645751..09c2f9f 100644 --- a/core/src/main/java/cn/qaiu/vx/core/Deploy.java +++ b/core/src/main/java/cn/qaiu/vx/core/Deploy.java @@ -65,7 +65,11 @@ public final class Deploy { // 读取yml配置 ConfigUtil.readYamlConfig(path.toString(), tempVertx) .onSuccess(this::readConf) - .onFailure(Throwable::printStackTrace); + .onFailure(err -> { + LOGGER.error("读取配置文件失败: {}", err.getMessage(), err); + LockSupport.unpark(mainThread); + System.exit(-1); + }); LockSupport.park(); deployVerticle(); } From 2f7304ab2d605f8b7f790373e3d551a19a4fae60 Mon Sep 17 00:00:00 2001 From: yukaidi Date: Fri, 29 May 2026 01:47:16 +0800 Subject: [PATCH 35/35] =?UTF-8?q?fix:=20Docker=20=E9=95=9C=E5=83=8F?= =?UTF-8?q?=E5=9C=B0=E5=9D=80=E6=94=B9=E4=B8=BA=E5=8A=A8=E6=80=81=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E4=BB=93=E5=BA=93=E5=90=8D=EF=BC=8C=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=20fork=20=E4=BB=93=E5=BA=93=E6=8E=A8=E9=80=81=E8=A2=AB?= =?UTF-8?q?=E6=8B=92=E7=BB=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/maven.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 74581fd..b075d3c 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -98,5 +98,5 @@ jobs: push: true platforms: linux/amd64,linux/arm64,linux/arm/v7 tags: | - ghcr.io/qaiu/netdisk-fast-download:${{ steps.tag.outputs.tag }} - ghcr.io/qaiu/netdisk-fast-download:latest + ghcr.io/${{ github.repository }}:${{ steps.tag.outputs.tag }} + ghcr.io/${{ github.repository }}:latest