1. 概述
JAR文件是Java存档。在构建Java应用程序时,我们可能会包含各种JAR文件作为库。
在本教程中,我们将探讨如何从给定类中查找JAR文件及其完整路径。
2. 问题简介
假设我们在运行时有一个Class对象,我们的目标是找出该类属于哪个JAR文件。
一个例子可以帮助我们快速理解问题。假设我们有Guava的Ascii类的Class实例,我们希望创建一个方法来找出包含Ascii类的JAR文件的完整路径。
我们将主要介绍两种不同的方法来获取JAR文件的完整路径。此外,我们将讨论它们的优缺点。
为简单起见,我们将通过单元测试断言来验证结果。
接下来,让我们看看它们的实际效果。
3. 使用getProtectionDomain()方法
Java的Class对象提供了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注解部分mock 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);
如果我们执行测试,两个测试都会通过。
6. 总结
在本文中,我们学习了如何从给定类中获取JAR文件的完整路径。
与往常一样,本教程的完整源代码可在GitHub上获得。