Chapter 4. 资源

4.1. 简介

Java标准的 java.net.URL接口和多种URL前缀处理类并不能很好地满足所有底层资源访问的需要。比如,还没有能从类路径或者ServletContext 的相对路径获得资源的标准URL实现。虽然能为特定的URL前缀注册新的处理类(类似已有前缀 http: 的处理类),但是这样做通常比较复杂,而且URL接口还缺少一些有用的功能,比如检查指向的资源是否存在的方法。

4.2.  Resource 接口

Spring的 Resource 接口是为了提供更强的访问底层资源能力的抽象。

public interface Resource extends InputStreamSource {

    boolean exists();

    boolean isOpen();

    URL getURL() throws IOException;

    File getFile() throws IOException;

    Resource createRelative(String relativePath) throws IOException;

    String getFilename();

    String getDescription();
}
public interface InputStreamSource {

    InputStream getInputStream() throws IOException;

}

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

  • getInputStream(): 定位并打开资源,返回读取此资源的一个 InputStream。每次调用预期会返回一个新的 InputStream,由调用者负责关闭这个流。

  • exists(): 返回标识这个资源在物理上是否的确存在的 boolean 值。

  • isOpen(): 返回标识这个资源是否有已打开流的处理类的 boolean 值。如果为 true,则此InputStream 就不能被多次读取, 而且只能被读取一次然后关闭以避免资源泄漏。除了 InputStreamResource,常见的resource实现都会返回 false

  • getDescription(): 返回资源的描述,一般在与此资源相关的错误输出时使用。此描述通常是完整的文件名或实际的URL地址。

其它方法让你获得表示该资源的实际的 URLFile 对象(如果隐含的实现支持该方法并保持一致的话)。

Spring自身处理资源请求的多种方法声明中将Resource 抽象作为参数而广泛地使用。 Spring APIs中的一些其它方法(比如许多ApplicationContext的实现构造函数),使用普通格式的 String 来创建与context相符的Resource,也可以使用特殊的路径String前缀来让调用者指定创建和使用特定的 Resource 实现。

Resource不仅被Spring自身大量地使用,它也非常适合在你自己的代码中独立作为辅助类使用。 用户代码甚至可以在不用关心Spring其它部分的情况下访问资源。这样的确会造成代码与Spring之间的耦合,但也仅仅是与很少量的辅助类耦合。这些类可以作为比 URL 更有效的替代,而且与为这个目的而使用其它类库基本相似。

需要注意的是 Resource 抽象并没有改变功能:它尽量使用封装。 比如 UrlResource 封装了URL,然后使用被封装的 URL 来工作。

4.3. 内置 Resource 实现

Spring提供了很多 Resource 的实现:

4.3.1.  UrlResource

UrlResource 封装了java.net.URL,它能够被用来访问任何通过URL可以获得的对象,例如:文件、HTTP对象、FTP对象等。所有的URL都有个标准的 String表示,这些标准前缀可以标识不同的URL类型,包括file:访问文件系统路径,http: 通过HTTP协议访问的资源,ftp: 通过FTP访问的资源等等。

UrlResource 对象可以在Java代码中显式地使用 UrlResource 构造函数来创建。但更多的是通过调用带表示路径的 String 参数的API函数隐式地创建。在后一种情况下,JavaBeans的 PropertyEditor 会最终决定哪种类型的 Resource 被创建。如果这个字符串包含一些众所周知的前缀,比如 classpath:,它就会创建一个对应的已串行化的 Resource。 然而,如果不能分辨出这个前缀,就会假定它是个标准的URL字符串,然后创建UrlResource

4.3.2.  ClassPathResource

这个类标识从classpath获得的资源。它会使用线程context的类加载器(class loader)、给定的类加载器或者用来载入资源的给定类。

如果类路径上的资源存在于文件系统里,这个 Resource 的实现会提供类似于java.io.File的功能。而如果资源是存在于还未解开(被servlet引擎或其它的环境解开)的jar包中,这些 Resource 实现会提供类似于java.net.URL 的功能。

ClassPathResource对象可以在Java代码中显式地使用ClassPathResource 构造函数来创建。但更多的是通过调用带表示路径的String参数的API函数隐式地创建。在后一种情况下,JavaBeans的 PropertyEditor 会分辨字符串中 classpath: 前缀,然后相应创建 ClassPathResource

