资源抽象(Resources)

Introduction

在 Java 中 java.net.URL 用来描述资源,但是对于某些资源,例如:类路径下的资源的访问,需要注册一个处理器 来处理,就像处理 http: 资源一样,之所以可以处理是因为 jdk 内置了。这种机制太复杂了。然而这种机制还缺少一些必要的 功能,例如检查资源的存在与否。

Resource 接口

Resource 接口在 cn.taketoday.core.io. 包中。它的出现抽象各种资源成为可能。 详细 API 可以查看 Resource.

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 getName();

}

Resource 接口中一些重要的方法如下:

Some of the most important methods from the Resource interface are:

  • getInputStream(): 返回一个 InputStream 从资源中读取。每次调用都会返回一个新的 InputStream。调用者负责关闭流。

  • exists(): 指示该资源是否实际存在。

  • isOpen(): 返回一个布尔值,指示该资源是否表示具有打开流的句柄。如果为 true,则 InputStream 不能被多次读取, 必须只能被读取一次,然后关闭以避免资源泄漏。对于所有通常的资源实现,返回 false,但 InputStreamResource 除外。

  • toString(): 返回对这个资源的描述,一般用于错误展示,包含的信息一般是 URL 地址,文件名之类的

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

Resource 接口的一些实现还实现了扩展的 WritableResource 接口,用于支持向其写入内容。

框架本身广泛使用 Resource 接口,这些用例都可以作为最佳实践。你可以使用该接口在你的应用程序中用来访问资源尽管有耦合。

内置的 Resource 直线

框架内置了 Resource 的几个实现:

UrlResource

UrlResource 包装了 java.net.URL 他能访问 URL 能够访问的所有资源。比如 文件, HTTP 资源,FTP等。 所有 URL 都有标准化的字符串表示形式,以便适当的标准化前缀用于指示一种 URL 类型与另一种 URL 类型。 这包括 file: 用于访问文件系统路径,https: 用于通过以下方式访问资源 HTTPS 协议、ftp: 用于通过 FTP 等访问资源。

UrlResource 一般使用一个包含字符串的构造器构造。这个字符串一般是代表着一条路径。

var resource = new UrlResource("http://localhost:9090");
    resource = UrlResource.from("http://localhost:9090");

ClassPathResource

这个实现代表着该资源是一个类路径下的资源。 它用线程上下文类加载器、给定的类加载器或给定的类加载资源。

它的实际底层可能是代表一个文件类型,或者是一个 URL 因为该资源可能存在 JAR 包中。在获取流 getInputStream() 的时候统一使用底层类加载器获取。

ClassPathResource 也是使用带有字符串的构造器构造,字符串标识类路径下的某个资源,一般不包含 classpath: 前缀。 该前缀是使用在 ResourceLoader#getResource 的接口。用来表示我要查找类路径下的资源。

FileSystemResource

该实现是代表着一个文件系统的 java.io.File 对象还支持 java.nio.file.Path 对象。对于纯 java.nio.file.Path 的 操作可以使用 PathResource

PathResource

该实现是包装了 java.nio.file.Path 。对资源所有的操作都将转到 java.nio.file.Path API。 支持解析成 FileURL。它也实现了 WritableResource 接口。

InputStreamResource

该实现适配了一个现有的 InputStream 对象,将 InputStream 直接转换为资源使用。这个实现通常是 一个已经打开了的资源。 isOpen() 方法返回 true。一般不能多次使用,除非底层传入的 InputStream 支持。

ByteArrayResource

该实现是为了适配一个现有的 字节数组getInputStream() 方法将返回一个 ByteArrayInputStream

@Override
public InputStream getInputStream() throws IOException {
  return new ByteArrayInputStream(this.byteArray);
}

可以多次使用该资源。

The ResourceLoader Interface

ResourceLoader 接口用来获取查找 Resource 对象。

public interface ResourceLoader {

  Resource getResource(String location);

  ClassLoader getClassLoader();
}

所有的 ApplicationContext 都实现了该接口,所以他们都有获取(查找)资源的能力。

