從類中獲取 JAR 文件的完整路徑
一、概述
JAR 文件是 Java 檔案。當我們構建 Java 應用程序時,我們可能會包含各種 JAR 文件作為庫。
在本教程中,我們將探討如何從給定的類中找到 JAR 文件及其完整路徑。
2. 問題介紹
假設我們在運行時有一個Class
對象。我們的目標是找出該類屬於哪個 JAR 文件。
一個例子可以幫助我們快速理解問題。假設我們有 Guava 的Ascii
類的類實例。我們想創建一個方法來找出包含Ascii
類的 JAR 文件的完整路徑。
我們將主要介紹兩種不同的方法來獲取 JAR 文件的完整路徑。此外,我們將討論它們的優缺點。
為簡單起見,我們將通過單元測試斷言來驗證結果。
接下來,讓我們看看他們的行動。
3. 使用getProtectionDomain()
方法
Java 的類對象提供了getProtectionDomain()
方法來獲取ProtectionDomain
對象。然後,我們可以通過ProtectionDomain
對象獲取CodeSource
。 CodeSource
實例將是我們正在尋找的 JAR 文件。此外, CodeSource.getLocation()
方法為我們提供了 JAR 文件的 URL 對象。最後,我們可以使用Paths
類來獲取 JAR 文件的完整路徑。
3.1。實現byGetProtectionDomain()
方法
如果我們將上面提到的所有步驟包裝在一個方法中,幾行代碼就可以完成這項工作:
public class JarFilePathResolver {
String byGetProtectionDomain(Class clazz) throws URISyntaxException {
URL url = clazz.getProtectionDomain().getCodeSource().getLocation();
return Paths.get(url.toURI()).toString();
}
}
接下來,我們以 Guava Ascii
類為例,測試我們的方法是否按預期工作:
String jarPath = jarFilePathResolver.byGetProtectionDomain(Ascii.class);
assertThat(jarPath).endsWith(".jar").contains("guava");
assertThat(new File(jarPath)).exists();
如我們所見,我們已經通過兩個斷言驗證了返回的jarPath
:
- 首先,路徑應該指向 Guava JAR 文件
- 如果
jarPath
是有效的完整路徑,我們可以從jarPath,
創建一個File
對象,並且該文件應該存在
如果我們運行測試,它就會通過。所以byGetProtectionDomain()
方法按預期工作。
3.2. getProtectionDomain()
方法的一些限制
如上面的代碼所示,我們的byGetProtectionDomain()
方法 非常緊湊和簡單。但是,如果我們閱讀getProtectionDomain()
方法的 JavaDoc,它會說**getProtectionDomain()
方法可能會拋出SecurityException
** 。
我們已經編寫了一個單元測試,並且測試通過了。這是因為我們正在本地開發環境中測試該方法。在我們的示例中,Guava JAR 位於我們的本地 Maven 存儲庫中。因此,沒有引發SecurityException
。
但是,某些平台,例如 Java/OpenWebStart 和某些應用服務器,可能會通過調用getProtectionDomain()
方法來禁止獲取ProtectionDomain
對象。因此,如果我們將應用程序部署到這些平台,我們的方法將失敗並拋出SecurityException.
接下來,讓我們看看另一種獲取 JAR 文件完整路徑的方法。
4. 使用getResource()
方法
我們知道我們調用Class.getResource
()方法來獲取類的資源的URL
對象。那麼我們就從這個方法入手,最終解析出對應JAR文件的全路徑。
4.1。實現byGetResource()
方法
讓我們先看一下實現,然後了解它是如何工作的:
String byGetResource(Class clazz) {
URL classResource = clazz.getResource(clazz.getSimpleName() + ".class");
if (classResource == null) {
throw new RuntimeException("class resource is null");
}
String url = classResource.toString();
if (url.startsWith("jar:file:")) {
// extract 'file:......jarName.jar' part from the url string
String path = url.replaceAll("^jar:(file:.*[.]jar)!/.*", "$1");
try {
return Paths.get(new URL(path).toURI()).toString();
} catch (Exception e) {
throw new RuntimeException("Invalid Jar File URL String");
}
}
throw new RuntimeException("Invalid Jar File URL String");
}
與byGetProtectionDomain
方法相比,上面的方法看起來很複雜。但實際上,它也很容易理解。
接下來,讓我們快速瀏覽一下該方法並了解其工作原理。為簡單起見,我們針對各種異常情況拋出RuntimeException
。
4.2.了解它是如何工作的
首先,我們調用Class.getResource(className)
方法來獲取給定類的 URL。
如果該類來自本地文件系統上的 JAR 文件,則 URL 字符串應採用以下格式:
jar:file:/FULL/PATH/TO/jarName.jar!/PACKAGE/HIERARCHY/TO/CLASS/className.class
例如,下面是 Linux 系統上 Guava 的Ascii
類的 URL 字符串:
jar:file:/home/kent/.m2/repository/com/google/guava/guava/31.0.1-jre/guava-31.0.1-jre.jar!/com/google/common/base/Ascii.class
正如我們所見,JAR 文件的完整路徑位於 URL 字符串的中間。
由於不同操作系統上的文件 URL 格式可能不同,我們將提取“ file:…..jar
”部分,將其轉換回URL
對象,並使用Paths
類以String
形式獲取路徑。
我們構建一個正則表達式並使用String
的replaceAll()
方法來提取我們需要的部分: String path = url.replaceAll(“^jar:(file:.*[.]jar)!/.*”, “$1”);
接下來,類似於byGetProtectionDomain()
方法,我們使用Paths
類獲得最終結果。
現在,讓我們創建一個測試來驗證我們的方法是否適用於 Guava 的Ascii
類:
String jarPath = jarFilePathResolver.byGetResource(Ascii.class);
assertThat(jarPath).endsWith(".jar").contains("guava");
assertThat(new File(jarPath)).exists();
如果我們試一試,測試就會通過。
5.結合兩種方法
到目前為止,我們已經看到了兩種解決問題的方法。 byGetProtectionDomain
方法簡單可靠,但由於安全限制,在某些平台上可能會失敗。
另一方面, byGetResource
方法沒有安全問題。但是,我們需要做更多的手動操作,例如處理不同的異常情況以及使用正則表達式提取 JAR 文件的 URL 字符串。
5.1。實現getJarFilePath()
方法
我們可以將這兩種方法結合起來。首先,讓我們嘗試使用byGetProtectionDomain()
解析 JAR 文件的路徑。如果失敗,我們調用byGetResource()
方法作為備用方法:
String getJarFilePath(Class clazz) {
try {
return byGetProtectionDomain(clazz);
} catch (Exception e) {
// cannot get jar file path using byGetProtectionDomain
// Exception handling omitted
}
return byGetResource(clazz);
}
5.2.測試getJarFilePath()
方法
為了模擬byGetProtectionDomain()
在我們的本地開發環境中拋出SecurityException
,讓我們添加 Mockito 依賴項並使用@Spy
註釋部分模擬JarFilePathResolver
:
@ExtendWith(MockitoExtension.class)
class JarFilePathResolverUnitTest {
@Spy
JarFilePathResolver jarFilePathResolver;
...
}
接下來,我們先測試一下getProtectionDomain()
方法沒有拋出SecurityException
的場景:
String jarPath = jarFilePathResolver.getJarFilePath(Ascii.class);
assertThat(jarPath).endsWith(".jar").contains("guava");
assertThat(new File(jarPath)).exists();
verify(jarFilePathResolver, times(1)).byGetProtectionDomain(Ascii.class);
verify(jarFilePathResolver, never()).byGetResource(Ascii.class);
如上代碼所示,除了測試路徑是否有效外,我們還驗證瞭如果我們可以通過byGetProtectionDomain()
方法獲取 JAR 文件的路徑,那麼永遠不應該調用byGetResource()
方法。
當然,如果byGetProtectionDomain()
拋出SecurityException
,這兩個方法將被調用一次:
when(jarFilePathResolver.byGetProtectionDomain(Ascii.class)).thenThrow(new SecurityException("not allowed"));
String jarPath = jarFilePathResolver.getJarFilePath(Ascii.class);
assertThat(jarPath).endsWith(".jar").contains("guava");
assertThat(new File(jarPath)).exists();
verify(jarFilePathResolver, times(1)).byGetProtectionDomain(Ascii.class);
verify(jarFilePathResolver, times(1)).byGetResource(Ascii.class);
如果我們執行測試,兩個測試都會通過。
六,結論
在本文中,我們學習瞭如何從給定的類中獲取 JAR 文件的完整路徑。
與往常一樣,完整的源代碼可在 GitHub 上獲得。