| 方法 | 參數 | 搜索路徑 | 文件名要求 | 典型用法 |
System.load(String absolutePath) | 動態庫絕對路徑 | 僅指定路徑,不依賴系統搜索 | 完整文件名 (含前綴/後綴) | 精確控制加載位置、打包在 JAR 內釋放到臨時目錄 |
System.loadLibrary(String libName) | 庫邏輯名稱 (不含前綴/後綴) | java.library.path、LD_LIBRARY_PATH (Linux)、PATH (Windows)、DYLD_LIBRARY_PATH (macOS) | 僅邏輯名,如 "mylib" → 自動映射為 libmylib.so / mylib.dll | 標準 JNI 開發、跨平台部署、依賴系統庫搜索機制 |
底層機制:JNI_OnLoad 與靜態初始化
// Java 端
public class NativeLib {
static {
// 方式一:loadLibrary (推薦標準 JNI)
System.loadLibrary("native-demo");
// 方式二:load (精確路徑)
// String path = System.getProperty("user.dir") + "/lib/native-demo.dll";
// System.load(path);
}
// 宣告 native 方法
public native int add(int a, int b);
public native void hello(String name);
}
// C/C++ 端 (native-demo.c)
#include
#include "NativeLib.h" // javac -h 生成
JNIEXPORT jint JNICALL Java_NativeLib_add(JNIEnv *env, jobject obj, jint a, jint b) {
return a + b;
}
JNIEXPORT void JNICALL Java_NativeLib_hello(JNIEnv *env, jobject obj, jstring name) {
const char *str = (*env)->GetStringUTFChars(env, name, NULL);
printf("Hello from C: %s\n", str);
(*env)->ReleaseStringUTFChars(env, name, str);
}
// 可選:庫加載入口,執行初始化、緩存類/方法 ID
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_8) != JNI_OK) return JNI_ERR;
// 緩存全局引用、註冊本地方法 (RegisterNatives) 等
return JNI_VERSION_1_8;
}
跨平台文件名映射規則
| 平台 | loadLibrary("foo") 映射為 | load("/abs/path/") 需提供 |
| Linux / Unix | libfoo.so | /abs/path/libfoo.so |
| macOS | libfoo.dylib (或 .so) | /abs/path/libfoo.dylib |
| Windows | foo.dll | C:/abs/path/foo.dll |
| AIX | libfoo.a (共享庫歸檔) | /abs/path/libfoo.a |
最佳實踐:構建腳本 (CMake/Make/Gradle) 按平台產出標準命名,Java 端統一 System.loadLibrary("foo"),部署時將對應平台庫放入 java.library.path 目錄。
java.library.path 設置方式
- 啟動參數:
java -Djava.library.path=/opt/native/lib -jar app.jar
- 代碼動態修改 (Hack,慎用):
Field field = ClassLoader.class.getDeclaredField("usr_paths");
field.setAccessible(true);
String[] paths = (String[]) field.get(null);
String[] newPaths = Arrays.copyOf(paths, paths.length + 1);
newPaths[newPaths.length - 1] = "/custom/native/path";
field.set(null, newPaths);
// 需同時修改 sys_paths 觸發重新初始化
- 容器化部署:Dockerfile
ENV LD_LIBRARY_PATH=/app/native 或將庫拷入 /usr/lib、/usr/local/lib。
進階場景:JAR 內打包動態庫並運行時釋放
public class NativeLoader {
private static final String LIB_NAME = "native-demo";
private static final String LIB_PATH_IN_JAR = "/native/" + System.mapLibraryName(LIB_NAME); // 自動帶前綴/後綴
static {
loadFromJar();
}
private static void loadFromJar() {
try {
// 1. 從 JAR 讀取庫文件到臨時文件
InputStream is = NativeLoader.class.getResourceAsStream(LIB_PATH_IN_JAR);
if (is == null) throw new UnsatisfiedLinkError("JAR 內未找到: " + LIB_PATH_IN_JAR);
File tempFile = File.createTempFile(LIB_NAME, null);
tempFile.deleteOnExit(); // JVM 退出時刪除
try (FileOutputStream fos = new FileOutputStream(tempFile)) {
byte[] buffer = new byte[4096];
int len;
while ((len = is.read(buffer)) != -1) fos.write(buffer, 0, len);
}
// 2. 使用 load 絕對路徑加載
System.load(tempFile.getAbsolutePath());
System.out.println("動態庫加載成功: " + tempFile.getAbsolutePath());
} catch (IOException e) {
throw new UnsatisfiedLinkError("動態庫釋放/加載失敗: " + e.getMessage(), e);
}
}
public native void doSomething();
}
優勢:單一 JAR 發布,無需單獨分發 .so/.dll,適合分發給最終用戶的桌面應用、插件、Agent。
常見錯誤與排查
| 錯誤 | 原因 | 解決 |
UnsatisfiedLinkError: no foo in java.library.path | loadLibrary 找不到庫 | 檢查 java.library.path、庫文件名是否符合平台規則、架構是否匹配 (x64/arm64) |
UnsatisfiedLinkError: /path/libfoo.so: wrong ELF class | 32/64 位不匹配 | JVM 與庫架構一致 (均 64 位或均 32 位) |
UnsatisfiedLinkError: /path/libfoo.so: undefined symbol | 缺少依賴庫 / 符號版本不匹配 | ldd libfoo.so 檢查依賴、確保編鏈環境一致 |
UnsatisfiedLinkError: Native Library already loaded in another classloader | 同一 JVM 多 ClassLoader 重複加載同名庫 | 共享庫加載在公共父 ClassLoader (如 Tomcat common/lib),或使用獨特庫名 |
加載成功但調用 native 方法報 UnsatisfiedLinkError | 方法簽名不匹配 (包名/類名/方法名/參數類型) | javac -h 重新生成頭文件、檢查 JNI 函數名規則 Java_pkg_Class_method |
現代替代方案:JNA / JNR-FFI / Panama (JDK 19+)
- JNA (Java Native Access):純 Java 實現,無需編寫 C++/JNI 代碼,直接映射庫函數,開發效率極高,性能略低於 JNI。
- JNR-FFI:類似 JNA,支持更多平台、更豐富類型映射。
- Project Panama (Foreign Function & Memory API):JDK 19+ Preview / JDK 21 正式,標準化純 Java 調用外部函數、操作堆外內存,未來標準方向。
// JNA 示例
public interface CLibrary extends Library {
CLibrary INSTANCE = Native.load("c", CLibrary.class);
int printf(String format, Object... args);
}
// 直接調用
CLibrary.INSTANCE.printf("Hello JNA: %d\n", 42);
總結
- loadLibrary:標準 JNI 方式,跨平台友好,依賴
java.library.path。
- load:精確控制路徑,適合 JAR 內打包釋放、插件系統、精確版本控制。
- 命名規則:
lib[name].so/.dylib (Linux/macOS)、[name].dll (Windows)。
- 架構必須匹配:JVM 64 位 ↔ 庫 64 位。
- 新項目優先評估 JNA / Panama,降低 JNI 開發維護成本。
- 上一篇文章: Servlet 獲取 HTML 表單多值參數:Select 多選、Radio、Checkbox、同名 Input 完整指南
- 下一篇文章: Velocity 模板引擎遍歷 List 與 Map 完整指南:指令語法與後台數據傳遞
Copyright ©
程式碼之家 All Rights Reserved