Skip to content

资源 (Resources)

本章介绍了 Spring 如何处理资源,以及你如何在 Spring 中使用资源。它包括以下主题:

简介

遗憾的是,Java 标准的 java.net.URL 类及其针对各种 URL 前缀的标准处理器,不足以满足对底层资源的所有访问。例如,没有标准化的 URL 实现可用于访问需要从类路径或相对于 ServletContext 获取的资源。虽然可以为特殊的 URL 前缀注册新的处理器(类似于现有的针对 http: 等前缀的处理器),但这通常非常复杂,且 URL 接口仍然缺乏一些理想的功能,例如检查所指向资源是否存在的方法。

Resource 接口

位于 org.springframework.core.io 包中的 Spring Resource 接口旨在成为一个更强大的接口,用于抽象对底层资源的访问。下面的清单提供了 Resource 接口的概览。有关更多详细信息,请参阅 Resource 的 JavaDoc。

java
public interface Resource extends InputStreamSource {

	boolean exists();

	boolean isReadable();

	boolean isOpen();

	boolean isFile();

	URL getURL() throws IOException;

	URI getURI() throws IOException;

	File getFile() throws IOException;

	ReadableByteChannel readableChannel() throws IOException;

	long contentLength() throws IOException;

	long lastModified() throws IOException;

	Resource createRelative(String relativePath) throws IOException;

	String getFilename();

	String getDescription();
}

Resource 接口的定义所示,它扩展了 InputStreamSource 接口。下面的清单显示了 InputStreamSource 接口的定义:

java
public interface InputStreamSource {

	InputStream getInputStream() throws IOException;
}

Resource 接口中一些最重要的方法是:

  • getInputStream():定位并打开资源,返回一个用于从资源中读取数据的 InputStream。预计每次调用都会返回一个新的 InputStream。调用者有责任关闭该流。
  • exists():返回一个 boolean,指示该资源是否以物理形式实际存在。
  • isOpen():返回一个 boolean,指示该资源是否代表一个带有打开流的句柄。如果为 true,则 InputStream 不能被多次读取,必须仅读取一次然后关闭以避免资源泄露。对于所有常规资源实现,除 InputStreamResource 外,均返回 false
  • getDescription():返回此资源的描述,用于处理该资源时的错误输出。这通常是文件的完全限定名称或资源的实际 URL。

其他方法允许你获取代表该资源的实际 URLFile 对象(如果底层实现兼容且支持该功能)。

一些 Resource 接口的实现还实现了扩展的 WritableResource 接口,用于支持写入操作。

Spring 自身广泛使用 Resource 抽象,在许多需要资源的方法签名中将其作为参数类型。一些 Spring API 中的其他方法(例如各种 ApplicationContext 实现的构造函数)接受一个 String,该字符串以朴素或简单的形式用于创建适合该上下文实现的 Resource;或者通过字符串路径上的特殊前缀,让调用者指定必须创建并使用特定的 Resource 实现。

虽然 Resource 接口在 Spring 中被大量使用,但它作为你代码中访问资源的通用工具类也非常方便,即使你的代码不知道或不关心 Spring 的任何其他部分。虽然这会将你的代码与 Spring 耦合,但它实际上仅耦合到这组很小的工具类,它们可以作为 URL 的更强大替代品,并且可以被视为与你用于此目的的任何其他库等价。

TIP

Resource 抽象并不取代功能。它在可能的情况下包装功能。例如,UrlResource 包装了一个 URL,并使用包装后的 URL 来执行其工作。

内置 Resource 实现

Spring 包含多个内置的 Resource 实现:

有关 Spring 中可用 Resource 实现的完整列表,请查阅 Resource JavaDoc 的 "All Known Implementing Classes" 部分。

UrlResource

UrlResource 封装了 java.net.URL,可用于访问通常可以通过 URL 访问的任何对象,如文件、HTTPS 目标、FTP 目标等。所有 URL 都有标准化的 String 表示,使用标准前缀指示一种 URL 类型。这包括用于访问文件系统路径的 file:,用于通过 HTTPS 协议访问资源的 https:,用于通过 FTP 访问资源的 ftp: 等。

