自定義 DLL 加載 – 修復“java.lang.UnsatisfiedLinkError”錯誤
一、簡介
在本快速教程中,我們將探討UnsatisfiedLinkError
的不同原因和解決方案。這是使用本機庫時遇到的常見且令人沮喪的錯誤。解決此錯誤需要徹底了解其原因並採取適當的糾正措施。
我們將討論諸如不正確的庫和方法名稱、缺少庫目錄規範、與類加載器衝突、不兼容的體系結構以及 Java 安全策略的作用等場景。
2. 場景和設置
我們將創建一個簡單的類來說明加載外部庫時可能出現的錯誤。考慮到我們在 Linux 上,讓我們加載一個名為“libtest.so”的簡單庫並調用它的test()
方法:
public class JniUnsatisfiedLink {
public static final String LIB_NAME = "test";
public static void main(String[] args) {
System.loadLibrary(LIB_NAME);
new JniUnsatisfiedLink().test();
}
public native String test();
public native String nonexistentDllMethod();
}
通常,我們希望將庫加載到靜態塊中,以確保它只加載一次。但是,為了更好地模擬錯誤,我們將其加載到main()
方法中。在這種情況下,我們的 lib 僅包含一個有效方法test()
,它返回一個String
。我們還聲明了一個nonexistentDllMethod()
來查看我們的應用程序的行為方式。
3. 未指定庫目錄
UnsatisfiedLinkError
最直接的原因是我們的庫不在 Java 期望庫所在的任何目錄中。這可能位於系統變量中,例如 Unix 或 Linux 上的LD_LIBRARY_PATH
或 Windows 上的PATH
。也可以通過System.load()
而不是loadLibrary()
使用我們的庫的完整路徑:
System.load("/full/path/to/libtest.so");
但是,為了避免特定於系統的解決方案,我們可以設置java.library.path
VM 屬性。該屬性接收一個或多個包含我們需要加載的庫的目錄路徑:
-Djava.library.path=/any/library/dir
目錄分隔符將取決於我們的操作系統。對於 Unix 或 Linux 來說是冒號,對於 Windows 來說是分號。
4. 庫名稱或權限不正確
獲得UnsatisfiedLinkError
最常見原因可能是使用了不正確的庫名稱。這是因為,為了使代碼盡可能與平台無關,Java 對庫名稱做了一些假設:
- 對於Windows ,它假定庫文件名以“.dll”結尾。
- 對於大多數類Unix系統,它採用“lib”前綴和“.so”擴展名。
- 最後,特別是對於Mac ,它採用“lib”前綴和“.dylib”(以前的“.jnilib”)擴展名。
因此,如果我們包含任何這些前綴或後綴,我們將收到錯誤:
@Test
public void whenIncorrectLibName_thenLibNotFound() {
String libName = "lib" + LIB_NAME + ".so";
Error error = assertThrows(UnsatisfiedLinkError.class, () -> System.loadLibrary(libName));
assertEquals(
String.format("no %s in java.library.path", libName),
error.getMessage()
);
}
順便說一句,這使得我們無法嘗試加載為與我們運行應用程序不同的平台構建的庫。在這種情況下,如果我們希望我們的應用程序是多平台的,我們必須為所有平台提供二進製文件。如果我們的 Linux 環境中的庫目錄中只有“test.dll”, System.loadLibrary(“test”)
將導致相同的錯誤。
同樣,如果我們在loadLibrary()
中包含路徑分隔符,我們會收到錯誤:
@Test
public void whenLoadLibraryContainsPathSeparator_thenErrorThrown() {
String libName = "/" + LIB_NAME;
Error error = assertThrows(UnsatisfiedLinkError.class, () -> System.loadLibrary(libName));
assertEquals(
String.format("Directory separator should not appear in library name: %s", libName),
error.getMessage()
);
}
最後,對我們的庫目錄沒有足夠的權限將導致同樣的錯誤。例如,我們至少需要Linux中的“執行”權限。另一方面,如果我們的文件至少沒有“讀取”權限,我們將收到類似以下的消息:
java.lang.UnsatisfiedLinkError: /path/to/libtest.so: cannot open shared object file: Permission denied
5. 方法名稱/用法不正確
如果我們聲明的本機方法與本機源代碼中聲明的任何方法都不匹配,我們也會收到錯誤,但僅當我們嘗試調用不存在的方法時:
@Test
public void whenUnlinkedMethod_thenErrorThrown() {
System.loadLibrary(LIB_NAME);
Error error = assertThrows(UnsatisfiedLinkError.class, () -> new JniUnsatisfiedLink().nonexistentDllMethod());
assertTrue(error.getMessage()
.contains("JniUnsatisfiedLink.nonexistentDllMethod"));
}
請注意, loadLibrary()
中沒有引發異常。
6. 庫已被另一個類加載器加載
如果我們在同一 Web 應用程序服務器(如 Tomcat)中的不同 Web 應用程序中加載相同的庫,則很可能會發生這種情況。然後,我們會得到錯誤:
Native Library libtest.so already loaded in another classloader
或者,如果它處於加載過程的中間,我們將得到:
Native Library libtest.so is being loaded in another classloader
解決此問題的最簡單方法是將用於加載庫的代碼放入 Web 應用程序服務器的共享目錄中的 JAR 中。例如,Tomcat 中的“
7. 不兼容的架構
使用舊庫時最有可能出現這種情況。我們無法加載為與運行應用程序的架構不同的架構編譯的庫 -例如,如果我們嘗試在 64 位系統上加載 32 位庫:
@Test
public void whenIncompatibleArchitecture_thenErrorThrown() {
Error error = assertThrows(UnsatisfiedLinkError.class, () -> System.loadLibrary(LIB_NAME + "32"));
assertTrue(error.getMessage()
.contains("wrong ELF class: ELFCLASS32"));
}
在上面的示例中,我們將我們的庫與 32 位標誌鏈接起來以進行測試。一些旁注:
- 如果我們嘗試通過重命名文件在不同平台上加載 DLL,也會發生類似的錯誤。然後,我們的錯誤將包含“無效的 ELF header”消息。
- 如果我們嘗試在不兼容的平台上加載我們的庫,則將找不到該庫。
8. 文件損壞
嘗試加載損壞的文件時總會導致UnsatisfiedLinkError
。為了說明這一點,讓我們看看當我們嘗試加載空文件時會發生什麼(請注意,此測試針對單個庫路徑進行了簡化,並考慮了 Linux 環境):
@Test
public void whenCorruptedFile_thenErrorThrown() {
String libPath = System.getProperty("java.library.path");
String dummyLib = LIB_NAME + "-dummy";
assertTrue(new File(libPath, "lib" + dummyLib + ".so").isFile());
Error error = assertThrows(UnsatisfiedLinkError.class, () -> System.loadLibrary(dummyLib));
assertTrue(error.getMessage().contains("file too short"));
}
為了避免這種情況,通常將 MD5 校驗和與二進製文件一起分發,以便我們檢查完整性。
9.Java安全策略
如果我們使用 Java 策略文件,我們需要為loadLibrary()
和我們的庫名稱授予RuntimePermission
:
grant {
permission java.lang.RuntimePermission "loadLibrary.test";
};
否則,在嘗試加載我們的庫時,我們會收到與此類似的錯誤:
java.security.AccessControlException: access denied ("java.lang.RuntimePermission" "loadLibrary.test")
請注意,為了使自定義策略文件生效,我們需要指定要使用安全管理器:
-Djava.security.manager
10. 結論
在本文中,我們探討了解決 Java 應用程序中UnsatisfiedLinkError
解決方案。我們討論了此錯誤的常見原因,並提供了有效解決這些問題的見解。通過實施這些見解並根據應用程序的特定需求對其進行定制,我們可以有效地解決UnsatisfiedLinkError
發生。
與往常一樣,源代碼可以在 GitHub 上獲取。