聊聊classpath及其资源获取
# 什么是Classpath
Classpath(类路径)是指操作系统或Java运行时环境用于查找类文件和资源文件的路径。它是一组目录和JAR文件的集合,用于告诉Java虚拟机(JVM)在哪里查找类文件和资源文件。
当Java程序执行时,JVM需要加载类文件和资源文件,以执行程序所需的功能。JVM会根据类路径来查找这些文件。类路径中包含的目录和JAR文件会被逐个搜索,直到找到所需的类文件或资源文件为止。
类路径可以由多个元素组成,元素之间使用分隔符进行分隔。在不同的操作系统上,类路径的分隔符可能不同。在Windows上,分隔符是分号(;),在Unix/Linux上,分隔符是冒号(:)。
类路径的设置有多种方式:
系统级别的类路径:可以通过设置操作系统的环境变量来指定类路径。例如,在Windows上可以设置
CLASSPATH
环境变量,将类路径设置为需要的目录和JAR文件。命令行参数:可以在命令行启动Java程序时,使用
-classpath
或-cp
参数来指定类路径。例如:java -cp /path/to/classes:/path/to/lib/* com.example.MyClass
。在IDE中设置:在开发环境的IDE中,可以通过配置项目的构建路径或依赖项来设置类路径。
需要注意的是,类路径的设置会影响到Java程序的运行。如果类文件或资源文件无法在类路径中找到,程序可能会抛出ClassNotFoundException
或NoClassDefFoundError
等异常。因此,正确设置类路径对于程序的正常运行非常重要。
# 资源获取的方式
# 1. 使用Class类中的方法
# a. 使用getResource()方法
在Java中,可以使用Class类的getResource()方法获取classpath下的资源。该方法返回一个URL对象,表示与给定字符串名相关的资源。
具体的使用方法如下:
URL url = YourClass.class.getResource("yourResourceName");
在上述代码中,YourClass.class
表示你当前的类,"yourResourceName"
是你在classpath下的资源名称,例如图片、音乐、文本文件等。
getResource() 方法在查找资源时,会首先在该类所在的包的目录下找,如果找不到,就在 CLASSPATH 环境变量所指定的路径下面找。
如果资源存放在src文件夹下,可以直接用资源的名称获取。如果资源在包里面,需要在资源名称前面加上包的名称,并用"/"替代"."。如:
URL url = YourClass.class.getResource("com/example/package/yourResourceName");
注意:如果在资源路径前面加"/",getREsource()方法会从ClassPath根下获取。例如:
URL url = YourClass.class.getResource("/yourResourceName");
这个时候,不管你的资源在那个包中,它都会从classpath根下查找。
最后,你可以通过返回的URL对象,获取到你资源的具体信息。例如:
URLConnection connection = url.openConnection();
InputStream inputStream = connection.getInputStream();
以上代码,你就可以获取到你资源的输入流,然后你可以根据你的需要,进行后续的操作。
# b. getResourceAsStream()方法
Class 类的 getResourceAsStream()
方法用于访问与类路径(classpath)相关的资源,它返回一个 InputStream
对象,可以直接用于读取资源数据。如果在资源路径前加 "/",getResourceAsStream()
方法会从 classpath 根目录下查找资源。
这个方法与 getResource()
方法类似,但是返回的是 InputStream
对象,而不是 URL
对象。
这是一个使用 getResourceAsStream()
方法的例子:
InputStream inputStream = YourClass.class.getResourceAsStream("/yourResourceName");
// 使用 BufferedReader 对象来读取资源
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
reader.close();
在上面的代码中,YourClass.class
代表当前类,"/yourResourceName"
是要加载的资源路径,应该是相对于 classpath 的路径。然后通过 InputStream
和 BufferedReader
对象,我们可以按行读取文件资源的数据。
请注意:在资源路径前加 "/" 时,getResourceAsStream()
会从 classpath 根目录下查找资源。 如果资源在包中,需要在资源名称前加包名,用 "/" 替换包名中的 ".":如 "com/example/package/yourResourceName"。
# 2. 使用ClassLoader类中的方法
在Java中,我们可以使用ClassLoader类中的一些方法来获取classpath资源。ClassLoader是Java中的一个抽象基类,它用于加载类和资源。
- getResource(String name):这个方法返回一个URL对象,该对象指向给定名称的资源。如果找不到资源,它将返回null。这个名称应该是一个相对于类路径的绝对路径。
URL url = getClass().getClassLoader().getResource("myResource.txt");
- getResourceAsStream(String name):这个方法返回一个InputStream对象,可以用于读取资源的内容。如果找不到资源,它将返回null。
InputStream in = getClass().getClassLoader().getResourceAsStream("myResource.txt");
- getResources(String name):这个方法返回一个枚举的URL对象,这些对象指向所有找到的给定名称的资源。如果找不到任何资源,它将返回一个空的枚举。
Enumeration<URL> urls = getClass().getClassLoader().getResources("myResource.txt");
# Class和ClassLoader类中的getResource方法的区别
Class.getResource(String path)
和ClassLoader.getResource(String path)
两个方法都是用来从classpath获取资源的。但是他们对资源路径的解析方式略有不同,这决定了在实际使用时需要提供不同的路径参数:
Class.getResource(String path)
:如果路径以'/'开头,那么它会从classpath的根路径开始寻找资源。否则,它会从当前类的包路径开始寻找资源。例如,如果当前类的包是com.example
,那么使用没有'/'前缀的路径"myResource.txt"
实际上会查找"com/example/myResource.txt"
。如果希望从classpath根路径查找资源,需要添加'/'前缀,例如"/myResource.txt"
。ClassLoader.getResource(String path)
:无论路径参数是否以'/'开头,都会从classpath的根路径开始寻找资源。所以'/'前缀在这种情况下是没有必要的,甚至可能产生错误的结果。
总结起来,Class.getResource()
相对于当前类所在的包进行解析,而ClassLoader.getResource()
则始终相对于classpath根路径进行解析。在实际使用时,需要根据具体的使用场景和资源路径来选择合适的方法。
# 3. Java 9模块化系统中的资源获取
Java 9引入了新的模块系统,旨在解决大型应用中的各种问题,如类路径问题、JAR地狱问题、封装问题等。
由于模块化系统对应用程序的打包和部署有所不同,因此,传统类路径上的资源可能不再可用,或者需要使用新的API来访问。
在Java 9以后的版本中,你仍然可以使用 ClassLoader
类中的 getResource()
、getResourceAsStream()
和 getResources()
方法, 但这大多数情况下适用于未模块化的代码。
对于模块化的代码,你应该使用 Module
类中的 getResourceAsStream
方法,该方法允许你从模块的路径中访问资源。例如,以下代码从当前模块获取了一个名为 "myResource.txt" 的资源:
InputStream in = getClass().getModule().getResourceAsStream("myResource.txt");
试图使用 ClassLoader
来加载模块资源可能会导致 null
值返回,因为资源可能被模块系统封装,以防止意外访问。
因此,如果你正在迁移现有代码以使用Java模块,或者正在编写新的模块化代码,应考虑使用 Module
类中的方法来访问你的资源。
如果要获取所有模块的资源,可以使用 ModuleLayer
类中的 findModule()
方法来获取模块,然后使用 Module
类中的 getResourceAsStream()
方法来获取资源。例如:
ModuleLayer layer = ModuleLayer.boot();
Set<Module> modules = layer.modules();
for (Module module : modules) {
InputStream in = module.getResourceAsStream("myResource.txt");
// ...
}
需要注意的是,在使用 GraalVM 创建的 Native Image 中,会跳过使用模块化系统的资源查找,其根本原因在于 GraalVM Native Image 编译应用程序为机器代码,过程中已经将应用程序编译到了固定的可执行文件中,因此无法再动态加载模块化系统的资源。
GraalVM Native Image 提前将应用程序的代码与所有依赖项打包,所有程序需要的资源都应当在编译时进行加载。编译后的应用程序无法动态地从类路径或磁盘中加载额外的资源或类,这也缩小了其安全攻击面。
因此,如果你的应用程序需要动态加载资源,那么你应该考虑使用传统的类路径,而不是模块化系统。
# 不同系统的Classpath资源获取
# OSGi bundles
在 OSGi 中,要获取类路径资源,可以使用 Bundle
类的 getEntry
方法或 findEntries
方法,这两个方法分别用于获取单个资源和搜索多个资源。
以下是一个例子:
Bundle bundle = FrameworkUtil.getBundle(myClass);
URL url = bundle.getEntry("myResource.txt");
if (url != null) {
// use the URL
}
如果你想搜索多个资源:
Bundle bundle = FrameworkUtil.getBundle(myClass);
Enumeration<URL> urls = bundle.findEntries("/", "myResource.txt", true);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
// use the URL
}
注意这些方法获取的是 bundle 内部的资源,不会搜索整个类路径。如果你需要搜索整个类路径,需要遍历所有已加载的 bundle,并对每个 bundle 调用 getEntry
或 findEntries
方法。
在 OSGi 中,类路径是与 bundle 相关的,每个 bundle 具有自己的类加载器,因此不像在传统 Java 应用中那样直接使用 ClassLoader.getSystemResource
或 ClassLoader.getSystemResources
。
需要注意的是,上面获取的URL表示的是bundle内部的资源,而不是类路径上的资源。 可以使用FileLocator.resolve(URL)
处理并解析bundle资源的URL,它会返回一个指向资源实际位置的URL。比如,如果资源被存储在一个JAR文件内,方法会返回指向JAR文件中资源的URL。
以下是如何使用的一个例子:
Bundle bundle = FrameworkUtil.getBundle(MyClass.class);
URL url = bundle.getEntry("myResource.txt");
URL resolvedUrl = FileLocator.resolve(url);
if (resolvedUrl != null) {
InputStream is = resolvedUrl.openStream();
// 使用输入流处理资源
}
这样,你将可以访问并处理存储在bundle或者关联JAR文件中的资源。
# JBoss VFS
为了在JBoss VFS中获取classpath资源,你可以使用JBoss VFS的API进行操作。
首先需要获取VirtualFile
的实例,这个实例代表着类路径中的一个资源或者目录。你可以使用VFS.getChild(String path)
方法来获取这个实例。然后你可以执行一些操作,比如获取它的URL、获取它的输入流、获取它下面的子资源等等。
以下是一个例子:
import org.jboss.vfs.VFS;
import org.jboss.vfs.VirtualFile;
...
String resourceName = "myResource.txt";
VirtualFile resourceFile = VFS.getChild(resourceName);
if (resourceFile.exists()) {
URL resourceURL = resourceFile.toURL();
// do something with the URL
}
你可以使用VirtualFileVisitor
接口来遍历一个目录下的所有子资源。以下是一个例子:
import org.jboss.vfs.VFS;
import org.jboss.vfs.VirtualFile;
import org.jboss.vfs.VisitorAttributes;
import org.jboss.vfs.VFSUtils;
import org.jboss.vfs.VirtualFileVisitor;
import org.jboss.vfs.VirtualVisitException;
...
VirtualFile directory = VFS.getChild("myDirectory");
directory.visit(new VirtualFileVisitor() {
public VisitorAttributes getAttributes() {
return VisitorAttributes.DEFAULT;
}
public void visit(VirtualFile file) throws IOException {
System.out.println("Found resource: " + file.getPathName());
}
});
这个例子中,visit(VirtualFile file)
方法会被调用一次,对directory
下的每个子资源。数次调用的时候,它会打印出资源的路径名。
# jar、war和zip文件
首先明确一个概念:Jar文件和War文件实际上都是一种特殊的压缩文件,它们使用了Zip格式进行压缩。这意味着你可以将Jar文件或War文件重命名为.zip,并使用标准的解压工具进行解压缩,以查看其中的内容。
Jar(Java Archive)文件是Java平台上常用的打包格式,用于打包Java类文件、资源文件和其他文件,以便于在Java应用程序中进行发布和共享。除了可以包含Java类和资源文件外,Jar文件还可以包含元数据、清单文件(Manifest)和其他信息。
War(Web Application Archive)文件是一种用于打包和分发Web应用程序的压缩文件格式,主要用于Java Web应用程序的部署。War文件通常包含Web应用程序的Servlet、JSP、HTML、CSS、JavaScript等文件,以及Web应用程序的配置文件和资源文件。
因此,Jar文件和War文件都是基于Zip格式的压缩文件,但它们具有不同的用途和约定的结构。在Java中,可以使用JarURLConnection类来读取和操作Jar文件和War文件中的内容。
以下是一个读取Jar文件中资源的例子:
URL url = new URL("jar:file:/path/to/myJar.jar!/myResource.txt");
JarURLConnection connection = (JarURLConnection) url.openConnection();
InputStream inputStream = connection.getInputStream();
# Spring 中的classpaht资源获取逻辑
在Spring中,对资源(Resource)的获取是采用统一的资源加载类ResourceLoader
的getResource()
方法来实现的。该方法主要是根据 classpath
前缀来获取指定的路径下的资源。
Spring解析资源路径的具体逻辑是:如果路径没有前缀,则默认添加 classpath:
前缀;如果有前缀,根据前缀的情况选择相应的资源加载器。classpath:
和 classpath*:
前缀分别使用 ClassPathResource
和 PathMatchingResourcePatternResolver
资源加载器。
ClassPathResource
是基于类路径的资源加载器,它将资源路径视为类路径。如果路径以 /
开头,则忽略前导 /
,直接将路径视为类路径;否则,如果路径不以 /
开头,会相对于该类的包路径进行搜索资源。
PathMatchingResourcePatternResolver
是一个基于模式匹配的资源加载器,它可以根据路径模式匹配类路径下的多个资源。
同时,还支持其他前缀,例如:
file:
前缀,使用FileSystemResource
加载器,基于文件系统路径加载资源;http:
或https:
前缀,使用UrlResource
加载器,基于 URL 加载资源;ftp:
或sftp:
前缀,同样使用UrlResource
加载器,基于 FTP 或 SFTP 协议加载资源。
无前缀时,默认使用 ClassPathResource
加载器,基于类路径加载资源。
不同类型的加载器区别在于,它们加载资源的根位置不同。例如,类路径资源加载器的根位置就是类路径,而文件系统资源加载器的根位置就是文件系统。
PathMatchingResourcePatternResolver
支持OSGI、JBoss VFS、Jar、War、Zip等资源的获取,是一个很强大的工具类。
以下是一些基本用法的示例:
通过
classpath*:
加载类路径下的所有匹配资源:ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); Resource[] resources = resolver.getResources("classpath*:com/example/**/*.xml"); for (Resource resource : resources) { System.out.println(resource.getDescription()); }
上述代码会加载类路径下
com/example
路径及其所有子目录下的所有XML文件。通过
file:
加载文件系统路径下的资源:ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); Resource[] resources = resolver.getResources("file:/path/to/**/*.xml"); for (Resource resource : resources) { System.out.println(resource.getDescription()); }
上述代码会加载文件系统路径
/path/to
及其所有子目录下的所有XML文件。通过
classpath:
加载类路径下单个资源:ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); Resource resource = resolver.getResource("classpath:com/example/config.xml"); System.out.println(resource.getDescription());
上述代码会加载类路径下
com/example/config.xml
资源。
请注意,PathMatchingResourcePatternResolver
也支持类路径下的jar
、war
、zip
等包中的资源加载,但需要注意不同运行环境可能存在差异,比如在IDE环境和实际部署环境中类路径的解析可能有所不同,需要根据实际环境调整资源路径或配置。
# 总结
总的来说,Java的Classpath是Java查找类文件和资源文件的路径,类路径包括的目录和JAR文件会按顺序逐个搜索,直到找到所需的类文件或资源文件。在平时的开发工作中,类路径的设置对Java程序的正常运行有着重要的影响。
在资源获取方式上,我们可以利用Class类的getResource()或getResourceAsStream()方法从类路径中获取资源,也可以使用ClassLoader类的对应方法。另外在使用Java9模块化系统的情况下,我们应优先使用Module类的getResourceAsStream方法。除此之外,不同系统,例如OSGi bundles,JBoss VFS,和我们常见的jar、war和zip文件,它们都有各自的获取Classpath资源的方式。
需要强调的是,在使用这些方法获取资源时,需要留意相对路径和绝对路径的使用,以及资源的具体管理方式,以防止出现资源无法访问的问题。
总之,正确的管理和使用Classpath资源是Java应用程序开发中一个至关重要的环节。
祝你变得更强!