UrlResource 可以由 Java 代码通过显式使用其构造函数创建,但通常在调用接受代表路径的 String 参数的 API 方法时隐式创建。对于后者,JavaBeans 的 PropertyEditor 最终决定创建哪种类型的 Resource。如果路径字符串包含已知的前缀(例如 classpath:),它会为该前缀创建合适的专用 Resource。但是,如果它不识别前缀,则假设该字符串是标准 URL 字符串并创建 UrlResource

ClassPathResource

该类代表应从类路径获取的资源。它使用线程上下文类加载器、给定的类加载器或给定的类来加载资源。

如果类路径资源驻留在文件系统中,则此 Resource 实现支持将其解析为 java.io.File;但对于驻留在 jar 中且未解压到文件系统的类路径资源,则不支持解析为 File。为了解决这个问题,各种 Resource 实现始终支持解析为 java.net.URL

ClassPathResource 可由 Java 代码显式创建,但通常在路径字符串带有特殊前缀 classpath: 时被隐式创建。

FileSystemResource

这是针对 java.io.File 句柄的 Resource 实现。它还支持 java.nio.file.Path 句柄,应用 Spring 标准的基于字符串的路径转换,但通过 java.nio.file.Files API 执行所有操作。对于纯粹基于 java.nio.path.Path 的支持,请改用 PathResourceFileSystemResource 支持解析为 FileURL

PathResource

这是针对 java.nio.file.Path 句柄的 Resource 实现,通过 Path API 执行所有操作和转换。它支持解析为 FileURL,并实现了扩展的 WritableResource 接口。PathResource 实际上是 FileSystemResource 的纯 java.nio.path.Path 变体,具有不同的 createRelative 行为。

ServletContextResource

这是针对 ServletContext 资源的 Resource 实现,它解释相关 Web 应用程序根目录内的相对路径。

它始终支持流访问和 URL 访问,但仅在 Web 应用程序归档文件(WAR)已解压且资源物理上位于文件系统上时才允许 java.io.File 访问。是否解压或直接从 JAR/数据库访问取决于具体的 Servlet 容器。

InputStreamResource

InputStreamResource 是针对给定的 InputStream 的实现。仅在没有其他特定 Resource 实现适用时才应使用它。特别是,在可能的情况下,首选 ByteArrayResource 或任何基于文件的 Resource 实现。

与其他 Resource 实现相比,这是一个 已打开 资源的描述符。因此,它从 isOpen() 返回 true。如果你需要多次读取流,或者需要将资源描述符存储在某处,请勿使用它。

ByteArrayResource

这是针对给定的字节数组的实现。它为给定的字节数组创建一个 ByteArrayInputStream

它对于从任何给定的字节数组加载内容非常有用,而不必求助于一次性的 InputStreamResource

ResourceLoader 接口

ResourceLoader 接口旨在由能够返回(即加载)Resource 实例的对象实现。

java
public interface ResourceLoader {

	Resource getResource(String location);

	ClassLoader getClassLoader();
}

所有的应用上下文(Application Context)都实现了 ResourceLoader 接口。因此,所有的应用上下文都可以用来获取 Resource 实例。

当你在特定的应用上下文上调用 getResource(),且指定的路径没有特定前缀时,你将获得适合该特定应用上下文的 Resource 类型。例如,假设针对 ClassPathXmlApplicationContext 实例运行以下代码:

java
Resource template = ctx.getResource("some/resource/path/myTemplate.txt");
kotlin
val template = ctx.getResource("some/resource/path/myTemplate.txt")

ClassPathXmlApplicationContext 中,该代码返回 ClassPathResource。如果针对 FileSystemXmlApplicationContext 运行相同的方法,它将返回 FileSystemResource。对于 WebApplicationContext,它将返回 ServletContextResource。它会同样地为每个上下文返回适当的对象。

因此,你可以以适合特定应用上下文的方式加载资源。

另一方面,无论应用上下文类型如何,你都可以通过指定特殊的 classpath: 前缀来强制使用 ClassPathResource

java
Resource template = ctx.getResource("classpath:some/resource/path/myTemplate.txt");
kotlin
val template = ctx.getResource("classpath:some/resource/path/myTemplate.txt")

同样,你可以通过指定任何标准的 java.net.URL 前缀来强制使用 UrlResource

