从类中获取JAR文件的完整路径

2023/06/15

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上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章