特定应用程序上下文上调用 getResource() 时,以及位置路径指定没有特定的前缀,您将返回一个 Resource 类型,即 适合特定的应用程序上下文。 例如,假设以下情况针对 ClassPathXmlApplicationContext 实例运行代码片段:

Resource template = loader.getResource("some/resource/path/myTemplate.txt");

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

因此,你可以使用特定的加载器去获取不同类型的资源。

您也可以强制使用 ClassPathResource,无论加载器类型,通过指定特殊的 classpath: 前缀,如下所示

Resource template = loader.getResource("classpath:some/resource/path/myTemplate.txt");

用样的,一也可以使用其他带有 URL 性质的地址例如 filehttps

Resource template = loader.getResource("file:///some/resource/path/myTemplate.txt");
Resource template = loader.getResource("https://myhost.com/resource/path/myTemplate.txt");

下面的表总结了从字符串到 Resource 的对应策略。

Table 1. Resource strings
前缀 举例 解释

classpath:

classpath:com/myapp/config.xml

从类路径加载。

file:

file:///data/config.xml

从文件系统加载。 FileSystemResource 注意事项.

https:

https://myserver/logo.png

URL 加载.

(none)

/data/config.xml

依赖底层实现.

The PatternResourceLoader Interface

PatternResourceLoader 接口继承了(扩展了) ResourceLoader 接口。 他支持 ResourceLoader 的功能以外还支持解析符合通配符的一系列资源。

public interface PatternResourceLoader extends ResourceLoader {

  String CLASSPATH_ALL_URL_PREFIX = "classpath*:";

  void scan(String locationPattern, ResourceConsumer consumer) throws IOException;

  Set<Resource> getResources(String locationPattern) throws IOException;

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

@FunctionalInterface
public interface ResourceConsumer {

  void accept(Resource t) throws IOException;
}

上面的定义中:scan 作为核心方法,其他的两个方法作为其变种。

上面可以看出,这个接口还定义了一个特殊的 classpath*: 资源前缀,表示从类路径中匹配资源。

classpath*:/config/beans.xml 表示将扫描类路径下所有 JAR 包中的 /config/beans.xml 资源。 classpath*:**/beans.xml 表示将扫描类路径下所有 JAR 包中的 beans.xml 资源。

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

PathMatchingPatternResourceLoader 是一个可独立 ApplicationContext 之外使用,也用于 ResourceArrayPropertyEditor 填充 Resource[] bean 属性。 PathMatchingPatternResourceLoader 能够将指定的资源位置路径解析为一个或多个匹配的 Resource 对象。 源路径可以是与目标具有一对一映射的简单路径 Resource,或者可以包含特殊的 classpath*: 前缀和/或内部 Ant 风格的正则表达式(使用 Infra 进行匹配 cn.taketoday.util.AntPathMatcher 实用程序)。后两者都有效通配符。

实现了 PatternResourceLoader 接口的 ApplicationContext,实际上是默认委托给了 PathMatchingPatternResourceLoader

ResourceLoaderAware 接口

ResourceLoaderAware 接口是一个特殊的回调接口,它标识期望提供 ResourceLoader 引用的组件。 以下显示了 ResourceLoaderAware 接口的定义:

public interface ResourceLoaderAware {

