1. Overview
1.概述
JAR files are Java archives. We may include various JAR files as libraries when we build Java applications.
JAR文件是Java的档案。当我们构建Java应用程序时,我们可能包括各种JAR文件作为库。
In this tutorial, we’ll explore how to find the JAR file and its full path from a given class.
在本教程中,我们将探讨如何从一个给定的类中找到JAR文件和它的完整路径。
2. Introduction to the Problem
2.对问题的介绍
Let’s say we have a Class object at runtime. Our goal is to find out which JAR file the class belongs to.
假设我们在运行时有一个Class对象。我们的目标是找出该类属于哪个JAR文件。
An example may help us understand the problem quickly. Let’s say we have the class instance of Guava‘s Ascii class. We want to create a method to find out the full path of the JAR file that holds the Ascii class.
一个例子可以帮助我们快速理解这个问题。假设我们有Guava的Ascii类的实例。我们想创建一个方法来找出存放Ascii类的JAR文件的完整路径。
We’ll mainly address two different methods to get the JAR file’s full path. Further, we’ll discuss their pros and cons.
我们将主要讨论两种不同的方法来获得JAR文件的完整路径。此外,我们将讨论它们的优点和缺点。
For simplicity, we’ll verify the result by unit test assertions.
为了简单起见,我们将通过单元测试断言来验证结果。
Next, let’s see them in action.
接下来,让我们看看他们的行动。
3. Using the getProtectionDomain() Method
3.使用getProtectionDomain()方法
Java’s class object provides the getProtectionDomain() method to obtain the ProtectionDomain object. Then, we can get the CodeSource through the ProtectionDomain object. The CodeSource instance will be the JAR file we’re looking for. Further, CodeSource.getLocation() method gives us the URL object of the JAR file. Finally, we can use the Paths class to get the full path of the JAR file.
Java的类对象提供了getProtectionDomain()方法来获取ProtectionDomain对象。然后,我们可以通过ProtectionDomain对象获得CodeSource。CodeSource实例将是我们要找的JAR文件。此外,CodeSource.getLocation()方法给了我们JAR文件的URL对象。最后,我们可以使用Paths类来获得JAR文件的完整路径。
3.1. Implementing the byGetProtectionDomain() Method
3.1.实现byGetProtectionDomain()方法
If we wrap all steps that we’ve mentioned above in a method, a couple of lines will do the job:
如果我们把上面提到的所有步骤都包在一个方法中,几行就可以完成工作。
public class JarFilePathResolver {
String byGetProtectionDomain(Class clazz) throws URISyntaxException {
URL url = clazz.getProtectionDomain().getCodeSource().getLocation();
return Paths.get(url.toURI()).toString();
}
}
Next, let’s take the Guava Ascii class as an example to test if our method works as expected:
接下来,让我们以Guava的Ascii类为例,测试我们的方法是否如预期的那样工作。
String jarPath = jarFilePathResolver.byGetProtectionDomain(Ascii.class);
assertThat(jarPath).endsWith(".jar").contains("guava");
assertThat(new File(jarPath)).exists();
As we can see, we’ve verified the returned jarPath through two assertions:
我们可以看到,我们已经通过两个断言验证了返回的jarPath。
- first, the path should point to the Guava JAR file
- if jarPath is a valid full path, we can create a File object from jarPath, and the file should exist
If we run the test, it passes. So the byGetProtectionDomain() method works as expected.
如果我们运行测试,它通过了。因此,byGetProtectionDomain()方法按预期工作。
3.2. Some Limitations of the getProtectionDomain() Method
3.2.getProtectionDomain()方法的一些局限性
As the code above shows, our byGetProtectionDomain() method is pretty compact and straightforward. However, if we read the JavaDoc of the getProtectionDomain() method, it says the getProtectionDomain() method may throw SecurityException.
正如上面的代码所示,我们的byGetProtectionDomain()方法是相当紧凑和简单的。然而,如果我们阅读getProtectionDomain()方法的JavaDoc,它说the getProtectionDomain()方法可能会抛出SecurityException。
We’ve written a unit test, and the test passes. This is because we’re testing the method in our local development environment. In our example, the Guava JAR is located in our local Maven repository. Therefore, no SecurityException was raised.
我们写了一个单元测试,而且测试通过了。这是因为我们是在本地开发环境中测试该方法。在我们的例子中,Guava JAR位于我们本地的Maven资源库中。因此,没有引发SecurityException。
However, some platforms, for instance, Java/OpenWebStart and some application servers, may prohibit getting the ProtectionDomain object by calling the getProtectionDomain() method. Therefore, if we deploy our application to those platforms, our method will fail and throw SecurityException.
然而,某些平台,例如Java/OpenWebStart和某些应用服务器,可能禁止通过调用getProtectionDomain()方法来获得ProtectionDomain对象。因此,如果我们将应用程序部署到这些平台,我们的方法将失败并抛出SecurityException。
Next, let’s see another approach to get the JAR file’s full path.
接下来,让我们看看另一种获取JAR文件全路径的方法。
4. Using the getResource() Method
4.使用getResource()方法
We know that we call the Class.getResource() method to get the URL object of the resource of the class. So let’s start with this method to resolve the full path of the corresponding JAR file finally.
我们知道,我们调用Class.getResource()方法来获取该类资源的URL对象。因此,让我们从这个方法开始,最终解决相应JAR文件的完整路径。
4.1. Implementing the byGetResource() Method
4.1.实现byGetResource()方法
Let’s first have a look at the implementation and then understand how it works:
让我们先看一下实现情况,然后了解它是如何工作的。
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");
}
Compared to the byGetProtectionDomain approach, the method above looks complex. But in fact, it’s pretty easy to understand as well.
与byGetProtectionDomain方法相比,上面的方法看起来很复杂。但事实上,它也很容易理解。
Next, let’s walk through the method quickly and understand how it works. For simplicity, we throw RuntimeException for various exception cases.
接下来,让我们快速浏览一下这个方法,了解它是如何工作的。为了简单起见,我们为各种异常情况抛出RuntimeException。
4.2. Understanding How It Works
4.2.了解它是如何工作的
First, we call the Class.getResource(className) method to get the URL of the given class.
首先,我们调用Class.getResource(className)方法来获取指定类的URL。
If the class is from a JAR file on the local filesystem, the URL string should be in this format:
如果该类是来自本地文件系统中的JAR文件,那么URL字符串应该是这样的格式。
jar:file:/FULL/PATH/TO/jarName.jar!/PACKAGE/HIERARCHY/TO/CLASS/className.class
For example, here’s the URL string of Guava’s Ascii class on a Linux system:
例如,这里是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
As we can see, the full path of the JAR file lies in the middle of the URL string.
我们可以看到,JAR文件的完整路径位于URL字符串的中间。
As the file URL format on different operating systems may differ, we’ll extract the “file:…..jar” part, convert it back to a URL object, and use the Paths class to get the path as a String.
由于不同操作系统上的文件URL格式可能不同,我们将提取”file:…..jar“部分,将其转换回URL对象,并使用Paths类来获得作为String的路径。
We build a regex and use String‘s replaceAll() method to extract the part we need: String path = url.replaceAll(“^jar:(file:.*[.]jar)!/.*”, “$1”);
我们建立一个重合词,并使用String的replaceAll()方法来提取我们需要的部分。String path = url.replaceAll(“^jar:(file:.*[.]jar)!/.*”, “$1”);
Next, similar to the byGetProtectionDomain() approach, we get the final result using the Paths class.
接下来,与byGetProtectionDomain()方法类似,我们使用Paths类获得最终结果。
Now, let’s create a test to verify if our method works with Guava’s Ascii class:
现在,让我们创建一个测试来验证我们的方法是否能在Guava的Ascii类中工作。
String jarPath = jarFilePathResolver.byGetResource(Ascii.class);
assertThat(jarPath).endsWith(".jar").contains("guava");
assertThat(new File(jarPath)).exists();
The test will pass if we give it a run.
如果我们让它运行一下,测试就会通过。
5. Combining the Two Methods
5.结合这两种方法
So far, we’ve seen two approaches to solve the problem. The byGetProtectionDomain approach is straightforward and reliable, but may fail on some platforms due to security limitations.
到目前为止,我们已经看到两种方法来解决这个问题。byGetProtectionDomain方法直接而可靠,但由于安全限制,在某些平台上可能失败。
On the other hand, the byGetResource method doesn’t have security issues. However, we need to do more manual manipulations, such as handling different exception cases and extracting the URL string of the JAR file using regex.
另一方面,byGetResource方法并不存在安全问题。然而,我们需要做更多的手工操作,比如处理不同的异常情况,以及使用regex提取JAR文件的URL字符串。
5.1. Implementing the getJarFilePath() Method
5.1.实现getJarFilePath()方法
We can combine the two methods. First, let’s try to resolve the JAR file’s path with byGetProtectionDomain(). If it fails, we call the byGetResource() method as a fallback:
我们可以结合这两种方法。首先,让我们尝试用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. Testing the getJarFilePath() Method
5.2.测试getJarFilePath()方法
To simulate byGetProtectionDomain() throwing SecurityException in our local development environment, let’s add Mockito dependency and partially mock the JarFilePathResolver using the @Spy annotation:
为了在本地开发环境中模拟byGetProtectionDomain()抛出SecurityException,让我们添加Mockito依赖,并使用@Spy注释部分模拟JarFilePathResolver。
@ExtendWith(MockitoExtension.class)
class JarFilePathResolverUnitTest {
@Spy
JarFilePathResolver jarFilePathResolver;
...
}
Next, let’s first test the scenario that the getProtectionDomain() method doesn’t throw a SecurityException:
接下来,让我们首先测试一下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);
As the code above shows, apart from testing whether the path is valid, we also verify that if we can get the JAR file’s path by the byGetProtectionDomain() method, the byGetResource() method should never be called.
如上面的代码所示,除了测试路径是否有效外,我们还验证了如果我们可以通过byGetProtectionDomain()方法获得JAR文件的路径,byGetResource()方法就不应该被调用。
Of course, if byGetProtectionDomain() throws SecurityException, the two methods will be called once:
当然,如果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);
If we execute the tests, both tests pass.
如果我们执行测试,两个测试都通过了。
6. Conclusion
6.结语
In this article, we’ve learned how to get a JAR file’s full path from a given class.
在这篇文章中,我们已经学会了如何从一个给定的类中获得一个JAR文件的完整路径。
As always, the complete source code is available over on GitHub.
一如既往,完整的源代码可在GitHub上获得,。