Chapter 4. 资源

4.1. 简介

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

4.2. Resource接口

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

public interface 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 进行判断。

[a] 参考标题为 Section 4.7.3, “FileSystemResource 说明” 的章节。

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 接口。

从Spring 2.5开始, 你可以使用ResourceLoader 的自动装配来代替实现 ResourceLoaderAware 接口。“传统的” constructorbyType的自动装配模式 (Section 3.3.5, “自动装配(autowire)协作者”已有论述)现在可以分别为构造方法参数及setter方法参数提供 ResourceLoader 类型的依赖。请使用新式的基于注解的自动装配特性以提供更大的灵活性(包括装配属性及多个方法参数的能力)。在这种情况下,只要属性、构造方法或者方法被 @Autowired注解修饰,ResourceLoader 就会被装配到需要ResourceLoader类型的属性、构造方法参数或者方法参数中。请查看章节 Section 3.11.1, “@Autowired”以了解进一步的信息。

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