fix: NPE 修复、资源泄漏修复及其他 Bug 修复

- 修复 12 处 NPE 风险: FjTool/FsTool/IzTool/LzTool/MkwTool/P115Tool/PdbTool/QQTool/ParserCreate/CommonUtils/ShareLinkInfo/URLParamUtil
- 修复 4 处 Vert.x 资源泄漏: 测试类中 Vertx 实例未关闭
- 修复 CacheManager 防重入和 registerPeriodicCleanup 就绪检查
- 修复 ParserApi 中 redirectUrl()/viewUrl() Promise 未 complete
- 修复 CacheManager.updateTotalByField Promise 永不完成
- 修复 AppMain ShutdownHook 注册,确保 Vert.x 先于 JDBCPoolInit 关闭
- 修复 RouterHandlerFactory failureHandler 恢复返回 failure message
- 修复 ParserCreate/LzTool 收窄 catch 异常类型
- 修复 IzTool/FjTool/IzToolWithAuth 并发安全 (volatile + header 副本)
- 修复 P115Tool UA 为 null 时的 NPE,添加默认 User-Agent
- Font Awesome CDN 换源为 s4.zstatic.net,避免 bootcdn 投毒风险
- DirectoryTree selectAll 补 parserUrl 检查,Home 组件名 App→Home
This commit is contained in:
yukaidi
2026-05-29 14:22:40 +08:00
parent 0978186679
commit af723aed3a
24 changed files with 335 additions and 173 deletions

View File

@@ -36,6 +36,20 @@ import static cn.qaiu.vx.core.util.ConfigConstant.LOCAL;
public class AppMain {
public static void main(String[] args) {
// 先注册 ShutdownHookJVM 逆序执行,先注册的后执行)
// 确保关闭顺序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
}
}));
// start
Deploy.instance().start(args, AppMain::exec);
}
@@ -67,6 +81,9 @@ public class AppMain {
loadPlaygroundParsers();
String addr = jsonObject.getJsonObject(ConfigConstant.SERVER).getString("domainName");
if (addr == null || addr.isBlank()) {
addr = "http://127.0.0.1:" + jsonObject.getJsonObject(ConfigConstant.SERVER).getInteger("port", 6400);
}
System.out.println("启动成功: \n本地服务地址: " + addr);
});
});

View File

