写在开头
同事那台机器,服务一启动就炸。
报错大概长这样:
java.lang.ExceptionInInitializerError
Caused by: com.xxx.crypto.CryptoException: Cannot load library: nativeapi
同一份代码,我们五六个人跑得好好的,就他那一台必挂。
这个 SDK 会把几个原生库解压到临时目录再加载。正常情况下 %TEMP% 底下能看到 5 个 dll。他截图给我,只有 4 个,少的那个正好就是报错点名的那个。
第一个念头太自然了:文件没生成出来。
我们照着这个方向查了将近两个小时,能试的都试了。先把 jar 包重装一遍,没用。我又把自己 Maven 仓库里的包整个拷给他,还是没用。怀疑过是他的杀毒软件把磁盘写入拦了,卸载掉重启一遍,照样没用。最后连 Maven 配置都怀疑上了,想着是不是仓库地址引错,Maven 背地里重新下了一份 jar 包。
没有一条对得上。
后来我把这个问题丢给了 WorkBuddy,一个桌面 Agent,让它带着我从代码这一侧重新找一遍。
一、报错点名的那个库,其实是无辜的
真正把我拉出来的,是一个逻辑上的矛盾。
我让 Agent 把 SDK 的加载逻辑反编译出来,一行一行看。
throw new CryptoException("Cannot load library: " + failedLibraryName);
关键在这个变量 failedLibraryName,它只在 catch 到 UnsatisfiedLinkError 的时候才会被赋值。
也就是说,程序走到抛异常这一行的时候,已经经历过一次 System.load() 了,而且失败了一次。
那文件必然是存在的。
文件不是没生成,是加载失败之后被删掉了。
二、文件为什么不见了
这个 SDK 干的事是这样的。
先把几个 dll 从 jar 里解压到 java.io.tmpdir/wcsp/<随机数字>/,再逐个 System.load(),干完活把整个临时目录删掉,删不掉就改个名标记一下。
第三步是最关键的。
Windows 上有个特性,已经映射进进程的 dll 文件是删不掉的。加载成功的库被映射了,文件删不动,留在原地。加载失败的库没被映射,可以正常删掉。
所以最后残留下来的是加载成功的那些。
缺哪个文件,恰恰说明哪个加载失败了。
这里还有半句要补。解析库这一步本身是静默的,那一组读不到就直接跳过,不抛错。所以你看到的场面是:文件没生成,日志里也什么都没有,一条线索都不给。
我在自己机器上翻了一下 %TEMP%,底下堆了二十来个 SDK 没清干净的残留目录。点开看,内容正好能对上。
五个变四个,少的就是报错里那一个。
到这里,文件没生成这条路彻底排除了。而且反过来,它还成了一条线索。
如果下次你在别人机器上撞见同类现象,有个最快的判据:去 %TEMP%\wcsp\ 底下找最新的那个数字目录,缺谁,就是谁加载失败。不用等报错,也不用翻日志。
三、五个库,为什么只挂了它一个
这是整件事里最值钱的一步。
Agent 把那几个 dll 的 PE 导入表全解析了一遍,看它们各自依赖哪些 C/C++ 运行库。
| dll |
依赖的运行库 |
| 报错那个 |
MSVCR100.dll |
| 库 B |
MSVCP140 + VCRUNTIME140 |
| 库 C |
VCRUNTIME140 |
| 日志库 |
无 |
只有它一个依赖 MSVCR100.dll。
MSVCR100 就是 VC++ 2010 的运行库。
这里有个很多 Java 开发者不会留意的点:VC++ 2010 运行库不是 Windows 自带的。系统内置的是 2015 之后那一套,也就是 VCRUNTIME140 系列。MSVCR100 到底在不在,完全取决于这台机器历史上装过什么软件。
那有的机器正常、有的机器必挂,就说通了。这件事跟 Java 版本没关系,跟机器装过什么有关系。
同时也解释了为什么偏偏是它。其他几个库依赖的 VCRUNTIME140 是系统自带的,它们没事。
顺带还排掉了一个我很早就怀疑过的方向。我一度以为 jar 里压根没带这个库,拆开一看,带了。所以从一开始就不是打包问题。
中间还有一层,差点把我带偏。
报错那个库除了 MSVCR100,还隐式依赖同目录下的日志库。
而 System.load(绝对路径) 有个挺反直觉的行为:它不会去被加载的那个 dll 自己所在的目录里找依赖。
这个我让它写了个最小程序测过。
System.load(dir + "nativeapi.dll"); // 必抛 UnsatisfiedLinkError: Can't find dependent libraries
System.load(dir + "syslog.dll");
System.load(dir + "nativeapi.dll"); // 成功
那正常机器为什么一点事没有?
因为 SDK 内部有个重试循环。第一轮日志库排在它后面,所以它失败;第二轮日志库已经加载好了,它就成功了。
这个重试在正常机器上把顺序问题掩盖掉了。
但它掩盖不了真正缺失的依赖。如果它是因为找不到 MSVCR100 而失败,重试多少次都一样。
四、真因和修复
根因既然锁在运行库上,那就去那台机器上确认。
一看 System32 底下那个 msvcr100.dll,没有。结案。
修复很简单,装官方的运行库,32 位和 64 位都装上,省得别的老程序接着踩。
x64 https://download.microsoft.com/download/1/6/5/165255E7-1014-4D0A-B094-B6A430A6BFFC/vcredist_x64.exe
x86 https://download.microsoft.com/download/1/6/5/165255E7-1014-4D0A-B094-B6A430A6BFFC/vcredist_x86.exe
装完重启 Java 进程就行,不用重启机器。
要是那台机器不让装东西,应急方案是从正常机器把文件拷过去。这里一定要认位数,拷错了等于白拷。
| 文件 |
大小 |
位数 |
System32\msvcr100.dll |
830,616 字节 |
x64 |
SysWOW64\msvcr100.dll |
774,808 字节 |
x86 |
落点选 java.exe 同目录最省事。因为 Windows 解析依赖时的搜索顺序里,第一站就是应用程序所在目录,然后才轮到 System32。
这里有个坑要绕开。别把它放进 %TEMP%\wcsp\<数字>\ 那个目录,那目录名每次启动都是新的随机数,而且 System.load 用绝对路径的时候,不会把被加载 dll 自己的目录加进搜索路径。上面那个顺序坑就是这么来的。
顺手留几条自检命令,遇到类似的可以直接用。
:: 两个位数目录一起看,有可能只装了 x86 版
dir C:\Windows\System32\msvcr100.dll C:\Windows\SysWOW64\msvcr100.dll
:: 确认 JVM 位数,决定装 x64 还是 x86
java -XX:+PrintFlagsFinal -version 2>&1 | findstr "sun.arch.data.model"
:: 注册表权威键,返回 Installed=0x1 就是装了
reg query "HKLM\SOFTWARE\Microsoft\VisualStudio\10.0\VC\VCRedist\x64" /v Installed
还有两条,也是让它写最小程序测的,-Djava.library.path 和把 dll 目录加进 PATH,都救不回来,别再往那两个方向花时间。
写在结尾
这次真正记住的是三件事。
一,报错点名的那个东西,未必就是问题所在。先确认文件到底在不在,再决定往哪一侧查。
二,Windows 上文件凭空消失,先想想是不是加载失败之后被清掉的。已经映射进进程的 dll 删不掉,留下来的那几个反而能告诉你谁成功了。
三,Java 的报错指向系统依赖的时候,别只在 Java 这一侧找。这次最后落到的,是一条跟 Java 一点关系都没有的运行库。
这次剥到根因,AI 帮了大忙。反编译 SDK 看加载逻辑,解那几个 dll 的 PE 导入表,写最小程序复现加载失败,都是它跑的。最后把它坐实的,是那台机器上一条最朴素的命令。