java
Resource template = ctx.getResource("file:///some/resource/path/myTemplate.txt");
Resource template2 = ctx.getResource("https://myhost.com/resource/path/myTemplate.txt");
kotlin
val template = ctx.getResource("file:///some/resource/path/myTemplate.txt")
val template2 = ctx.getResource("https://myhost.com/resource/path/myTemplate.txt")

下表总结了将 String 对象转换为 Resource 对象的策略:

前缀示例说明
classpath:classpath:com/myapp/config.xml从类路径加载。
file:file:///data/config.xml从文件系统作为 URL 加载。参见 FileSystemResource 提示
https:https://myserver/logo.png作为 URL 加载。
(无)/data/config.xml取决于底层 ApplicationContext

ResourcePatternResolver 接口

ResourcePatternResolver 接口是 ResourceLoader 接口的扩展,它定义了将位置模式(例如 Ant 风格的路径模式)解析为 Resource 对象的策略。

java
public interface ResourcePatternResolver extends ResourceLoader {

	String CLASSPATH_ALL_URL_PREFIX = "classpath*:";

	Resource[] getResources(String locationPattern) throws IOException;
}

如上所示,该接口还定义了一个特殊的 classpath*: 资源前缀,用于匹配类路径下的所有匹配资源。在这种情况下,资源位置应该是没有占位符的路径——例如 classpath*:/config/beans.xml。类路径中的 JAR 文件或不同目录可以包含具有相同路径和相同名称的多个文件。有关 classpath*: 前缀通配符支持的更多详细信息,请参见应用上下文构造函数资源路径中的通配符及其子章节。

可以检查传入的 ResourceLoader(例如通过 ResourceLoaderAware 提供的)是否也实现了此扩展接口。

PathMatchingResourcePatternResolver 是一个独立的实现,可在 ApplicationContext 之外使用,也被 ResourceArrayPropertyEditor 用于填充 Resource[] 类型的 Bean 属性。它能够将指定的资源位置路径解析为一个或多个匹配的 Resource 对象。源路径可以是一个具有一对一映射的简单路径,或者包含特殊的 classpath*: 前缀和/或内部 Ant 风格的正则表达式(使用 Spring 的 AntPathMatcher 匹配)。这两者实际上都是通配符。

TIP

任何标准 ApplicationContext 中的默认 ResourceLoader 实际上都是实现 ResourcePatternResolver 接口的 PathMatchingResourcePatternResolver 实例。ApplicationContext 实例本身也实现了该接口,并委托给默认的解析器。

ResourceLoaderAware 接口

ResourceLoaderAware 接口是一个特殊的毁调接口,用于识别期望获得 ResourceLoader 引用的组件。

java
public interface ResourceLoaderAware {

	void setResourceLoader(ResourceLoader resourceLoader);
}

当一个类实现 ResourceLoaderAware 并被部署到应用上下文(作为 Spring 管理的 Bean)时,它会被应用上下文识别。应用上下文随后调用 setResourceLoader(ResourceLoader),并将自身作为参数传入(记住,Spring 中的所有应用上下文都实现了 ResourceLoader 接口)。

由于 ApplicationContext 是一个 ResourceLoader,Bean 也可以实现 ApplicationContextAware 接口并直接使用提供的应用上下文来加载资源。但是,通常情况下,如果你只需要资源加载功能,最好使用专门的 ResourceLoader 接口。这样代码仅耦合到资源加载接口(可视为工具接口),而不是整个 Spring ApplicationContext 接口。

在应用程序组件中,你也可以依靠 自动装配 (Autowiring) ResourceLoader 作为实现 ResourceLoaderAware 接口的替代方案。“传统”的 constructorbyType 自动装配模式能够分别为构造函数参数或 setter 方法参数提供 ResourceLoader。为了获得更多灵活性(包括自动装配字段和多参数方法的能力),请考虑使用基于注解的自动装配特性。在这种情况下,只要字段、构造函数或方法带有 @Autowired 注解,ResourceLoader 就会被装配进去。有关更多信息,请参阅使用 @Autowired

TIP

要为包含通配符或使用 classpath*: 前缀的路径加载一个或多个 Resource 对象,请考虑将 ResourcePatternResolver 实例自动装配到你的组件中,而不是 ResourceLoader

资源作为依赖

