概述
JAR 文件是 Java 的归档格式。在构建 Java 应用时,我们通常会引入多个 JAR 文件作为依赖库。
本文将探讨如何根据给定的类对象,找到其所在的 JAR 文件及其完整路径。
问题场景
假设我们在运行时获取到一个 Class
对象,目标是确定该类属于哪个 JAR 文件。
举个例子:我们持有 Guava 库中 Ascii
类的实例,需要实现一个方法来定位包含该类的 JAR 文件的完整路径。
我们将重点介绍两种获取 JAR 文件路径的方法,并分析各自的优缺点。为简化验证,所有结果将通过单元测试断言确认。
接下来看具体实现。
使用 getProtectionDomain() 方法
Java 的类对象提供了 getProtectionDomain()
方法来获取 ProtectionDomain
对象。通过该对象可进一步获取 CodeSource
,而 CodeSource
实例正是我们需要的 JAR 文件。最后调用 CodeSource.getLocation()
获取 JAR 文件的 URL 对象,再用 Paths
类将其转换为完整路径。
实现 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();
通过两个断言验证结果:
- 路径应指向 Guava 的 JAR 文件
- 该路径应能创建有效的
File
对象且文件实际存在
测试通过,证明方法符合预期。
getProtectionDomain() 方法的局限性
上述方法虽然简洁,但查阅 getProtectionDomain()
的 JavaDoc 会发现:**该方法可能抛出 SecurityException
**。
测试在本地开发环境通过,是因为 Guava JAR 位于本地 Maven 仓库中,未触发安全限制。但在某些平台(如 Java/OpenWebStart 或部分应用服务器)上,调用该方法获取 ProtectionDomain
可能被禁止。若应用部署到此类环境,方法将失败并抛出异常。
接下来看另一种获取路径的方法。
使用 getResource() 方法
我们知道 Class.getResource()
方法能获取类资源的 URL 对象。基于此,我们可以解析出对应 JAR 文件的完整路径。
实现 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:")) {
// 从 URL 字符串中提取 'file:......jarName.jar' 部分
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
,该方法稍显复杂,但逻辑依然清晰。
工作原理解析
首先调用 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 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();
测试通过,方法正常工作。
组合两种方法
目前我们已掌握两种解决方案:byGetProtectionDomain
简单可靠但存在安全限制,byGetResource
无安全问题但需手动处理异常和字符串解析。
实现 getJarFilePath() 方法
可组合两种方法:优先尝试 byGetProtectionDomain()
,失败时回退到 byGetResource()
:
String getJarFilePath(Class clazz) {
try {
return byGetProtectionDomain(clazz);
} catch (Exception e) {
// byGetProtectionDomain 获取路径失败,异常处理逻辑省略
}
return byGetResource(clazz);
}
测试 getJarFilePath() 方法
为模拟本地环境中的 SecurityException
,引入 Mockito 依赖,**使用 @Spy
注解部分模拟 JarFilePathResolver
**:
@ExtendWith(MockitoExtension.class)
class JarFilePathResolverUnitTest {
@Spy
JarFilePathResolver jarFilePathResolver;
...
}
先测试 getProtectionDomain()
正常的情况:
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()
成功,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 文件路径的两种方法:
- **
getProtectionDomain()
**:简洁高效,但在某些受安全限制的平台可能失效 - **
getResource()
**:无安全限制,但需手动处理 URL 解析和异常
通过组合两种方法,既保证了简单场景下的高效性,又增强了复杂环境的兼容性。实际开发中推荐使用组合方案,避免踩坑。
完整源代码可在 GitHub 获取。