4.3.3.  FileSystemResource

这是为处理 java.io.File 而准备的Resource实现。它既可以作为File提供,也可以作为URL

4.3.4.  ServletContextResource

这是为 ServletContext 资源提供的 Resource 实现,它负责解析相关web应用根目录中的相对路径。

它始终支持以流和URL的方式访问。 但是只有当web应用包被解开并且资源在文件系统的物理路径上时,才允许以 java.io.File 方式访问。是否解开并且在文件系统中访问,还是直接从JAR包访问或以其它方式访问如DB(这是可以想象的),仅取决于Servlet容器。

4.3.5.  InputStreamResource

这是为给定的 InputStream 而准备的 Resource 实现。它只有在没有其它合适的 Resource 实现时才使用。而且,只要有可能就尽量使用 ByteArrayResource 或者其它基于文件的Resource 实现。

与其它 Resource 实现不同的是,这是个 已经 打开资源的描述符-因此 isOpen() 函数返回 true。 如果你需要在其它位置保持这个资源的描述符或者多次读取一个流,请不要使用它。

4.3.6.  ByteArrayResource

这是为给定的byte数组准备的 Resource 实现。 它会为给定的byte数组构造一个 ByteArrayInputStream

它在从任何给定的byte数组读取内容时很有用,因为不用转换成单一作用的 InputStreamResource

4.4. ResourceLoader

ResourceLoader 接口由能返回(或者载入)Resource 实例的对象来实现。

public interface ResourceLoader {
    Resource getResource(String location);
}

所有的application context都实现了 ResourceLoader 接口, 因此它们可以用来获取Resource 实例。

当你调用特定application context的 getResource() 方法, 而且资源路径并没有特定的前缀时,你将获得与该application context相应的 Resource 类型。例如:假定下面的代码片断是基于ClassPathXmlApplicationContext 实例上执行的:

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

这将返回ClassPathResource;如果是基于FileSystemXmlApplicationContext 实例上执行的,那你将获得FileSystemResource。而对于 WebApplicationContext 你将获得ServletContextResource,依此类推。

这样你可以在特定的application context中用流行的方法载入资源。

另一方面,无论什么类型的application context, 你可以通过使用特定的前缀 classpath: 强制使用ClassPathResource

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

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

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

下面的表格概述了 StringResource 的转换规则:

Table 4.1. Resource strings

前缀例子说明

classpath:

classpath:com/myapp/config.xml

从classpath中加载。

file:

file:/data/config.xml

作为 URL 从文件系统中加载。[a]

http:

http://myserver/logo.png

作为 URL 加载。

(none)

/data/config.xml

根据 ApplicationContext 进行判断。

4.5.  ResourceLoaderAware 接口

ResourceLoaderAware是特殊的标记接口,它希望拥有一个ResourceLoader 引用的对象。

public interface ResourceLoaderAware {
   void setResourceLoader(ResourceLoader resourceLoader);
}

当实现了 ResourceLoaderAware接口的类部署到application context(比如受Spring管理的bean)中时,它会被application context识别为 ResourceLoaderAware。 接着application context会调用setResourceLoader(ResourceLoader)方法,并把自身作为参数传入该方法(记住,所有Spring里的application context都实现了ResourceLoader接口)。

既然 ApplicationContext 就是ResourceLoader,那么该bean就可以实现 ApplicationContextAware接口并直接使用所提供的application context来载入资源,但是通常更适合使用特定的满足所有需要的 ResourceLoader实现。 这样一来,代码只需要依赖于可以看作辅助接口的资源载入接口,而不用依赖于整个Spring ApplicationContext 接口。

4.6. 把Resource作为属性来配置

如果bean自身希望通过一些动态方式决定和提供资源路径,那么让这个bean通过 ResourceLoader 接口去载入资源就很有意义了。考虑一个载入某类模板的例子,其中需要哪种特殊类型由用户的角色决定。 如果同时资源是静态的,完全不使用 ResourceLoader 接口很有意义, 这样只需让这些bean暴露所需的 Resource 属性,并保证他们会被注入。