  void setResourceLoader(ResourceLoader resourceLoader);
}

当一个类实现了 ResourceLoaderAware 接口,并且被 `ApplicationContext 管理,ApplicationContext 将会 在合适的时机调用 setResourceLoader(ResourceLoader) 把自己作为参数传递给该方法。

由于 ApplicationContext 是一个 ResourceLoader ,因此该 bean 还可以实现 ApplicationContextAware 接口 并直接使用提供的应用程序上下文(ApplicationContext)加载资源。不过,一般来说,最好使用专门的 ResourceLoader 接口。该代码仅与资源加载相关接口(可以被认为是实用程序接口)而不是整个 ApplicationContext 接口。

在应用程序组件中,您还可以依赖 ResourceLoader 的自动装配(实现 ResourceLoaderAware 接口的替代方案)。 传统constructorbyType 自动装配模式(如 自动装配 中所述) 能够作为构造器参数或 setter 方法参数。为了获得更大的灵活性(包括能够自动装配字段和多参数方法),考虑使用基于注释的 自动装配功能。 在这种情况下 ResourceLoader 会自动注入到一个字段中。有关详细信息,请参阅 使用 @Autowired

为了加载含有通配符或者包含特殊的 classpath*: 资源前缀的一个或多个 Resource 对象的时候,请考虑注入 PatternResourceLoader 对象到你的应用中。 而不是使用 ResourceLoader

当资源作为依赖项时

如果 bean 本身依赖了某种资源,那么当然可以考虑使用 ResourceLoader 接口或者 PatternResourceLoader 接口去加载资源。 但也可以直接注入一个 Resource 对象。这个资源对象将是动态的( 对于静态的资源获取 使用 ResourceLoader 接口(或 PatternResourceLoader 接口)更好)。

动态的资源底层使用 JavaBeans PropertyEditor,它可以转换 String 路径到 Resource 对象。 例如,以下 MyBean 类有一个 template

package example;

public class MyBean {

  private Resource template;

  public setTemplate(Resource template) {
    this.template = template;
  }

  // ...
}

在 XML 配置文件中 template 字段,只需配置一个字符串路径即可:

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

请注意,资源路径没有前缀。因此 ApplicationContext 本身将被用作 ResourceLoader,资源可能是 ClassPathResourceFileSystemResourceMockContextResource,具体取决于 应用程序上下文的确切类型。

如果需要强制使用特定的 Resource 类型,可以使用前缀。这以下两个示例展示了如何强制使用 ClassPathResourceUrlResource(后者用于访问文件系统中的文件):

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

如果重构 MyBean 类需要使用注解的方式注入资源,则 myTemplate.txt 的路径可以存储在名为 template.path 的 - 例如, 在可供基础设施 Environment 使用的属性文件中 (详见 Environment 接口). template.path 可以使用 @Value 注解,底层特殊的 PropertyEditor 将会转换字符串到 Resource 对象。

@Component
public class MyBean {

  private final Resource template;

  public MyBean(@Value("${template.path}") Resource template) {
    this.template = template;
  }

  // ...
}

进一步,如果我们想要支持多个模板资源,例如这些资源在多个 JAR 包中,我们可以使用 classpath*: 前缀。 定义 templates.path = classpath*:/config/templates/*.txt 然后就可以注入到以下代码中。

@Component
public class MyBean {

  private final Resource[] templates;

  public MyBean(@Value("${templates.path}") Resource[] templates) {
    this.templates = templates;
  }

  // ...
}

Application Contexts 和资源路径

本节介绍如何使用 resources 创建 application contexts 包括使用 XML、如何使用通配符以及其他方式。

构造 Application Contexts

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

当这样的位置路径没有前缀时,得到的 Resource 用于加载 Bean 定义取决于具体的 application context 。 例如以下示例,创建了一个 ClassPathXmlApplicationContext

ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/appContext.xml");

Bean 定义 从类路径下加载,因为 ClassPathResource 被使用。 然而思考一下下面的例子,使用 FileSystemXmlApplicationContext:

ApplicationContext ctx =new FileSystemXmlApplicationContext("conf/appContext.xml");

上面的代码将从文件系统路径下加载(在这个例子中,从当前的相对路径开始加载)

值得注意的是如果使用特殊的 classpath 前缀或者是 标准的 URL 前缀,这将会覆盖之前的默认加载位置。 例如:

ApplicationContext ctx =
	new FileSystemXmlApplicationContext("classpath:conf/appContext.xml");

使用 FileSystemXmlApplicationContext 从类路径下去加载 Bean 定义 。然而它仍然是 FileSystemXmlApplicationContext。 如果它被当做 ResourceLoader 接口来使用(使用 getResource(String location))那么任何没有前缀的路径仍然当做文件系统路径。

构造 ClassPathXmlApplicationContext 实例 — 快捷方式

ClassPathXmlApplicationContext 提供了几个好用的构造器,基本思想是你可以仅提供一个字符串数组仅包含 XML 文件本身的文件名 (没有前导路径信息)并提供一个 Class

一下是资源目录布局:

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

下面的 ClassPathXmlApplicationContext 实例,在类路径下定义了 services.xmlrepositories.xml

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

其他的构造器使用详见:https://docs.today-tech.cn/today-infrastructure/docs/5.0.0-Draft.1-SNAPSHOT/javadoc-api/cn/taketoday/context/support/ClassPathXmlApplicationContext.html[ClassPathXmlApplicationContext]

Application Context 构造器资源路径中的通配符

Application Context 构造函数值中的资源路径可以是简单路径(如前所示),每个都有一个到目标 Resource 的一对一映射, 或者,可能包含特殊的 classpath*: 前缀或内部 Ant 样式模式(通过使用 PathMatcher 实用程序进行匹配)。 后者实际上都是通配符。

此机制的用途之一是当您需要进行组件式应用程序组装时。 所有组件可以将 Context 定义片段发布到众所周知的位置,并且, 当使用前缀相同的 classpath*: 路径创建 Application Context 时,所有组件片段都会自动读取。

需要注意的是,此通配符特定于 application context 中资源路径构造函数的使用(或者当您直接使用 PathMatcher 实用程序类层次结构时) 并且是构建时解析。 它与 Resource 类型本身无关。不能使用 classpath*: 前缀来构造实际的 Resource,如 一个资源一次仅指向一个资源。

Ant 风格匹配

资源路径可以包含 Ant 样式匹配,如以下示例所示:

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

当路径包含 Ant 样式匹配时,解析器遵循更复杂的尝试解析通配符。直到最后一个非通配符段并从中获取 URL。如果此 URL 不是 jar: URL 或 特定于容器的变体,从中获取一个 java.io.File,并通过遍历来解析通配符文件系统。对于 jar URL,解析器要么得到一个 java.net.JarURLConnection 或者手动解析jar URL,然后遍历 jar 文件的内容来解析通配符。

对可移植性的影响

如果指定的路径已经是一个 file URL(无论是因为 ResourceLoader 是文件系统,还是明确指定的),通配符将以完全可移植的方式工作。

如果指定的路径是一个 classpath 位置,解析器必须通过调用 Classloader.getResource() 来获取最后一个非通配符路径段的 URL。 由于这只是路径的一个节点(而不是最后的文件),因此在这种情况下实际上是未定义的(在 ClassLoader 的javadoc中)返回的 URL 是什么样的。 在实践中,它总是一个代表目录的 java.io.File(其中类路径资源解析为文件系统位置)或某种类型的 jar URL(其中类路径资源解析为jar位置)。 然而,这个操作存在可移植性问题。

如果获取了最后一个非通配符段的 jar URL,则解析器必须能够从中获取一个 java.net.JarURLConnection,或者手动解析 jar URL, 以便能够遍历 jar 的内容并解析通配符。这在大多数环境中都有效,但在其他环境中可能失败, 强烈建议在您的特定环境中充分测试来自 jar 的资源的通配符解析,然后再依赖它。

classpath*: 前缀

当构建基于 XML 的 application context 时,定位字符串可以使用特殊的 classpath*: 前缀,如下例所示:

ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath*:conf/appContext.xml");

这个特殊前缀指定了必须获取所有与给定名称匹配的类路径资源(内部实际上是通过调用 ClassLoader.getResources(…​) 实现的), 然后将它们合并以形成最终的 application context 定义。

通配符类路径依赖于底层 ClassLoadergetResources() 方法。由于大多数应用服务器现在提供自己的 ClassLoader 实现, 因此行为可能会有所不同,特别是在处理 JAR 文件时。一个简单的测试来检查 classpath*: 是否工作是使用 ClassLoader 从类路径中的 JAR 文件中加载文件:getClass().getClassLoader().getResources("<someFileInsideTheJar>")。 尝试使用位于不同位置的具有相同名称的文件进行此测试,例如,在类路径上的不同 JAR 文件中具有相同名称和相同路径的文件。 如果返回了不适当的结果,请查看应用程序服务器文档以了解可能影响 ClassLoader 行为的设置。

另外,您还可以将 classpath*: 前缀与位置路径的其余部分结合使用 PathMatcher 模式(例如,classpath*:META-INF/*-beans.xml)。 在这种情况下,解析策略相当简单:在最后一个非通配符路径段上使用 ClassLoader.getResources() 调用来获取类加载器层次结构中所有匹配的资源, 然后对每个资源使用前面描述的相同 PathMatcher 解析策略来处理通配符子路径。

其他与通配符相关的注意事项

请注意,当与 Ant 样式模式结合使用时,classpath*: 只能可靠地与至少一个根目录配合使用,而不是匹配开始之前,除非实际的目标文件位于文件系统中。 这意味着诸如 classpath*:*.xml 这样的匹配可能无法从 JAR 文件的根目录中检索文件,而只能从已展开的目录的根目录中检索。

框架检索类路径条目的能力源自 JDK 的 ClassLoader.getResources() 方法,该方法仅对空字符串(表示要搜索的潜在根目录)返回文件系统位置。 框架还会计算 URLClassLoader 的运行时配置和 JAR 文件中的 java.class.path 清单,但这并不能保证可移植性。

在扫描类路径包时,需要类路径中存在相应的目录条目。当您使用 Ant 构建 JAR 文件时,请不要激活 JAR 任务的 `files-only`开关。 此外,基于某些环境中的安全策略,类路径目录可能不会被暴露出来,例如 JDK 1.7.0_45 及更高版本的独立应用程序(这需要在清单中设置 'Trusted-Library')。

在 JDK 9 的模块路径(Jigsaw)上,基础设施类路径扫描通常按预期工作。在这里,将资源放入专用目录也是非常推荐的, 这样可以避免搜索 JAR 文件根目录时出现的可移植性问题。

Ant 样式匹配与 classpath: 资源结合使用时,并不能保证在根包在多个类路径位置中都存在时能够找到匹配的资源。考虑以下资源位置的示例:

com/mycompany/package1/service-context.xml

可能使用的用于查找该文件的 Ant 样式路径:

classpath:com/mycompany/**/service-context.xml

这样的资源可能只存在于类路径中的一个位置,但当尝试使用类似前面示例的路径来解析它时,解析器会基于 getResource("com/mycompany") 返回的(第一个)URL 进行工作。如果此基本包节点存在于多个 ClassLoader 位置中,则所需资源可能不会存在于找到的第一个位置。 因此,在这种情况下,您应该优先使用带有相同 Ant 样式匹配的 classpath*:,该模式会搜索所有包含 com.mycompany 基本包的类路径位置:classpath*:com/mycompany/**/service-context.xml

FileSystemResource 注意事项

一个未附加到 FileSystemApplicationContextFileSystemResource (也就是说,当 FileSystemApplicationContext 不是实际的 ResourceLoader 时), 会按照您所期望的方式处理绝对和相对路径。相对路径是相对于当前工作目录,而绝对路径是相对于文件系统的根目录。

ApplicationContext ctx =new FileSystemXmlApplicationContext("conf/context.xml");
ApplicationContext ctx = new FileSystemXmlApplicationContext("/conf/context.xml");

以下示例也是等价的(尽管它们应该是不同的才更合理,因为一个是相对路径,另一个是绝对路径):

FileSystemXmlApplicationContext ctx = ...;
ctx.getResource("some/resource/path/myTemplate.txt");
FileSystemXmlApplicationContext ctx = ...;
ctx.getResource("/some/resource/path/myTemplate.txt");

实际上,如果您需要真正的绝对文件系统路径,应该避免在 FileSystemResourceFileSystemXmlApplicationContext 中使用绝对路径,并通过使用 file: URL 前缀强制使用 UrlResource。以下示例展示了如何这样做:

// actual context type doesn't matter, the Resource will always be UrlResource
ctx.getResource("file:///some/resource/path/myTemplate.txt");
// force this FileSystemXmlApplicationContext to load its definition via a UrlResource
ApplicationContext ctx =
	new FileSystemXmlApplicationContext("file:///conf/context.xml");