Java 加载 DLL 时出现 UnsatisfiedLinkError
结论
出现这类错误时,目标 DLL 可能存在,真正加载失败的是它依赖的其他 DLL。System.load(...) 加载目标文件后,Windows 还会继续加载相关依赖。依赖缺失、架构不匹配、运行库未安装或 DLL 初始化失败,都可能导致 Java 抛出 UnsatisfiedLinkError。此时的错误信息仍可能是 “Can’t load library” 或 “The specified module could not be found”。
从示例来看,首先要检查这段硬编码路径:
System.load("C:/Users/foru/CLionProjects/CheckerGameLibrary/cmake-build-debug/libCheckerGameLibrary.dll");
它指向开发机器上的 CLion 构建目录。程序复制到另一台设备后,这个目录通常不存在。即使目录存在,里面也可能只有主 DLL,没有相关依赖。IntelliJ 中的配置只对相应的运行配置生效,不会自动安装 DLL、C++ 运行库或其他本地依赖。
先确认 Java 实际加载的文件
System.load 要求传入 DLL 的绝对路径。不要在代码中写死开发机器的用户名和构建目录,可以先用以下代码检查路径:
import java.nio.file.Files;
import java.nio.file.Path;
public class NativeLoader {
static {
Path dll = Path.of(
"native",
"libCheckerGameLibrary.dll"
).toAbsolutePath().normalize();
System.out.println("DLL path: " + dll);
System.out.println("Exists: " + Files.exists(dll));
System.out.println("Regular file: " + Files.isRegularFile(dll));
System.out.println("Readable: " + Files.isReadable(dll));
System.out.println("JVM architecture: " + System.getProperty("os.arch"));
System.out.println("Java home: " + System.getProperty("java.home"));
System.load(dll.toString());
}
}
应用从不同的工作目录启动时,相对路径对应的位置也会变化。可以同时输出当前工作目录:
System.out.println("Working directory: " + System.getProperty("user.dir"));
发布时,建议把 DLL 放在由应用统一管理的目录中,例如:
checker-game/
├─ app.jar
└─ native/
├─ libCheckerGameLibrary.dll
└─ 其他依赖 DLL
再通过启动脚本固定工作目录,或使用配置项传入本地库目录。
System.load 和 System.loadLibrary 的区别
两者处理路径的方式不同。
使用绝对路径加载
System.load("C:\\checker-game\\native\\libCheckerGameLibrary.dll");
System.load 会直接加载指定文件,不通过 java.library.path 查找主 DLL。
按库名搜索
System.loadLibrary("CheckerGameLibrary");
使用这种方式时,不要填写目录或 .dll 后缀。JVM 会根据 java.library.path 搜索库文件。启动时可以这样指定路径:
java -Djava.library.path="C:\checker-game\native" -jar app.jar
以下代码可以查看当前搜索路径:
System.out.println(System.getProperty("java.library.path"));
java.library.path 只负责帮助 JVM 找到主 DLL。主 DLL 的其他依赖仍由 Windows 加载器查找。即使主 DLL 使用绝对路径加载,也不代表它的所有依赖都能自动找到。
DLL 存在但仍然无法加载的常见原因
1. 依赖 DLL 缺失
假设 libCheckerGameLibrary.dll 依赖以下文件:
libstdc++-6.dll
libgcc_s_seh-1.dll
winpthread-1.dll
SomeThirdParty.dll
如果开发环境使用 MinGW、MSYS2 或第三方 C++ 库,这些依赖可能只安装在开发机器上。只把主 DLL 复制到新设备,程序仍然无法运行。
使用 Microsoft Visual C++ 编译时,程序也可能需要对应架构和版本的 Microsoft Visual C++ Redistributable。具体需要哪套运行库,应根据 DLL 的实际依赖和构建工具链判断,不能只看文件名。
2. 间接依赖的架构不匹配
主 DLL 和 Java 都是 64 位,只能确认第一层架构一致。主 DLL 依赖的每个本地库也必须采用兼容架构。
例如,以下组合仍会加载失败:
64-bit JVM
└─ 64-bit libCheckerGameLibrary.dll
└─ 32-bit ThirdParty.dll
Windows 常见的相关提示包括 %1 is not a valid Win32 application,通常表示架构不匹配或文件格式有误。
3. Windows 找不到依赖库
Windows 会按照系统的 DLL 搜索规则查找依赖,具体行为还受 Windows 版本、应用清单和加载方式影响。可以按以下方式排查:
- 将应用的私有依赖放到可执行程序所在目录或明确的本地库目录中。
- 启动 Java 进程前,将依赖目录加入
PATH。 - 修改环境变量后,重新启动终端和 IntelliJ。已经运行的进程不会自动获得新的环境变量。
- 不要依赖 CLion、MSYS2 或 Visual Studio 安装目录中偶然存在的 DLL。
可以在 PowerShell 中临时设置:
$env:Path = "C:\checker-game\native;$env:Path"
java -jar app.jar
如果应用只能在 IDE 中运行,需要比较 IDE 和命令行进程的 PATH、工作目录及所用 JDK。
4. 文件被安全策略阻止
从网络或其他设备复制过来的 DLL,可能受到 Windows 安全标记、杀毒软件、应用控制策略或目录权限的限制。需要检查:
- 当前用户对 DLL 及其目录是否有读取和执行权限;
- DLL 是否已被隔离或删除;
- 文件属性中是否有“解除锁定”选项;
- Windows 事件日志或安全软件日志中是否有拦截记录。
不要通过关闭系统安全功能进行验证。应先查看拦截记录,只对确认可信的构建产物解除限制。
5. DLL 初始化失败
所有文件都能找到时,DLL 的 DllMain 或静态 C++ 初始化代码仍可能失败。例如,初始化代码读取了开发机器上的配置,访问了不存在的目录,或尝试加载另一个插件。
遇到这种情况,需要查看 Windows 加载器返回的具体错误,并使用进程监控工具或本地调试器继续定位。
如何检查 DLL 的依赖项
如果已经安装 Visual Studio 或 Build Tools,可以在对应的开发者命令提示符中运行:
dumpbin /DEPENDENTS libCheckerGameLibrary.dll
查看文件头和目标架构:
dumpbin /HEADERS libCheckerGameLibrary.dll
/DEPENDENTS 会列出导入表中的直接依赖,但未必能直接指出新设备上缺少哪个文件。也可以使用 Dependencies 工具检查递归依赖、架构和无法解析的模块。
如果静态检查没有发现问题,可以使用 Process Monitor:
- 按 Java 进程名过滤,例如
java.exe或javaw.exe。 - 按操作类型或结果进一步过滤 DLL 文件访问。
- 运行程序,观察
NAME NOT FOUND、PATH NOT FOUND或ACCESS DENIED。 - 检查主 DLL 加载前后有哪些 DLL 被连续查找但始终失败。
Process Monitor 会显示大量正常的路径探测记录。某个路径出现 NAME NOT FOUND 未必代表故障,还要确认同一个 DLL 后续是否在其他目录中成功找到。
确认 JVM 和 DLL 架构
应查看当前实际运行的 Java,而不只是 IntelliJ 中的项目设置:
java -version
where java
也可以输出 JVM 属性:
System.out.println(System.getProperty("java.version"));
System.out.println(System.getProperty("java.vendor"));
System.out.println(System.getProperty("java.home"));
System.out.println(System.getProperty("os.arch"));
os.arch 可以用于初步判断。检查 PE 文件架构时,应以 dumpbin /HEADERS 或其他 PE 分析工具的结果为准。
区分“DLL 加载失败”和“JNI 方法找不到”
主 DLL 成功加载后,如果 JNI 导出函数的名称或签名不匹配,通常会在调用本地方法时看到类似错误:
java.lang.UnsatisfiedLinkError:
'long main.Main.nativeCreate()'
或者:
java.lang.UnsatisfiedLinkError:
no nativeCreate in java.library.path
这和当前的 Can't load library 发生在不同阶段。出现前一种问题时,需要检查 JNI 函数导出、包名、类名和方法签名,例如:
extern "C"
JNIEXPORT jlong JNICALL
Java_main_Main_nativeCreate(JNIEnv* env, jobject object) {
// ...
}
可以通过以下命令查看 DLL 导出的符号:
dumpbin /EXPORTS libCheckerGameLibrary.dll
使用 C++ 编译 JNI 函数时,通常需要添加 extern "C",否则 C++ 名称修饰可能导致 JVM 找不到预期的导出符号。更可靠的做法是使用 javac -h 根据 Java 声明生成 JNI 头文件,再让 C++ 实现严格匹配头文件中的声明。
建议的排查顺序
- 删除代码中指向开发机器的绝对路径,确认新设备上的实际 DLL 路径。
- 使用
Files.exists、Files.isRegularFile和Files.isReadable输出检查结果。 - 确认 IntelliJ 和命令行使用的是同一个 JDK。
- 检查 JVM、主 DLL 和所有间接依赖的架构。
- 使用
dumpbin /DEPENDENTS或 Dependencies 查看依赖链。 - 安装构建该 DLL 所需的 C/C++ 运行库,或随应用部署允许再分发的私有依赖。
- 使用 Process Monitor 查找缺失、路径错误或被拒绝访问的 DLL。
- 主 DLL 成功加载后,如果调用
nativeCreate()等方法仍然报错,再检查 JNI 导出名称和方法签名。
就这个案例而言,最可能的原因是代码仍在引用开发机器上的 CLion 构建路径,或者新设备上只有 libCheckerGameLibrary.dll,缺少它依赖的 C++ 运行库及第三方 DLL。建议先检查完整依赖链,再考虑调整 IntelliJ 的 Library 设置。
备注:内容仅供参考。