让注入这些属性的工作变得如此容易的原因是,所有的application context注册并使用了能把 String 路径变为 Resource 对象的特殊 PropertyEditor JavaBeans。因此如果 myBeanResource 类型的template属性, 那它就能够使用简单的字符串配置该资源,如下所示:

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

可以看到资源路径没有前缀,因为application context本身要被作为 ResourceLoader 使用,这个资源会被载入为ClassPathResourceFileSystemResourceServletContextResource等等,这取决于context类型。

如果有必要强制使用特殊的 Resource 类型,那你就可以使用前缀。下面的两个例子说明了如何强制使用 ClassPathResourceUrlResource (其中的第二个被用来访问文件系统中的文件)。

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

4.7. Application context 和Resource 路径

4.7.1. 构造application context

application context构造器通常使用字符串或字符串数组作为资源(比如组成context定义 的XML文件)的定位路径。

当这样的定位路径没有前缀时,指定的 Resource 类型会通过这个路径来被创建并被用来载入bean的定义,这都取决于你所指定的application context。例如,如果你使用下面的代码来创建ClassPathXmlApplicationContext

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

这些Bean的定义会通过classpath载入并使用ClassPathResource。而如果你象下面这么创建FileSystemXmlApplicationContext

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

这些Bean的定义会通过文件系统从相对于当前工作目录中被载入。

请注意如果定位路径使用classpath前缀或标准的URL前缀,那它就会覆盖默认的Resource 类型。因此下面的FileSystemXmlApplicationContext...

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

...实际上会通过classpath载入其bean定义。然而它仍是个FileSystemXmlApplicationContext。 如果后面它被当作ResourceLoader 来使用,那么任何没有使用前缀的路径依然会被当作一个文件系统路径。

4.7.1.1. 创建 ClassPathXmlApplicationContext 实例 - 简介

ClassPathXmlApplicationContext 提供了多种构造方法以便于初始化。但其核心是,如果我们仅仅提供由XML文件名组成的字符串数组(没有完整路径信息), 而且提供了Class;那么该ClassPathXmlApplicationContext就 会从给定的类中抽取路径信息。

希望通过一个示例把这些阐述清楚。假设有这样的目录结构:

 com/
  foo/
	services.xml
	daos.xml
	MessengerService.class

'services.xml''daos.xml' 中定义的bean组成的 ClassPathXmlApplicationContext 实例会象这样地来实例化...

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

欲了解 ClassPathXmlApplicationContext 多种构造方法的细节,请参考它的Javadocs。

4.7.2. Application context构造器中资源路径的通配符

Application context构造器中资源路径的值可以是简单的路径(就像上面的那样),即一对一映射到一个目标资源; 或者可以包含特殊的"classpath*:"前缀和Ant风格的正则表达式(用Spring的 PathMatcher 工具来匹配)。 后面的二者都可以使用通配符。

该机制的一个用处就是做组件类型的应用组装。所有的组件都可以用通用的定位路径“发布”context定义片断, 这样当使用相同的 classpath*: 前缀创建最终的application context时,所有的组件片断都会被自动装入。

请注意,这个通配符只在application context构造器的资源路径中 (或直接在类的层次中使用 PathMatcher 工具时)有效, 它会在构造时进行解析。这与 Resource 类型本身没有关联。因为同一时刻只能指向一个资源,所以不能使用 classpath*: 前缀来构造实际的Resource

4.7.2.1. Ant风格的pattern

在包含Ant风格的pattern时,例如:

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

解析器会进行一个预先定义的复杂的过程去试图解析通配符。 它根据路径中最后一个非通配符片断产生一个Resource并从中获得一个URL。 如果这个URL不是一个"jar:" URL或特定容器的变量(例如WebLogic中的 "zip:",WebSphere中的"wsjar"等等), 那么可以从中获得一个java.io.File, 并用它从文件系统中解析通配符。如果是一个jar URL,解析器可以从中取得一个 java.net.JarURLConnection,或者手工解析该jar URL, 随后遍历jar文件以解析通配符。

4.7.2.1.1. 潜在的可移植性

如果给定的路径已经是一个文件URL(可以是显式的或者是隐式的), 由于基本的ResourceLoader是针对文件系统的,那么通配符一定能够移植。