如果 Bean 本身将通过某种动态过程确定和提供资源路径,那么让该 Bean 使用 ResourceLoaderResourcePatternResolver 接口来加载资源可能更有意义。例如,考虑加载某种模板,其中所需的特定资源取决于用户的角色。如果资源是静态的,则完全消除 ResourceLoader 接口(或 ResourcePatternResolver 接口)的使用,让 Bean 公开它需要的 Resource 属性,并期望它们被注入到其中,这更有意义。

使注入这些属性变得微不足道的是,所有应用上下文都注册并使用一个特殊的 JavaBeans PropertyEditor,它可以将 String 路径转换为 Resource 对象。例如,以下 MyBean 类具有一个 Resource 类型的 template 属性。

java
public class MyBean {

	private Resource template;

	public void setTemplate(Resource template) {
		this.template = template;
	}
	// ...
}
kotlin
class MyBean(var template: Resource)

在 XML 配置文件中,该属性可以配置一个简单的字符串:

xml
<bean id="myBean" class="example.MyBean">
	<property name="template" value="some/resource/path/myTemplate.txt"/>
</bean>

请注意,资源路径没有前缀。由于应用上下文本身将被用作 ResourceLoader,因此根据上下文的具体类型,资源通过 ClassPathResourceFileSystemResourceServletContextResource 加载。

如果需要强制使用特定的 Resource 类型,可以使用前缀:

xml
<property name="template" value="classpath:some/resource/path/myTemplate.txt"/>
<property name="template" value="file:///some/resource/path/myTemplate.txt"/>

如果使用注解驱动配置,可以将路径存储在属性文件中(例如 Spring Environment 提供的属性)。然后可以使用 @Value 注解结合占位符引用路径。Spring 将检索路径字符串,特殊的 PropertyEditor 将其转换为 Resource 对象注入。

java
@Component
public class MyBean {

	private final Resource template;

	public MyBean(@Value("${template.path}") Resource template) {
		this.template = template;
	}
}
kotlin
@Component
class MyBean(@Value("\${template.path}") private val template: Resource)

如果我们要支持在类路径的多个位置(例如类路径中的多个 JAR)下发现的多个模板,可以使用特殊的 classpath*: 前缀和通配符。

java
@Component
public class MyBean {

	private final Resource[] templates;

	public MyBean(@Value("${templates.path}") Resource[] templates) {
		this.templates = templates;
	}
}
kotlin
@Component
class MyBean(@Value("\${templates.path}") private val templates: Array<Resource>)

应用上下文与资源路径

本节介绍如何使用资源创建应用上下文,包括与 XML 一起使用的快捷方式、如何使用通配符以及其他细节。

构造应用上下文

应用上下文构造函数(针对特定的应用上下文类型)通常接受一个或多个字符串作为资源的位置路径,例如组成上下文定义的 XML 文件。

当此类路径没有前缀时,具体创建哪种 Resource 取决入具体的应用上下文。例如,ClassPathXmlApplicationContext 将使用 ClassPathResource,而 FileSystemXmlApplicationContext 将使用 FileSystemResource(通常相对于当前工作目录)。

使用特殊的 classpath 前缀或标准的 URL 前缀会覆盖默认类型。例如,使用 FileSystemXmlApplicationContext 但带上 classpath:conf/appContext.xml 前缀,将从类路径加载定义,但它仍然是一个 FileSystemXmlApplicationContext,如果后续作为 ResourceLoader 使用,无前缀路径仍被视为文件系统路径。

实例化 ClassPathXmlApplicationContext 的快捷方式

ClassPathXmlApplicationContext 提供了一些便捷的构造函数。基本思想是你可以仅提供 XML 文件的文件名字符串数组(不带路径信息),并提供一个 Class。上下文将从提供的类中派生路径信息。

假设目录结构如下:

text
com/
  example/
    services.xml
    repositories.xml
    MessengerService.class

你可以这样实例化:

java
ApplicationContext ctx = new ClassPathXmlApplicationContext(
	new String[] {"services.xml", "repositories.xml"}, MessengerService.class);
kotlin
val ctx = ClassPathXmlApplicationContext(arrayOf("services.xml", "repositories.xml"), MessengerService::class.java)

应用上下文构造函数资源路径中的通配符

资源路径可以包含 Ant 风格的模式或特殊的 classpath*: 前缀。