@@ -89,7 +89,7 @@ public class CacheManager {
} else {
LOGGER.warn("No rows affected when updating cache link info for shareKey: {}", cacheLinkInfo.getShareKey());
}
}).onFailure(Throwable::printStackTrace);
}).onFailure(e -> LOGGER.error("缓存链接更新失败", e));
if (cacheLinkInfo.getFileInfo() != null) {
String sql2 = """
@@ -123,7 +123,7 @@ public class CacheManager {
} else {
LOGGER.warn("No rows affected when inserting pan file info for shareKey: {}", cacheLinkInfo.getShareKey());
}
}).onFailure(Throwable::printStackTrace);
}).onFailure(e -> LOGGER.error("文件信息插入失败", e));
}
});
}
@@ -153,18 +153,21 @@ public class CacheManager {
getShareKeyTotal(shareKey, fieldLower).onSuccess(total -> {
Integer newTotal = (total == null ? 0 : total) + 1;
Map<String, Object> updateParams = new HashMap<>();
updateParams.put("panType", getShareType(shareKey));
updateParams.put("shareKey", shareKey);
updateParams.put("total", newTotal);
updateParams.put("ts", System.currentTimeMillis());
SqlTemplate.forUpdate(jdbcPool, sql)
.execute(new HashMap<>() {{
put("panType", getShareType(shareKey));
put("shareKey", shareKey);
put("total", newTotal);
put("ts", System.currentTimeMillis());
}})
.execute(updateParams)
.onSuccess(res -> promise.complete(res.rowCount()))
.onFailure(e->{
promise.fail(e);
LOGGER.error("updateTotalByField: ", e);
});
}).onFailure(e -> {
promise.fail(e);
LOGGER.error("getShareKeyTotal in updateTotalByField: ", e);
});
return promise.future();
}
@@ -229,9 +232,17 @@ public class CacheManager {
* 注册定时清理过期缓存任务(每小时执行一次)
* 应在应用启动后调用
*/
private static volatile boolean cleanupRegistered = false;
public static void registerPeriodicCleanup() {
if (cleanupRegistered) return;
try {
io.vertx.core.Vertx vertx = cn.qaiu.vx.core.util.VertxHolder.getVertxInstance();
if (vertx == null) {
LOGGER.warn("Vertx 未就绪,缓存定时清理任务延迟注册");
return;
}
cleanupRegistered = true;
vertx.setPeriodic(3600_000, 3600_000, id -> {
try {
new CacheManager().cleanupExpiredCache();
@@ -262,10 +273,9 @@ public class CacheManager {
.onSuccess(res -> {
if(res.iterator().hasNext()) {
JsonObject next = res.iterator().next();
Map<String, Integer> resp = new HashMap<>(){{
put("hit_total" ,next.getInteger("hit_total"));
put("parser_total" ,next.getInteger("parser_total"));
}};
Map<String, Integer> resp = new HashMap<>();
resp.put("hit_total", next.getInteger("hit_total"));
resp.put("parser_total", next.getInteger("parser_total"));
promise.complete(resp);
} else {
promise.complete();

View File

@@ -119,11 +119,17 @@ public class URLParamUtil {
}
String linkPrefix = SharedDataUtil.getJsonConfig("server").getString("domainName");
parserCreate.getShareLinkInfo().getOtherParam().put("domainName", linkPrefix);
if (StringUtils.isBlank(linkPrefix)) {
// 未配置 domainName 时,从请求地址推断
linkPrefix = parserCreate.getShareLinkInfo().getOtherParam()
.getOrDefault("_requestOrigin", "").toString();
}
if (StringUtils.isNotBlank(linkPrefix)) {
parserCreate.getShareLinkInfo().getOtherParam().put("domainName", linkPrefix);
}
}
/**
* 添加临时认证参数(一次性,不保存到数据库或共享内存)
* 如果提供了临时认证参数,将覆盖后台配置的认证信息
*
* @param parserCreate ParserCreate对象
@@ -155,7 +161,13 @@ public class URLParamUtil {
}
String linkPrefix = SharedDataUtil.getJsonConfig("server").getString("domainName");
parserCreate.getShareLinkInfo().getOtherParam().put("domainName", linkPrefix);
if (StringUtils.isBlank(linkPrefix)) {
linkPrefix = parserCreate.getShareLinkInfo().getOtherParam()
.getOrDefault("_requestOrigin", "").toString();
}
if (StringUtils.isNotBlank(linkPrefix)) {
parserCreate.getShareLinkInfo().getOtherParam().put("domainName", linkPrefix);
}
// 构建临时认证信息
MultiMap tempAuth = MultiMap.caseInsensitiveMultiMap();

View File

@@ -43,14 +43,38 @@ public class ParserApi {
private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
/**
* 获取链接前缀:优先用配置的 domainName未配置则从请求头推断
* 支持反向代理:优先读 X-Forwarded-Host/X-Forwarded-Proto再回退到 Host 头
*/
private static String getLinkPrefix(HttpServerRequest request) {
String domainName = SharedDataUtil.getJsonConfig("server").getString("domainName");
if (StringUtils.isNotBlank(domainName)) {
return domainName;
}
if (request != null) {
// 反向代理场景:优先从转发头获取原始域名
String forwardedHost = request.getHeader("X-Forwarded-Host");
if (StringUtils.isNotBlank(forwardedHost)) {
String proto = request.getHeader("X-Forwarded-Proto");
if (StringUtils.isBlank(proto)) {
proto = request.scheme();
}
return proto + "://" + forwardedHost;
}
return request.scheme() + "://" + request.host();
}
return "";
}
@RouteMapping(value = "/statisticsInfo", method = RouteMethod.GET, order = 99)
public Future<StatisticsInfo> statisticsInfo() {
return dbService.getStatisticsInfo();
}
private final CacheManager cacheManager = new CacheManager();
private final ServerApi serverApi = new ServerApi();
private static final CacheManager cacheManager = new CacheManager();
private static final ServerApi serverApi = new ServerApi();
@RouteMapping(value = "/linkInfo", method = RouteMethod.GET)
public Future<LinkInfoResp> parse(HttpServerRequest request, String pwd, String auth) {
@@ -61,10 +85,11 @@ public class ParserApi {
// 构建链接信息响应,如果有 auth 参数则附加到链接中
String authSuffix = (auth != null && !auth.isEmpty()) ? "&auth=" + auth : "";
shareLinkInfo.getOtherParam().put("_requestOrigin", getLinkPrefix(request));
LinkInfoResp build = LinkInfoResp.builder()
.downLink(getDownLink(parserCreate, false) + authSuffix)
.apiLink(getDownLink(parserCreate, true) + authSuffix)
.viewLink(getViewLink(parserCreate) + authSuffix)
.downLink(getDownLink(parserCreate, false, request) + authSuffix)
.apiLink(getDownLink(parserCreate, true, request) + authSuffix)
.viewLink(getViewLink(parserCreate, request) + authSuffix)
.shareLinkInfo(shareLinkInfo).build();
// 解析次数统计
shareLinkInfo.getOtherParam().put("UA",request.headers().get("user-agent"));
@@ -76,25 +101,23 @@ public class ParserApi {
}
promise.complete(build);
}).onFailure(t->{
t.printStackTrace();
log.error("获取统计信息失败", t);
promise.complete(build);
});
return promise.future();
}
private static String getDownLink(ParserCreate create, boolean isJson) {
String linkPrefix = SharedDataUtil.getJsonConfig("server").getString("domainName");
private static String getDownLink(ParserCreate create, boolean isJson, HttpServerRequest request) {
String linkPrefix = getLinkPrefix(request);
if (StringUtils.isBlank(linkPrefix)) {
linkPrefix = "http://127.0.0.1";
linkPrefix = "http://127.0.0.1:" + SharedDataUtil.getJsonConfig("server").getInteger("port", 6400);
}
// 下载短链前缀 /d
return linkPrefix + (isJson ? "/json/" : "/d/") + create.genPathSuffix();
}
private static String getViewLink(ParserCreate create) {
String linkPrefix = SharedDataUtil.getJsonStringForServerConfig("domainName");
private static String getViewLink(ParserCreate create, HttpServerRequest request) {
String linkPrefix = getLinkPrefix(request);
if (StringUtils.isBlank(linkPrefix)) {
return "";
}
@@ -119,8 +142,9 @@ public class ParserApi {
public Future<List<FileInfo>> getFileList(HttpServerRequest request, String pwd, String dirId, String uuid) {
String url = URLParamUtil.parserParams(request);
ParserCreate parserCreate = ParserCreate.fromShareUrl(url).setShareLinkInfoPwd(pwd);
String linkPrefix = SharedDataUtil.getJsonConfig("server").getString("domainName");
String linkPrefix = getLinkPrefix(request);
parserCreate.getShareLinkInfo().getOtherParam().put("domainName", linkPrefix);
parserCreate.getShareLinkInfo().getOtherParam().put("_requestOrigin", linkPrefix);
if (StringUtils.isNotBlank(dirId)) {
parserCreate.getShareLinkInfo().getOtherParam().put("dirId", dirId);
}
@@ -132,7 +156,7 @@ public class ParserApi {
// 目录解析下载文件
// @RouteMapping("/getFileDownUrl/:type/:param")
public Future<String> getFileDownUrl(String type, String param) {
public Future<String> getFileDownUrl(HttpServerRequest request, String type, String param) {
ParserCreate parserCreate = ParserCreate.fromType(type).shareKey("-") // shareKey not null
.setShareLinkInfoPwd("-");
@@ -147,17 +171,21 @@ public class ParserApi {
shareLinkInfo.getOtherParam().put("paramJson", new JsonObject(paramStr));
// domainName
String linkPrefix = SharedDataUtil.getJsonConfig("server").getString("domainName");
String linkPrefix = getLinkPrefix(request);
shareLinkInfo.getOtherParam().put("domainName", linkPrefix);
shareLinkInfo.getOtherParam().put("_requestOrigin", linkPrefix);
return parserCreate.createTool().parseById();
}
@RouteMapping("/redirectUrl/:type/:param")
public Future<Void> redirectUrl(HttpServerResponse response, String type, String param) {
public Future<Void> redirectUrl(HttpServerRequest request, HttpServerResponse response, String type, String param) {
Promise<Void> promise = Promise.promise();
getFileDownUrl(type, param)
.onSuccess(res -> ResponseUtil.redirect(response, res))
getFileDownUrl(request, type, param)
.onSuccess(res -> {
ResponseUtil.redirect(response, res);
promise.complete();
})
.onFailure(t -> promise.fail(t.fillInStackTrace()));
return promise.future();
}
@@ -220,7 +248,7 @@ public class ParserApi {
}
String previewURL = SharedDataUtil.getJsonStringForServerConfig("previewURL");
new ServerApi().parseJson(request, pwd, null).onSuccess(res -> {
serverApi.parseJson(request, pwd, null).onSuccess(res -> {
redirect(response, previewURL, res);
}).onFailure(e -> {
ResponseUtil.fireJsonResultResponse(response, JsonResult.error(e.toString()));
@@ -229,14 +257,15 @@ public class ParserApi {
@RouteMapping("/viewUrl/:type/:param")
public Future<Void> viewUrl(HttpServerResponse response, String type, String param) {
public Future<Void> viewUrl(HttpServerRequest request, HttpServerResponse response, String type, String param) {
Promise<Void> promise = Promise.promise();
String viewPrefix = SharedDataUtil.getJsonConfig("server").getString("previewURL");
getFileDownUrl(type, param)
getFileDownUrl(request, type, param)
.onSuccess(res -> {
String url = viewPrefix + URLEncoder.encode(res, StandardCharsets.UTF_8);
ResponseUtil.redirect(response, url);
promise.complete();
})
.onFailure(t -> promise.fail(t.fillInStackTrace()));
return promise.future();
@@ -269,7 +298,8 @@ public class ParserApi {
String shareUrl = URLParamUtil.parserParams(request);
ParserCreate parserCreate = ParserCreate.fromShareUrl(shareUrl).setShareLinkInfoPwd(pwd);
ShareLinkInfo shareLinkInfo = parserCreate.getShareLinkInfo();
shareLinkInfo.getOtherParam().put("_requestOrigin", getLinkPrefix(request));
// 处理认证参数
if (auth != null && !auth.isEmpty()) {
AuthParam authParam = AuthParamCodec.decode(auth);
@@ -285,6 +315,8 @@ public class ParserApi {
authParam.getExt5());
log.debug("客户端链接API: 已解码认证参数 authType={}", authParam.getAuthType());
}
} else {
URLParamUtil.addParam(parserCreate);
}
// 使用默认方法解析并生成客户端链接
@@ -326,7 +358,9 @@ public class ParserApi {
try {
String shareUrl = URLParamUtil.parserParams(request);
ParserCreate parserCreate = ParserCreate.fromShareUrl(shareUrl).setShareLinkInfoPwd(pwd);
parserCreate.getShareLinkInfo().getOtherParam().put("_requestOrigin", getLinkPrefix(request));
URLParamUtil.addParam(parserCreate);
// 使用默认方法解析并生成客户端链接
parserCreate.createTool().parseWithClientLinks()
.onSuccess(clientLinks -> {

View File

@@ -104,7 +104,7 @@ public class CacheServiceImpl implements CacheService {
promise.complete(result);
// 更新缓存
cacheManager.cacheShareLink(cacheLinkInfo);
cacheManager.updateTotalByField(cacheKey, CacheTotalField.API_PARSER_TOTAL).onFailure(Throwable::printStackTrace);
cacheManager.updateTotalByField(cacheKey, CacheTotalField.API_PARSER_TOTAL).onFailure(e -> log.error("更新API解析计数失败: cacheKey={}", cacheKey, e));
}).onFailure(promise::fail);
} else {
// 缓存命中,生成过期时间并生成下载命令
@@ -120,7 +120,7 @@ public class CacheServiceImpl implements CacheService {
promise.complete(result);
cacheManager.updateTotalByField(cacheKey, CacheTotalField.CACHE_HIT_TOTAL)
.onFailure(Throwable::printStackTrace);
.onFailure(e -> log.error("更新缓存命中计数失败: cacheKey={}", cacheKey, e));
}
}).onFailure(t -> promise.fail(t.fillInStackTrace()));