一次未审查的 AI Agent 操作引发的 OSGi 缓存悬案
前一篇 把 launch.json 里 sourcePaths 失效的表象扒了个底朝天。本以为故事到那里就收尾了,没想到真正的元凶还藏在更深处:OSGi 缓存让扩展目录里的 JAR 改了等于没改,运行时照旧加载旧版本。而这份 JAR 之所以被改,起因是一次没盯紧的 AI Agent 操作。更戏剧的是,这颗子弹飞了一圈,最后打中的是作者本人。
问题现象
在 Trae CN 中调试 Spring Boot 应用时,对 JAR 中的类(如 DefaultApplicationArguments)设断点命中后,VS Code 打开的是 jdt:// 反编译视图,而非 sourcePaths 指向的 external/ 目录中的源码文件。而通过类名覆盖放在 src/main/java/ 下的 SpringApplication.java 可以正确打开源码。
排查思路
确认 sourcePaths 是否传入
在 launch.json 中配置了 sourcePaths 指向 external/spring-boot-2.7.18-sources 目录。通过 DAP 日志确认 attach 请求中 sourcePaths 参数已正确传入:
"sourcePaths": ["d:/project/external/java-project/external/spring-boot-2.7.18-sources"]
排除假设:sourcePaths 未传入。
用 Arthas 确认 convertDebuggerSourceToClient 的参数
Arthas 附加到 JDT LS 进程(PID 25216),观察 convertDebuggerSourceToClient 方法的入参:
watch com.microsoft.java.debug.core.adapter.handler.StackTraceRequestHandler convertDebuggerSourceToClient '{params[0], params[1], params[2]}' 'params[0].contains("Default")' -x 3 -n 1
结果:
params[0] = org.springframework.boot.DefaultApplicationArguments (全限定名)
params[1] = DefaultApplicationArguments.java (sourceName)
params[2] = org\springframework\boot\DefaultApplicationArguments.java (relativeSourcePath)
relativeSourcePath 是包相对路径,格式正确。如果 sourceLookup 被调用,理论上可以拼出正确路径找到文件。
确认 sourceLookup 是否被调用
watch com.microsoft.java.debug.core.adapter.AdapterUtils sourceLookup '{params, returnObj}' -x 3 -n 1
结果:方法从未被触发。
这说明 convertDebuggerSourceToClient 内部没有走到 sourceLookup 逻辑。代码在某个分支提前返回了。
用 Arthas 反编译运行时的实际代码
jad com.microsoft.java.debug.core.adapter.handler.StackTraceRequestHandler convertDebuggerSourceToClient
反编译结果显示运行时代码的关键分支:
if (!StringUtils.isBlank(uri)) {
if (uri.startsWith("file:")) {
return new Types.Source(sourceName, clientPath, sourceReference);
}
// 非 file: URI 直接返回,完全跳过 sourceLookup
return new Types.Source(sourceName, uri, sourceReference);
}
String absoluteSourcepath = AdapterUtils.sourceLookup(context.getSourcePaths(), relativeSourcePath);
当 JDT 返回 jdt:// URI(非 file: 开头),代码直接 return,完全跳过 sourceLookup。这就是根因:运行时代码没有 sourcePaths 回退逻辑。
对比参考源码与运行时代码
参考源码中的 convertDebuggerSourceToClient 有 resolveSourceFromSourcePaths 方法,会在非 file: URI 时先尝试 sourcePaths 回退。但 Arthas 的 sm 命令确认运行时类中不存在此方法:
sm com.microsoft.java.debug.core.adapter.handler.StackTraceRequestHandler * -d
方法列表中只有 convertDebuggerSourceToClient,没有 resolveSourceFromSourcePaths。运行时版本和参考源码版本不一致。
提取部署版本对比
从两个扩展目录分别提取 plugin JAR,再从 plugin JAR 中提取内嵌的 core JAR,最后反编译 StackTraceRequestHandler 类进行 diff 对比。
用 Git Bash 执行提取和对比:
# 创建输出目录
mkdir -p compare/trae compare/vscode
# 从 Trae 扩展目录的 plugin JAR 提取 core JAR 和 class 文件
cd compare/trae
unzip -o "C:/Users/Administrator/.trae-cn/extensions/vscjava.vscode-java-debug-0.59.0-universal/server/com.microsoft.java.debug.plugin-0.53.2.jar" "lib/com.microsoft.java.debug.core-0.53.2.jar"
unzip -o "lib/com.microsoft.java.debug.core-0.53.2.jar" "com/microsoft/java/debug/core/adapter/handler/StackTraceRequestHandler.class"
# 从 VS Code 扩展目录的 plugin JAR 提取 core JAR 和 class 文件
cd ../vscode
unzip -o "C:/Users/Administrator/.vscode/extensions/vscjava.vscode-java-debug-0.59.0/server/com.microsoft.java.debug.plugin-0.53.2.jar" "lib/com.microsoft.java.debug.core-0.53.2.jar"
unzip -o "lib/com.microsoft.java.debug.core-0.53.2.jar" "com/microsoft/java/debug/core/adapter/handler/StackTraceRequestHandler.class"
# 用 javap 反编译 class 文件
cd ..
javap -c -p trae/StackTraceRequestHandler.class > trae/StackTraceRequestHandler.trae.txt
javap -c -p vscode/StackTraceRequestHandler.class > vscode/StackTraceRequestHandler.vscode.txt
# 生成 diff
diff -u trae/StackTraceRequestHandler.trae.txt vscode/StackTraceRequestHandler.vscode.txt > StackTraceRequestHandler.diff.txt
对比结果:
- Trae 扩展目录中的版本:非
file:URI 时先调用resolveSourceFromSourcePaths(),尝试sourcePaths回退,找不到才返回jdt://URI - VS Code 扩展目录中的版本:非
file:URI 时直接返回jdt://URI,无sourcePaths回退
确认运行时加载的是哪个版本
Arthas jad 输出中包含 ClassLoader 信息:
ClassLoader: org.eclipse.osgi.internal.loader.EquinoxClassLoader@76ef1c90[com.microsoft.java.debug.plugin:0.53.2(id=109)]
Location: /C:/Users/Administrator/AppData/Roaming/Trae CN/User/globalStorage/redhat.java/1.55.0/config_win/org.eclipse.osgi/109/0/.cp/lib/com.microsoft.java.debug.core-0.53.2.jar
JDT LS 从 OSGi 缓存加载 core JAR,不是从扩展目录直接加载。
SHA256 对比三个来源
提取三处 core JAR 并计算哈希:
# OSGi 缓存
sha256sum "C:/Users/Administrator/AppData/Roaming/Trae CN/User/globalStorage/redhat.java/1.55.0/config_win/org.eclipse.osgi/109/0/.cp/lib/com.microsoft.java.debug.core-0.53.2.jar"
# Trae 扩展目录(先提取内嵌 core JAR)
unzip -o -j "C:/Users/Administrator/.trae-cn/extensions/vscjava.vscode-java-debug-0.59.0-universal/server/com.microsoft.java.debug.plugin-0.53.2.jar" "lib/com.microsoft.java.debug.core-0.53.2.jar" -d /tmp/trae_core
sha256sum /tmp/trae_core/com.microsoft.java.debug.core-0.53.2.jar
# VS Code 扩展目录(先提取内嵌 core JAR)
unzip -o -j "C:/Users/Administrator/.vscode/extensions/vscjava.vscode-java-debug-0.59.0/server/com.microsoft.java.debug.plugin-0.53.2.jar" "lib/com.microsoft.java.debug.core-0.53.2.jar" -d /tmp/vscode_core
sha256sum /tmp/vscode_core/com.microsoft.java.debug.core-0.53.2.jar
| 来源 | 大小 | SHA256 |
|---|---|---|
| OSGi 缓存 | 420,005 | 4F84C051... |
| Trae 扩展目录 | 423,586 | 225EC344... |
| VS Code 扩展目录 | 457,397 | 57DCF47D... |
三个 core JAR 全部不同。
再对比 StackTraceRequestHandler.class 的 SHA256:
# 从各 core JAR 中提取 class 文件
unzip -o -j /tmp/trae_core/com.microsoft.java.debug.core-0.53.2.jar "com/microsoft/java/debug/core/adapter/handler/StackTraceRequestHandler.class" -d /tmp/cls_trae
unzip -o -j /tmp/vscode_core/com.microsoft.java.debug.core-0.53.2.jar "com/microsoft/java/debug/core/adapter/handler/StackTraceRequestHandler.class" -d /tmp/cls_vscode
unzip -o -j "C:/Users/Administrator/AppData/Roaming/Trae CN/User/globalStorage/redhat.java/1.55.0/config_win/org.eclipse.osgi/109/0/.cp/lib/com.microsoft.java.debug.core-0.53.2.jar" "com/microsoft/java/debug/core/adapter/handler/StackTraceRequestHandler.class" -d /tmp/cls_osgi
sha256sum /tmp/cls_osgi/StackTraceRequestHandler.class
sha256sum /tmp/cls_trae/StackTraceRequestHandler.class
sha256sum /tmp/cls_vscode/StackTraceRequestHandler.class
| 来源 | SHA256 |
|---|---|
| OSGi 缓存 | E896B36F... |
| Trae 扩展目录 | 1F3D6403... |
| VS Code 扩展目录 | E896B36F... |
OSGi 缓存中的类文件与 VS Code 版本字节级一致,与 Trae 扩展目录中的版本不同。
到这里作者下了第一个结论:JDT LS 运行时加载的是 VS Code 版本的旧 JAR,而非 Trae 扩展目录中的新版 JAR。当时顺理成章的推断是——Trae CN 复用了 VS Code 的缓存。这个判断对了一半,也错了一半,后面会看到它怎么把作者带进沟里。
缓存问题分析
涉及的文件
plugin JAR(外壳):
C:\Users\Administrator\.trae-cn\extensions\vscjava.vscode-java-debug-0.59.0-universal\server\com.microsoft.java.debug.plugin-0.53.2.jar— Trae 扩展目录,含新版 core JARC:\Users\Administrator\.vscode\extensions\vscjava.vscode-java-debug-0.59.0\server\com.microsoft.java.debug.plugin-0.53.2.jar— VS Code 扩展目录,含旧版 core JAR
core JAR(内嵌在 plugin JAR 的 lib/ 下):
com.microsoft.java.debug.core-0.53.2.jar— 包含StackTraceRequestHandler等核心调试逻辑类
OSGi 缓存目录:
C:\Users\Administrator\AppData\Roaming\Trae CN\User\globalStorage\redhat.java\1.55.0\config_win\org.eclipse.osgi\
├── .manager\
│ ├── .fileTable.6
│ ├── .fileTable.7
│ └── .fileTableLock
├── framework.info.6 ← 序列化的 bundle 注册表
├── 109\ ← com.microsoft.java.debug.plugin:0.53.2
│ └── 0\
│ └── .cp\ ← Bundle-ClassPath 解压目录
│ └── lib\
│ ├── com.microsoft.java.debug.core-0.53.2.jar ← 旧版
│ ├── commons-io-2.19.0.jar
│ ├── reactive-streams-1.0.4.jar
│ └── rxjava-2.2.21.jar
├── 108\
├── 12\
├── 141\
├── 53\
├── 56\
└── 66\
OSGi 缓存机制
JDT LS 通过 Eclipse Equinox OSGi 框架加载插件。启动参数中 -configuration 指向 config_win 目录。java-debug 插件被注册为 OSGi bundle(编号 109)。
安装流程:
- JDT LS 启动,
redhat.java扩展读取javaExtensions配置 vscjava.vscode-java-debug扩展注册其 plugin JAR 路径- OSGi 安装 plugin JAR 为 bundle,解压
Bundle-ClassPath中声明的内嵌 JAR 到.cp/lib/目录 - 后续启动时,OSGi 检测到 bundle 已安装,复用
.cp/lib/下的缓存文件
缓存失效场景:
排查过程中,Trae 扩展目录里的 plugin JAR 内容发生了变化(内嵌的 core JAR 被替换为新版),但版本号仍是 0.53.2。OSGi 比较的是 bundle 的 location 和 version,两者都未变,所以判定为同一 bundle,直接复用缓存中的旧版 core JAR,不会重新从扩展目录解压。至于这份 JAR 为什么会被换掉,后文"一段误判与回旋镖"会详细交代。
JDT LS 维护者在社区讨论中也确认了这一行为:OSGi bundle 安装器仅在 bundle 版本号不同时才会卸载旧 bundle 并安装新 bundle,版本号相同则直接复用缓存。
OSGi 缓存源码分析
查看 JDT LS 的 BundleUtils.loadBundles 方法源码。该方法在 JDT LS 初始化时被 InitHandler 调用,负责将扩展目录中的 plugin JAR 安装为 OSGi bundle。
VS Code 扩展端通过 collectJavaExtensions 收集所有扩展声明的 javaExtensions 路径,传入 JDT LS 的初始化选项:
export function collectJavaExtensions(extensions: readonly vscode.Extension<any>[]): string[] {
const result = [];
for (const extension of extensions) {
const contributesSection = extension.packageJSON['contributes'];
if (contributesSection) {
const javaExtensions = contributesSection['javaExtensions'];
if (Array.isArray(javaExtensions) && javaExtensions.length) {
for (const javaExtensionPath of javaExtensions) {
result.push(path.resolve(extension.extensionPath, javaExtensionPath));
}
}
}
}
return result;
}
JDT LS 端的 BundleUtils.loadBundles 对每个 bundle 执行以下逻辑:
public static void loadBundles(Collection<String> bundleLocations) throws CoreException {
for (String bundleLocation : bundleLocations) {
String location = getBundleLocation(bundleLocation, true);
BundleInfo bundleInfo = getBundleInfo(bundleLocation);
Bundle[] bundles = getBundles(bundleInfo.getSymbolicName(), frameworkWiring);
if (bundles != null) {
Version currentBundleVersion = Version.parseVersion(bundleInfo.getVersion());
if (bundleInfo.isSingleton()) {
if (bundles[0].getLocation().equals(location)
&& bundles[0].getVersion().equals(currentBundleVersion)) {
continue; // 跳过安装,复用缓存
}
uninstallBundle(bundlesToStart, toRefresh, bundles[0]);
} else {
for (Bundle bundle : bundles) {
if (bundle.getVersion().equals(currentBundleVersion)) {
if (bundle.getLocation().equals(location)) {
shouldSkip = true; // 跳过安装,复用缓存
} else {
uninstallBundle(bundlesToStart, toRefresh, bundle);
}
break;
}
}
if (shouldSkip) {
continue;
}
}
}
installBundle(context, bundlesToStart, location);
}
}
关键在于 getBundleLocation 方法使用 reference: 协议:
private static String getBundleLocation(String location, boolean useReference) throws MalformedURLException {
File f = new File(location);
String bundleLocation = f.toURI().toString();
if (useReference) {
bundleLocation = REFERENCE_PREFIX + bundleLocation;
}
return bundleLocation;
}
reference: 协议让 OSGi 框架直接引用扩展目录中的 JAR 文件,而非复制到缓存目录。但 Bundle-ClassPath 中声明的内嵌 JAR(如 lib/com.microsoft.java.debug.core-0.53.2.jar)会在首次安装时被解压到 .cp/lib/ 目录。
loadBundles 的缓存判断逻辑是:同时比较 bundle 的 location(文件路径)和 version(MANIFEST.MF 中的 Bundle-Version)。两者都相同则 continue 跳过安装,直接复用 OSGi 缓存中已解压的 .cp/lib/ 内容。代码不会检查 JAR 文件本身的内容是否变化(如哈希值或修改时间)。
回到本次问题:
location:reference:file:/C:/Users/Administrator/.trae-cn/extensions/.../com.microsoft.java.debug.plugin-0.53.2.jar,扩展目录路径不变,location 不变version:MANIFEST.MF 中的Bundle-Version为0.53.2,AI 修改 JAR 内容时并未修改版本号,version 不变
两者都相同,loadBundles 直接 continue 跳过,OSGi 复用缓存中已解压的旧版 core JAR。LSP 并不会每次启动都重新解压。
时间线还原
- 7/1 — VS Code 从 Marketplace 安装
vscode-java-debug扩展(旧版 JAR,无resolveSourceFromSourcePaths) - 更早 — Trae CN 也安装了同一扩展(当时 Marketplace 版本一致,同为旧版)
- 7/27 20:51 — JDT LS 启动,OSGi 缓存了旧版 plugin JAR,解压 core JAR 到
.cp/lib/ - 7/27 22:51 — 排查僵局中,作者让 AI 加几行日志输出便于定位;AI 顺手把
resolveSourceFromSourcePaths修复也补了进去,重新构建替换了 Trae 扩展目录的 JAR(版本号仍是0.53.2)。Arthas 一打发现没生效,作者没回滚文件就由它去了 - 7/27 22:56 — JDT LS 重启,OSGi 检测到 bundle 版本号未变(
0.53.2),复用缓存中的旧版
验证命令
查看 JAR 内的 class 文件列表
# 列出 core JAR 中的所有类文件
unzip -l lib/com.microsoft.java.debug.core-0.53.2.jar | grep StackTraceRequestHandler
# 直接从 plugin JAR 中查看内嵌 core JAR 的类文件
unzip -l "C:/Users/Administrator/.trae-cn/extensions/vscjava.vscode-java-debug-0.59.0-universal/server/com.microsoft.java.debug.plugin-0.53.2.jar" | grep core-0.53.2
提取 class 文件并反编译
# 从 JAR 中提取单个 class 文件
unzip -o lib/com.microsoft.java.debug.core-0.53.2.jar "com/microsoft/java/debug/core/adapter/handler/StackTraceRequestHandler.class"
# 用 javap 反编译查看方法签名和字节码
javap -c -p StackTraceRequestHandler.class
# 只看方法签名(不反编译字节码)
javap -p StackTraceRequestHandler.class
# 查找特定方法是否存在
javap -p StackTraceRequestHandler.class | grep resolveSourceFromSourcePaths
比较不同来源的 class 文件
# SHA256 对比
sha256sum osgi/StackTraceRequestHandler.class trae/StackTraceRequestHandler.class vscode/StackTraceRequestHandler.class
# 二进制比较
diff <(xxd osgi/StackTraceRequestHandler.class) <(xxd trae/StackTraceRequestHandler.class)
# 反编译后的文本对比
javap -c -p osgi/StackTraceRequestHandler.class > osgi.txt
javap -c -p trae/StackTraceRequestHandler.class > trae.txt
diff -u osgi.txt trae.txt
从 plugin JAR 提取内嵌的 core JAR
# 提取内嵌 core JAR 到当前目录
unzip -o "C:/Users/Administrator/.trae-cn/extensions/vscjava.vscode-java-debug-0.59.0-universal/server/com.microsoft.java.debug.plugin-0.53.2.jar" "lib/com.microsoft.java.debug.core-0.53.2.jar"
# 提取内嵌 core JAR 到指定目录(-j 去掉路径前缀)
unzip -o -j "C:/Users/Administrator/.trae-cn/extensions/vscjava.vscode-java-debug-0.59.0-universal/server/com.microsoft.java.debug.plugin-0.53.2.jar" "lib/com.microsoft.java.debug.core-0.53.2.jar" -d /tmp/trae_core
Arthas 命令
# 附加到 JDT LS 进程
java -jar arthas-boot.jar <pid>
# 查看运行时类的方法列表
sm com.microsoft.java.debug.core.adapter.handler.StackTraceRequestHandler * -d
# 反编译运行时的方法
jad com.microsoft.java.debug.core.adapter.handler.StackTraceRequestHandler convertDebuggerSourceToClient
# 观察方法入参和返回值
watch com.microsoft.java.debug.core.adapter.handler.StackTraceRequestHandler convertDebuggerSourceToClient '{params[0], params[2], returnObj}' 'params[0].contains("Default")' -x 4 -n 1
# 确认 sourceLookup 是否被调用
watch com.microsoft.java.debug.core.adapter.AdapterUtils sourceLookup '{params, returnObj}' -x 3 -n 1
解决方案
清除 OSGi 缓存,让 JDT LS 重新从扩展目录安装新版 JAR:
- 关闭 Trae CN(确保 JDT LS 进程停止)
- 删除 OSGi 缓存目录:
rm -rf "C:/Users/Administrator/AppData/Roaming/Trae CN/User/globalStorage/redhat.java/1.55.0/config_win/org.eclipse.osgi"
- 重启 Trae CN,JDT LS 将从扩展目录重新安装所有 bundle,新版 core JAR 生效
如果只想更新单个 bundle,可以只删除对应 bundle 的缓存目录:
rm -rf "C:/Users/Administrator/AppData/Roaming/Trae CN/User/globalStorage/redhat.java/1.55.0/config_win/org.eclipse.osgi/109"
验证修复
删除 OSGi 缓存中 bundle 109 的旧版 core JAR 后,重启 Trae CN 并启动 JDT LS。用 Arthas 附加到新的 JDT LS 进程(PID 19976),反编译运行时的 convertDebuggerSourceToClient 方法:
jad com.microsoft.java.debug.core.adapter.handler.StackTraceRequestHandler convertDebuggerSourceToClient
ClassLoader 信息显示 bundle 已重新安装,编号从 109 变为 143:
ClassLoader:
+-org.eclipse.osgi.internal.loader.EquinoxClassLoader@6b1024fe[com.microsoft.java.debug.plugin:0.53.2(id=143)]
+-jdk.internal.loader.ClassLoaders$PlatformClassLoader@6f169fa6
Location:
/C:/Users/Administrator/AppData/Roaming/Trae CN/User/globalStorage/redhat.java/1.55.0/config_win/org.eclipse.osgi/143/0/.cp/lib/com.microsoft.java.debug.core-0.53.2.jar
反编译结果中,关键逻辑已发生变化:
public static Types.Source convertDebuggerSourceToClient(
String fullyQualifiedName, String sourceName, String relativeSourcePath,
IDebugAdapterContext context) throws URISyntaxException {
Source source = context.getSourceLookupCache().
computeIfAbsent(fullyQualifiedName, key -> {
Source result = ((ISourceLookUpProvider)context
.getProvider(ISourceLookUpProvider.class))
.getSource(key, relativeSourcePath);
if (result == null) {
return new Source("", SourceType.LOCAL);
}
return result;
});
Integer sourceReference = 0;
String uri = source.getUri();
if (source.getType().equals(SourceType.REMOTE)) {
sourceReference = context.createSourceReference(source.getUri());
}
if (!StringUtils.isBlank(uri)) {
if (uri.startsWith("file:")) {
String clientPath = AdapterUtils.convertPath(
uri, context.isDebuggerPathsAreUri(), context.isClientPathsAreUri());
return new Types.Source(sourceName, clientPath, sourceReference);
}
return new Types.Source(sourceName, uri, sourceReference);
}
String absoluteSourcepath = AdapterUtils.sourceLookup(
context.getSourcePaths(), relativeSourcePath);
if (absoluteSourcepath != null) {
return new Types.Source(sourceName, absoluteSourcepath, sourceReference);
}
return null;
}
注意最后部分:当 uri 为空时(即 JDT 未返回有效 URI),代码会走到 AdapterUtils.sourceLookup(),使用 sourcePaths 进行回退查找。这正是之前缺失的逻辑。
验证新加载的 core JAR 哈希值:
cd C:/Users/Administrator/AppData/Roaming/Trae CN/User/globalStorage/redhat.java/1.55.0/config_win/org.eclipse.osgi/143/0/.cp/lib
sha256sum com.microsoft.java.debug.core-0.53.2.jar
57dcf47d926b1b6b3b65b0f092346b298e6d8a2b48452c407d8c5684ff8ceb09 *com.microsoft.java.debug.core-0.53.2.jar
哈希值为 57DCF47D,与 VS Code 扩展目录中的版本一致。清除缓存后,JDT LS 重新加载了 JAR。
根因追溯
清除缓存后,Trae 和 VS Code 的 core JAR 内容一致,StackTraceRequestHandler 的逻辑也相同。但这里浮现出一个更深层的问题:为什么参考源码中存在正确的 sourcePaths 回退逻辑,而实际部署的 release 版本却没有?
回顾前文,需要先厘清 java-debug 项目的来源与版本关系。java-debug 是 VS Code Debugger for Java 扩展(vscjava.vscode-java-debug)的开源源码项目,托管在 GitHub。本次涉及的版本对应关系如下:
- 扩展版本:
vscjava.vscode-java-debug-0.59.0(Trae CN 安装的是0.59.0-universal变体),从扩展市场安装 - plugin JAR(OSGi bundle 外壳):
com.microsoft.java.debug.plugin-0.53.2.jar,Bundle-Version为0.53.2 - core JAR(内嵌核心逻辑):
com.microsoft.java.debug.core-0.53.2.jar,包含StackTraceRequestHandler等类 - 参考源码:从 GitHub 下载的 java-debug 源码项目,版本为 pre-release
0.59.2026072407,其中已包含resolveSourceFromSourcePaths修复 - 实际运行版本:Trae CN 和 VS Code 默认安装的是 release 版本(稳定版),两者 core JAR 原本内容一致,都缺少这个修复
一段误判与回旋镖
排查到这里,作者一度把锅扣在了 VS Code 头上。两个扩展目录里都有 vscjava.vscode-java-debug,而 OSGi 缓存里加载的 core JAR 哈希恰好与 VS Code 扩展目录中的版本一致。顺理成章的推断是:Trae CN 复用了 VS Code 那份缓存,把 VS Code 的旧 JAR 当成自己的加载了。
顺着这条线索往下挖,才发现完全不是那么回事。Trae CN 和 VS Code 各有各的 globalStorage,OSGi 缓存目录彼此独立,根本不互通。两者哈希一致,只是因为装的都是同一份 release 稳定版——巧合,不是共享。真正造成 Trae 扩展目录与缓存差异的,是另外一桩"家务事"。
时间倒回 7/27 22:51,排查陷入僵局时,作者让 AI 在 convertDebuggerSourceToClient 里加几行日志输出,好把中间变量打印出来定位卡在哪。AI 照做了,顺带把参考源码里的 resolveSourceFromSourcePaths 修复也一起补了进去,重新构建出 JAR 替换到 Trae 扩展目录。作者当时想得很简单:反正只是加日志,跑完就丢,没在意。结果 Arthas 一打,日志没出来,方法也没多——修改后的 JAR 根本没生效。
既然没生效,那它就等于没改过,作者也就懒得回滚文件,由它去了。谁能想到,这个"没生效所以不用管"的判断,后来结结实实地打了自己一记回旋镖。
正是因为这份"没生效"的修改,Trae 扩展目录里的 core JAR 被悄悄换成了哈希 225EC344 的新版,而 OSGi 缓存里还躺着首次安装时解压的哈希 4F84C051 旧版。两份 JAR 内容出现差异,却又都顶着 0.53.2 的版本号。等到排查后期回过头比对三处 JAR 哈希时,这组对不上的数字差点把整条排查链路带偏——"源码对了运行时却错了"的困惑,正是从这颗当初没当回事的子弹飞回来开始的。
这里涉及两处 core JAR 的位置:
- 源 JAR(AI 修改后的新版):内嵌在 Trae 扩展目录的 plugin JAR 中,完整路径为
C:\Users\Administrator\.trae-cn\extensions\vscjava.vscode-java-debug-0.59.0-universal\server\com.microsoft.java.debug.plugin-0.53.2.jar,其lib/下的com.microsoft.java.debug.core-0.53.2.jar(哈希225EC344)被 AI 重新构建替换为新版 - 缓存 JAR(运行时实际加载的旧版):位于 OSGi 缓存目录
C:\Users\Administrator\AppData\Roaming\Trae CN\User\globalStorage\redhat.java\1.55.0\config_win\org.eclipse.osgi\109\0\.cp\lib\com.microsoft.java.debug.core-0.53.2.jar(哈希4F84C051),是首次安装时从 plugin JAR 解压出来的旧版本
两者之所以能长期共存而不被刷新,是因为 BundleUtils.loadBundles 只比较 location 和 version,不检查文件内容。location 是扩展目录路径,没动;version 是 MANIFEST.MF 里的 Bundle-Version,AI 改 JAR 时也没改版本号。两者都没变,loadBundles 直接 continue 跳过,OSGi 缓存里的旧版 core JAR 照旧复用。如果没有 AI 那次"没生效就没管"的修改,Trae 扩展目录和缓存里的 JAR 本就一致,这层困惑压根不会出现。
查看 java-debug 项目的提交历史,找到了修复提交 3f03453a,该提交在 convertDebuggerSourceToClient 中添加了 sourcePaths 回退逻辑,使得非 file: URI 时会先尝试从用户配置的 sourcePaths 中查找源码文件。这是一个近期修复的 bug,对应社区中一个长期存在的问题:用户配置的 sourcePaths 在某些场景下完全不生效,调试器始终返回 jdt:// URI 而忽略用户配置。
问题的本质在于:Debugger for Java 的 release 版本(稳定版)尚未包含这个修复,而 pre-release 版本(预发布版)已经包含。这就解释了为什么参考源码有修复而运行时没有——参考的是 pre-release,运行的是 release。
要彻底解决这个问题,将 Debugger for Java 从 release 版本切换到 pre-release 版本即可获取最新的 bug 修复。在 VS Code 或 Trae CN 的扩展面板中,找到 Debugger for Java,点击齿轮图标选择"切换到预发布版本",安装完成后重启即可。
回旋镖落地的几点教训
回头看整件事,有几条教训值得记下:
AI 改完的 JAR 没生效,不代表它"等于没改"。文件系统可不管你运行时加载的是哪份——扩展目录里的 JAR 已经被替换了,哈希变了,这个事实会一直躺在那里,等到某天你回头比对时跳出来咬人。"没生效所以不用管"是最危险的判断,它把一个显性的问题压成了隐性的地雷。
OSGi 缓存只认 location 和 version,不认内容。这意味着任何不 bump 版本号就替换 JAR 的操作,都会制造出"目录里是新的、缓存里是旧的"分裂状态。开发时本地构建替换扩展 JAR 是常见操作,但只要版本号没变,重启多少次都加载不到新版。清理缓存是唯一的兜底。
哈希相同不等于来源相同。OSGi 缓存里的 JAR 哈希和 VS Code 扩展目录里的一致,作者一度以为是 Trae 偷用了 VS Code 的缓存。实际上两边只是恰好装了同一份 release 版本。排查时看到"对得上"的数字,最容易顺着惯性编一个因果故事,而真相往往是巧合。
最后一条给自己:让 AI 加日志就加日志,别让它顺手"把参考实现也补上"。一次没盯紧的操作,加上一个"没生效就没管"的侥幸心理,凑成了这出回旋镖。子弹飞了一圈,最后正中自己的眉心。
更多推荐



所有评论(0)