这在组件化组装应用时非常有用。所有组件都可以将其上下文定义片段发布到类路径下的某个已知位置,最后使用带有 classpath*: 前缀的路径即可自动拾取所有片段。

请注意,这种通配符解析是特定于构造函数的,在构造时解析。它与 Resource 类型本身无关,你不能使用 classpath*: 前缀来构造单个 Resource 实例。

Ant 风格的模式

路径位置可以包含 Ant 风格的模式,例如:

text
/WEB-INF/*-context.xml
com/mycompany/**/applicationContext.xml
file:C:/some/path/*-context.xml
classpath:com/mycompany/**/applicationContext.xml

解析器会为路径中最后一个非通配符段之前的部分获取一个 URL。如果不是 JAR URL,则获取 java.io.File 并遍历文件系统;如果是 JAR URL,则解析器要么获取 JarURLConnection,要么手动解析 JAR 内容。

关于可移植性的说明:

  • file URL 保证通配符工作。
  • 对于 classpath 位置,结果取决于 ClassLoader.getResource() 返回的 URL 类型。在某些环境下,解析 JAR 中的通配符可能会失败,建议在使用前彻底测试。

classpath*: 前缀

该前缀指定必须获取类路径下匹配给定名称的所有资源(通过 ClassLoader.getResources(...) 调用),然后合并。

TIP

类路径通配符依赖于 ClassLoader.getResources()。由于现在的应用服务器提供自己的类加载器,行为可能有所不同。你可以通过调用 getClass().getClassLoader().getResources("<someFileInsideTheJar>") 来进行测试。

FileSystemResource 提示

FileSystemResource 没有附加到 FileSystemApplicationContext 时,它会按预期处理绝对和相对路径。

但出于历史兼容性原因,当 FileSystemApplicationContext 作为 ResourceLoader 时,它会强制所有附加的 FileSystemResource 实例将所有路径视为相对路径,无论是否以斜杠开头。这意味着以下是等效的:

  • new FileSystemXmlApplicationContext("conf/context.xml")
  • new FileSystemXmlApplicationContext("/conf/context.xml")

如果你需要强制使用绝对路径,建议通过 file: 前缀来强制使用 UrlResource

  • ctx.getResource("file:///some/resource/path/myTemplate.txt")

补充教学

1. Resource 接口的底层思想

Resource 接口存在的最大意义是 “位置无关性”。 在传统的 Java 开发中,如果你想读取一个文件,你需要区分它是文件系统中的 File、类路径下的资源流,还是网络上的 URL。 Spring 通过 Resource 接口,让你只需要关心“我如何获取这个流”(getInputStream()),而不需要关心资源到底在哪。

2. 常用操作技巧:如何读取资源内容?

虽然 Resource 提供了流,但手动读取流比较繁琐。Spring 提供了几个实用的工具类:

  • FileCopyUtils:可以快速将资源流复制到字节数组、字符串或另一个输出流。
  • StreamUtils:提供了一些更现代的方法来操作流,例如 copyToString(InputStream, Charset)
  • ResourceUtils(不推荐在代码中大量使用):主要用于解析资源路径。

3. @Value 注解的“魔力”解析

你可能会好奇,为什么 @Value("classpath:config.properties") Resource res 能直接工作? 这是因为 Spring 内部注册了一个 ResourceEditor(基于 JavaBeans 的 PropertyEditor 机制)。在属性注入阶段,当 Spring 发现目标类型是 Resource 而输入值是 String 时,会自动调用 ResourceLoader 接口的 getResource() 方法进行转换。

4. ProtocolResolver:扩展你自己的前缀

如果你觉得 classpath:file: 不够用,比如你想支持 oss:(阿里云对象存储)前缀。 你可以实现 ProtocolResolver 接口,并使用 DefaultResourceLoader.addProtocolResolver() 注册它。这样,所有的 ResourceLoader 都能识别你自定义的前缀了。

5. 易混淆的前缀:classpath: vs classpath*:

  • classpath::只从第一个找到的位置加载资源。如果有多个 JAR 包含同名文件,它只认第一个。
  • classpath*::会扫描类路径(包括所有的 JAR)并找到所有匹配的文件。常用于加载插件的配置文件。

Based on Spring Framework.