修复 Jackson 3 迁移引入的看图 500,并大幅补充单元测试
生产缺陷(2026-09-14 Spring Boot 4.1.1 / Jackson 2→3 迁移引入): - GalleryUtil.parseImageKeys 解析 mpv 页 imagelist 时,行尾分号属 JS 语法, Jackson 3 默认 FAIL_ON_TRAILING_TOKENS=true 会抛 StreamReadException。 改为先由 parseImagelist 剥掉 JS 语句外壳(前缀 + 行尾分号)再交给 JSON 解析。 已用线上真实页面验证:修复前必抛异常,修复后正确解析出 154 页 imgkey。 - GalleryManageService.cacheImagesKey 只 catch IOException,而 Jackson 3 的 JacksonException 继承 RuntimeException,异常穿透 Controller(无 @ControllerAdvice) 表现为 HTTP 500。改为同时兜 JacksonException 并按业务失败返回。 同时用 parseGidFromLink 拦住段数不足的畸形链接(原先会抛 ArrayIndexOutOfBoundsException 变成 500)。 既有行为修正: - PersonalService.deleteFile 原先依据 hutool FileUtil.del 的返回值报成功, 而该函数对不存在的目标返回 true,导致路径写错也显示「删除成功」; 改为先校验存在性,不存在则回「文件不存在」。 - PersonalService.getFiles 的 listFiles()==null 分支原先报「文件夹为空」, 但该返回值只在 I/O 出错时出现,会掩盖权限/磁盘问题,改为「读取文件夹失败」。 测试:143 → 379 用例,指令覆盖率 36.2% → 80.2%(分支 34.6% → 69.8%)。 新增控制器(MockMvc standaloneSetup)、服务层、拦截器/过滤器、WebSocket、 配置装配与 MyBatis AOT hints 的测试;全部离线可跑,不加载 Spring 上下文、不连库、不占端口。
This commit is contained in:
@@ -11,6 +11,7 @@ import com.lion.lionwebsite.Util.ImageFileCache;
|
||||
import java.nio.file.Path;
|
||||
import com.lion.lionwebsite.Util.GalleryUtil;
|
||||
import com.lion.lionwebsite.Util.Response;
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
@@ -371,7 +372,17 @@ public class GalleryManageService {
|
||||
|
||||
public String cacheImagesKey(String url) {
|
||||
Response response = Response.generateResponse();
|
||||
String gid = String.valueOf(GalleryUtil.parseGid(url));
|
||||
|
||||
// 畸形链接在这里就拦掉:段数不足会让 url.split("/")[5] 抛 ArrayIndexOutOfBoundsException,
|
||||
// 而 parseGid 也会对无 /g/ 的链接返回 null,两者都不该变成 500。
|
||||
String[] segments = url == null ? null : url.split("/");
|
||||
Integer parsedGid = parseGidFromLink(url);
|
||||
if (parsedGid == null || segments == null || segments.length <= 5) {
|
||||
response.failure("链接错误");
|
||||
return response.toJSONString();
|
||||
}
|
||||
|
||||
String gid = String.valueOf(parsedGid);
|
||||
GidToKey gidToKey = imageCacheMapper.selectKeyByGid(gid);
|
||||
//已缓存过,直接返回
|
||||
if(gidToKey != null) {
|
||||
@@ -381,7 +392,7 @@ public class GalleryManageService {
|
||||
try {
|
||||
gidToKey = new GidToKey();
|
||||
gidToKey.setGid(gid);
|
||||
gidToKey.setKey(url.split("/")[5].strip());
|
||||
gidToKey.setKey(segments[5].strip());
|
||||
ArrayList<ImageKeyCache> imageKeyCaches = GalleryUtil.parseImageKeys(url);
|
||||
if(imageKeyCaches == null)
|
||||
return response.failure("该图片已下架或已被删除").toJSONString();
|
||||
@@ -390,8 +401,10 @@ public class GalleryManageService {
|
||||
for (ImageKeyCache imageKeyCache : imageKeyCaches)
|
||||
imageCacheMapper.insertImageKeyCache(imageKeyCache);
|
||||
response.success(objectMapper.valueToTree(gidToKey));
|
||||
}catch (IOException e){
|
||||
log.error(e.getMessage());
|
||||
}catch (IOException | JacksonException e){
|
||||
// Jackson 3 的解析异常继承 RuntimeException 而非 IOException,
|
||||
// 只捕 IOException 会漏掉第三方页面格式变化,穿透成 500。
|
||||
log.warn("缓存图片索引失败 gid={} errorType={}", gid, e.getClass().getSimpleName());
|
||||
response.failure("网络波动或其他异常");
|
||||
}
|
||||
return response.toJSONString();
|
||||
|
||||
@@ -117,7 +117,9 @@ public class PersonalService{
|
||||
response.success(new ObjectMapper().valueToTree(fileMaps).toString());
|
||||
}
|
||||
else
|
||||
response.failure("文件夹为空");
|
||||
// listFiles() 只在 I/O 出错时返回 null(可读空目录返回长度为 0 的数组),
|
||||
// 报「文件夹为空」会掩盖权限/磁盘问题,这里如实提示。
|
||||
response.failure("读取文件夹失败");
|
||||
}
|
||||
return response.toJSONString();
|
||||
}
|
||||
@@ -359,7 +361,11 @@ public class PersonalService{
|
||||
path = URLDecoder.decode(path, StandardCharsets.UTF_8);
|
||||
File file = new File(path);
|
||||
|
||||
if(FileUtil.del(file))
|
||||
// hutool 的 FileUtil.del 对不存在的目标返回 true(幂等语义),
|
||||
// 直接据此报成功会让「路径写错/文件已不存在」也回「删除成功」,前端误以为已删除。
|
||||
if (!file.exists())
|
||||
response.failure("文件不存在");
|
||||
else if (FileUtil.del(file))
|
||||
response.success("删除成功");
|
||||
else
|
||||
response.failure("删除失败");
|
||||
|
||||
@@ -185,7 +185,7 @@ public class GalleryUtil {
|
||||
ArrayList<ImageKeyCache> imageKeyCaches = new ArrayList<>();
|
||||
AtomicInteger page = new AtomicInteger(1);
|
||||
gid2MpvKey.put(gid, scripts[1].split("=")[1].replace(";", "").replace("\"", "").replace(" ", ""));
|
||||
JsonNode nodes = objectMapper.readValue(scripts[2].replace("var imagelist = ", ""), JsonNode.class);
|
||||
JsonNode nodes = parseImagelist(scripts[2]);
|
||||
nodes.forEach((n) -> {
|
||||
ImageKeyCache imageKeyCache = new ImageKeyCache();
|
||||
imageKeyCache.setGid(gid);
|
||||
@@ -196,6 +196,19 @@ public class GalleryUtil {
|
||||
return imageKeyCaches;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 mpv 页里的 imagelist 行。
|
||||
* 该行是 JS 赋值语句(`var imagelist = [...];`),行尾分号属 JavaScript 语法而非 JSON,
|
||||
* 因此先剥掉「var imagelist = 」前缀与行尾分号,再交给 JSON 解析。
|
||||
* Jackson 3 默认开启 FAIL_ON_TRAILING_TOKENS,若把分号留给它会在解析时报错。
|
||||
*/
|
||||
private static JsonNode parseImagelist(String scriptLine) {
|
||||
String json = scriptLine.replace("var imagelist = ", "").trim();
|
||||
if (json.endsWith(";"))
|
||||
json = json.substring(0, json.length() - 1).trim();
|
||||
return objectMapper.readValue(json, JsonNode.class);
|
||||
}
|
||||
|
||||
public static String getMpvKey(String url){
|
||||
String gid = String.valueOf(parseGid(url));
|
||||
String key = gid2MpvKey.get(gid);
|
||||
|
||||
Reference in New Issue
Block a user