如果给定的路径是一个classpath的位置,那么解析器必须通过一个 Classloader.getResource() 调用获得最后一个 非通配符路径片断的URL。因为这仅仅是一个路径的节点(不是最终的文件), 所以它并未确切定义(在 ClassLoader Javadocs里) 此处究竟会返回什么类型的URL。一般情况下,当classpath资源解析为一个文件系统位置时, 返回一个代表目录的 java.io.File;当解析为jar位置时, 返回某类jar URL。当然,这个操作涉及到可移植性。

如果从最后一个非通配符片断中获得一个jar URL,那么解析器一定能从中取得一个 java.net.JarURLConnection,或者手动解析jar URL以遍历jar文件, 从而解析通配符。这一操作在大多数环境中能正常工作,不过也有例外, 因此我们强烈建议特定环境中的jar资源通配符解析应在正式使用前要经过彻底测试。

4.7.2.2. classpath*: 前缀

当构造基于XML的application context时,路径字符串可能使用特殊的 classpath*: 前缀:

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

此前缀表示所有与给定名称匹配的classpath资源都应该被获取(其中,这经常会在调用 ClassLoader.getResources(...)) 时发生),并接着将那些资源全并成最终的application context定义。

[Note]Classpath*: 的可移植性

带通配符的classpath依赖于底层classloader的 getResources() 方法。 现在大多数的应用服务器提供自己的classloader实现,它们在处理jar文件时的行为也许会有所不同。 要测试 classpath*: 是否有效,可以简单地用classloader从classpath中的jar文件里加载一个文件: getClass().getClassLoader().getResources("<someFileInsideTheJar>")。 针对两个不同位置但有相同名字的文件来运行测试。如果结果不对,那么就查看一下应用服务器的文档, 特别是那些可能影响classloader行为的设置。

"classpath*:"前缀也能在位置路径的其他部分结合PathMatcher pattern一起使用,例如"classpath*:META-INF/*-beans.xml"。在这里, 解析策略很简单:对最后一个非通配符路径片断使用一个ClassLoader.getResources()调用, 从类加载层次中获得所有满足的资源,随后针对子路径的通配符, 将同一个PathMatcher解析策略运用于每个资源之上。

4.7.2.3. 其他关于通配符的说明

请注意如果目标文件不是在文件系统中,那么"classpath*:" 在结合Ant风格的pattern时必须在pattern开始前包含至少一个根路径才能保证其正确性。 像"classpath*:*.xml"这样的pattern不能从jar文件的根目录取得文件, 而只能从这个根目录的子目录中获得文件。这个问题源自JDK中 ClassLoader.getResources() 方法的一个局限性,即该方法在传入空String(指示要搜索的潜在根目录)时只返回文件系统位置。

如果搜索的根包在多个类路径位置上,带"classpath:"的Ant风格pattern 资源不能保证一定可以找到匹配的资源。这是因为像

    com/mycompany/package1/service-context.xml

这样的资源只可能在一个位置,但如果要解析的是如下路径

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

解析器会排除getResource("com/mycompany");返回的(第一个)URL。 如果这个基础包节点存在于多个classloader位置,最终要找的资源未必会被发现。 因此在这种情况中最好在这个Ant风格的pattern中使用"classpath*:", 这样就会搜索包含根包在内所有类路径。

4.7.3.  FileSystemResource 提示

一个并没有与 FileSystemApplicationContext 绑定的 FileSystemResource(也就是说FileSystemApplicationContext 并不是真正的ResourceLoader),会象你期望的那样分辨绝对和相对路径。 相对路径是相对于当前的工作目录,而绝对路径是相对与文件系统的根目录。

为了向前兼容的目的,当 FileSystemApplicationContext 是个 ResourceLoader 时它会发生变化。FileSystemApplicationContext 会简单地让所有绑定的 FileSystemResource 实例把绝对路径都当成相对路径, 而不管它们是否以反斜杠开头。也就是说,下面的含义是相同的:

ApplicationContext ctx =
    new FileSystemClassPathXmlApplicationContext("conf/context.xml");
ApplicationContext ctx =
    new FileSystemClassPathXmlApplicationContext("/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 it's definition via a UrlResource
ApplicationContext ctx =
    new FileSystemXmlApplicationContext("file:/conf/context.xml");