第 5 章 Resource Loading服务指南

5.1. 资源概述
5.1.1. 什么是资源?
5.1.2. 如何表示资源?
5.1.3. 如何访问资源?
5.1.4. 如何遍历资源?
5.1.5. 有什么问题?
5.2. Spring的ResourceLoader机制
5.2.1. Resource接口
5.2.2. ResourceLoaderResourcePatternResolver接口
5.2.3. 在代码中取得资源
5.2.4. Spring如何装载资源?
5.2.5. Spring ResourceLoader的缺点
5.3. Resource Loading服务
5.3.1. 替换Spring ResourceLoader
5.3.2. 定义新资源
5.3.3. 重命名资源
5.3.4. 重定向资源
5.3.5. 匹配资源
5.3.6. 在多个ResourceLoader中查找
5.3.7. 装载parent容器中的资源
5.3.8. 修改资源文件的内容
5.3.9. 直接使用ResourceLoadingService
5.3.10. 在非Web环境中使用Resource Loading服务
5.4. ResourceLoader参考
5.4.1. FileResourceLoader
5.4.2. WebappResourceLoader
5.4.3. ClasspathResourceLoader
5.4.4. SuperResourceLoader
5.4.5. 关于ResourceLoader的其它考虑
5.5. 本章总结

Webx框架中,包含了一套用来查找和装载资源的服务 —— Resource Loading服务。

Resource Loading服务从Spring ResourceLoader机制中扩展而来,并且和Spring框架融为一体。因此,你不需要写特别的Java代码,就可以让所有利用Spring ResourceLoader机制的代码,直接享用Webx所提供的新的Resource Loading机制。

5.1. 资源概述

5.1.1. 什么是资源?

在一个稍具规模的应用程序中,经常要做的一件事,就是查找资源、读取资源的内容。这里所谓的“资源”,是指存放在某一介质中,可以被程序利用的文件、数据。例如,基于Java的WEB应用中,常用到下面的资源:

  • 配置文件:*.xml*.properties等。

  • Java类文件:*.class

  • JSP页面、Velocity模板文件:*.jsp*.vm等。

  • 图片、CSS、JavaScript文件:*.jpg*.css*.js等。

5.1.2. 如何表示资源?

在Java中,有多种形式可以表示一个资源:

表 5.1. 资源的表示

可表示资源的对象说明
java.io.​File

可代表文件系统中的文件或目录。例如:

  • 文件系统中的文件:“c:\config.sys”。

  • 文件系统中的目录:“c:\windows\”。

java.net.​URL

统一资源定位符。例如:

  • 文件系统中的文件:c:\config.sys,可以表示成URL:“file:///c:/config.sys”。

  • 文件系统中的目录:c:\windows\,可以表示成URL:“file:///c:/windows/”。

  • 远程WEB服务器上的文件:“http://www.springframework.org/schema/beans.xml”。

  • Jar包中的某个文件,可以表示成URL:“jar:file:///c:/my.jar!/my/file.txt”。

java.io.​InputStream

输入流对象,可用来直接访问资源的内容。例如:

  • 文件系统中的文件:c:\config.sys,可以用下面的代码来转换成输入流:

    new FileInputStream("c:\\config.sys");
  • 远程WEB服务器上的文件,可以用下面的代码来转换成输入流:

    new URL("http://www.springframework.org/schema/beans.xml")​.openStream();
  • Jar包中的某个文件,可以用下面的代码来转换成输入流:

    new URL("jar:file:///c:/my.jar!/my/file.txt")​.openStream();

然而,并不是所有的资源,都可以表现成上述所有的形式。比如,

  • Windows文件系统中的目录,无法表现为输入流。

  • 而远程WEB服务器上的文件无法转换成File对象。

  • 多数资源都可以表现成URL形式。但也有例外,例如,如果把数据库中的数据看作资源,那么一般来说这种资源无法表示成URL

5.1.3. 如何访问资源?

不同类型的资源,需要用不同的方法来访问。

访问CLASSPATH中的资源

将资源放在CLASSPATH是最简单的做法。我们只要把所需的资源文件打包到Jar文件中,或是在运行java时,用-classpath参数中指定的路径中。接下来我们就可以用下面的代码来访问这些资源:

例 5.1. 访问CLASSPATH中的资源

URL resourceURL = getClassLoader().getResource("java/lang/String.class"); // 取得URL
InputStream resourceContent = getClassLoader().getResourceAsStream("java/lang/String.class"); // 取得输入流
访问文件系统中的资源

下面的代码从文件资源中读取信息:

例 5.2. 访问文件系统中的资源

File resourceFile = new File("c:\\test.txt"); // 取得File
InputStream resourceContent = new FileInputStream(resourceFile); // 取得输入流
访问Web应用中的资源

Web应用既可以打包成war文件,也可以展开到任意目录中。因此Web应用中的资源(JSP、模板、图片、Java类、配置文件)不总是可以用文件的方式存取。虽然Servlet API提供了ServletContext.getRealPath()方法,用来取得某个资源的实际文件路径,但该方法很可能返回null —— 这取决于应用服务器的实现,以及Web应用的部署方式。更好的获取WEB应用资源的方法如下:

例 5.3. 访问Web应用中的资源

URL resourceURL = servletContext.getResource("/WEB-INF/web.xml"); // 取得URL
InputStream resourceContent = servletContext.getResourceAsStream("/WEB-INF/web.xml"); // 取得输入流
访问Jar/Zip文件中的资源

下面的代码读取被打包在Jar文件中的资源信息:

例 5.4. 访问Jar/Zip文件中的资源

URL jarURL = new File(System.getProperty("java.home") + "/lib/rt.jar").toURI().toURL();
URL resourceURL = new URL("jar:" + jarURL + "!/java/lang/String.class"); // 取得URL
InputStream resourceContent = resourceURL.openStream(); // 取得输入流
访问其它资源

还可以想到一些访问资源的方法,例如从数据库中取得资源数据。

5.1.4. 如何遍历资源?

有时候,我们不知道资源的路径,但希望能找出所有符合条件的资源,这个操作叫作遍历。例如,找出所有符合pattern “/WEB-INF/webx-*.xml”的配置文件。

遍历文件系统

例 5.5. 遍历文件系统

File parentResource = new File("c:\\windows");
File[] subResources = parentResource.listFiles();
遍历WEB应用中的资源

例 5.6. 遍历WEB应用中的资源

Set<String> subResources = servletContext.getResourcePaths("/WEB-INF/");
遍历Jar/zip文件中的资源

例 5.7. 遍历Jar/zip文件中的资源

File jar = new File("myfile.jar");
ZipInputStream zis = new ZipInputStream(new FileInputStream(jar));

try {
    for (ZipEntry entry = zis.getNextEntry(); entry != null; entry = zis.getNextEntry()) {
        // visit entry
    }
} finally {
    zis.close();
}

并非所有类型的资源都支持遍历操作。通常遍历操作会涉及比较复杂的递归算法。

5.1.5. 有什么问题?

应用程序访问资源时,有什么问题呢?

首先,资源表现形式的多样性,给应用程序的接口设计带来一点麻烦。假如,我写一个ConfigReader类,用来读各种配置文件。那么我可能需要在接口中列出所有的资源的形式:

例 5.8. 用来读取配置文件的接口

public interface ConfigReader {
    Object readConfig(File configFile);
    Object readConfig(URL configURL);
    Object readConfig(InputStream configStream);
}

特别是当一个通用的框架,如Spring和Webx,需要在对象之间传递各种形式的资源的时候,这种多样性将导致很大的编程困难。

其次,有这么多种查找资源和遍历资源的方法,使我们的应用程序和资源所在的环境高度耦合。这种耦合会妨碍代码的可移植性和可测试性。

比如,我希望在非WEB的环境下测试一个模块,但这个模块因为要存取Web应用下的资源,而引用了ServletContext对象。在测试环境中并不存在ServletContext而导致该模块难以被测试。再比如,我希望测试的一个模块,引用了classpath下的某个配置文件(这也是一种耦合)。而我希望用另一个专为该测试打造的配置文件来代替这个文件。由于原配置文件是在classpath中,因此是难以替换的。

对于不打算重用的应用程序来说,这个问题还不太严重:大不了我预先设定好,就从这个地方,以固定的方式存取资源。然而就算这样,也是挺麻烦的。有的人喜欢把资源放在某个子目录下,有的人喜欢把资源放在CLASSPATH下,又有人总是通过ServletContext来存取Web应用下的资源。当你要把这些不同人写的模块整合起来时,你会发现很难管理。

一种可能发生的情形是,因为某些原因,环境发生改变,导致资源的位置、存取方式不得不跟着改变。比如将老系统升级为新系统。但一些不得不继续使用的老代码,由于引用了旧环境的资源而不能工作 —— 除非你去修改这些代码。有时修改老代码是很危险的,可能导致不可预知的错误。又比如,由于存储硬件的改变或管理的需要,我们需要将部分资源移到另一个地方(我们曾经将Web页面模板中的某个子目录,移动到一个新的地方,因为这些模板必须由新的CMS系统自动生成)。想要不影响现有代码来完成这些事,是很困难的。

5.2. Spring的ResourceLoader机制

Spring内置了一套ResourceLoader机制,很好地解决了访问资源的大部分问题。

5.2.1. Resource接口

Spring将所有形式的资源表现概括成一个Resource接口。如下所示(下面的接口定义是被简化的,有意省略了一些东西,以便突出重点):

例 5.9. Spring的Resource接口(简化)

public interface Resource {
    InputStream getInputStream();
    URL getURL();
    File getFile();
    boolean exists();
}

Resource接口向应用程序屏蔽了资源表现形式的多样性。于是,前面例子中的ConfigReader就可以被简化成下面的样子:

例 5.10. 用来读取配置文件的接口(简化后)

public interface ConfigReader {
    Object readConfig(Resource configResource);
}

事实上,Spring正是利用Resource接口来初始化它的ApplicationContext的:

例 5.11. Spring用Resource接口来代表用来初始化ApplicationContext的配置文件

public abstract class AbstractXmlApplicationContext extends ... {
    ...
    protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) {
        Resource[] configResources = getConfigResources();
        ...
    }

    protected Resource[] getConfigResources();
}

5.2.2. ResourceLoaderResourcePatternResolver接口

Spring不仅可以通过ResourceLoader接口来取得单一的资源对象,还可以通过ResourcePatternResolver遍历并取得多个符合指定pattern的资源对象。这个设计向应用程序屏蔽了查找和遍历资源的复杂性。

ResourceLoader和ResourcePatternResolver接口

图 5.1. ResourceLoaderResourcePatternResolver接口

5.2.3. 在代码中取得资源

5.2.3.1. 通过ResourceLoader取得资源

例 5.12. 通过ResourceLoader取得资源

public class MyBean implements ResourceLoaderAware  {
    private ResourceLoader loader;

    public void setResourceLoader(ResourceLoader loader)  {
        this.loader = loader;
    }

    public void func() {
        Resource resource = loader.getResource("myFile.xml"); 
        ...
    }
}

实现了ResourceLoaderAware接口。要取得资源,必须要拿到ResourceLoader对象。而通过ResourceLoaderAware接口拿到ResourceLoader是最简单的方法。

调用所取得的ResourceLoader来取得资源。

5.2.3.2. 直接注入资源

另一种更简便的方法是,将资源直接“注入”到bean中 —— 你不需要手工调用ResourceLoader来取得资源的方式来设置资源。例如:

例 5.13. 直接注入资源

public class MyBean {
    private URL resource;

    public void setLocation(URL resource)  {
        this.resource = resource;
    }

    ……
}

Spring配置文件可以这样写:

<bean id="myBean" class="MyBean">
    <property name="location" value="myFile.xml" />
</bean>

此处注入资源的URL

这样,Spring就会把适当的myFile.xml所对应的资源注入到myBean对象中。此外,Spring会自动把Resource对象转换成URLFile等普通对象。在上面的例子中,MyBean并不依赖于Resource接口,只依赖于URL类。

将代码稍作修改,就可以注入一组资源:

例 5.14. 注入一组资源

    public void setLocations(URL[] resources)  {
        this.resources = resources;
    }

配置文件:

    <property name="locations" value="WEB-INF/webx-*.xml" />

此处注入资源的URL的数组。

上例中,可以直接得到所有符合pattern “WEB-INF/webx-*.xml”的配置文件。显然这是通过ResourcePatternResolver取得的。

5.2.4. Spring如何装载资源?

Spring是如何装载资源文件的呢?Spring装载资源的方案是由ApplicationContext决定的。不同的ApplicationContext类,实现了不同的资源装载方案。

Spring ApplicationContext实现了资源装载的具体方案

图 5.2. Spring ApplicationContext实现了资源装载的具体方案

5.2.4.1. ClassPathXmlApplicationContext

ClassPathXmlApplicationContext支持从classpath中装载资源。

例 5.15. ClassPathXmlApplicationContext - 从classpath中装载资源

假如我以下面的方式启动Spring,那么系统将支持从classpath中装载资源。

ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");

ClassPathXmlApplicationContext装载资源文件myFile.xml的逻辑,相当于如下代码:

URL resource = getClassLoader().getResource("myFile.xml");

5.2.4.2. FileSystemXmlApplicationContext

FileSystemXmlApplicationContext支持从文件系统中装载资源。

例 5.16. FileSystemXmlApplicationContext - 从文件系统中装载资源

假如我以下面的方式启动Spring,那么系统将支持从文件系统中装载资源。

ApplicationContext context = new FileSystemXmlApplicationContext("beans.xml");

FileSystemXmlApplicationContext装载资源文件myFile.xml的逻辑,相当于如下代码:

File resource = new File("myFile.xml");

5.2.4.3. XmlWebApplicationContext

XmlWebApplicationContext支持从webapp上下文中(也就是ServletContext对象中)装载资源。

例 5.17. XmlWebApplicationContext - 从Web应用的根目录中装载资源

假如我以下面的方式启动Spring,那么系统将支持从Web应用的根目录中装载资源。

ApplicationContext context = new XmlWebApplicationContext();

context.setConfigLocation("/WEB-INF/beans.xml");
context.setServletContext(servletContext);
context.refresh();

也可以让ContextLoaderListener来创建XmlWebApplicationContext,只需要在/WEB-INF/web.xml中添加如下配置:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/beans.xml</param-value>
</context-param>
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

XmlWebApplicationContext装载资源文件myFile.xml的逻辑,相当于如下代码:

URL resource = servletContext.getResource("myFile.xml");

5.2.4.4. ClasspathClasspath*前缀

除了用ClassPathXmlApplicationContext以外,事实上所有的Spring ApplicationContext实现也都支持装载classpath中的资源。可以用下面两种方法:

表 5.2. Spring ApplicationContext装载classpath资源的方法

方法说明
使用classpath:前缀例如:“classpath:myFile.xml” —— 在classpath中装载资源myFile.xml
使用classpath*:前缀例如:“classpath*:/META-INF/my*.xml” —— 在classpath中装载所有符合pattern的资源。

5.2.5. Spring ResourceLoader的缺点

鱼和熊掌不可得兼

Spring ResourceLoader是由ApplicationContext来实现的。而你一次只能选择一种ApplicationContext的实现 —— 如果你选择了XmlWebApplicationContext,你就放弃了FileSystemXmlApplicationContext;反之亦然。

在WEB应用中,由于Spring使用了XmlWebApplicationContext,因此你就无法装载文件系统下的资源。

不透明性

你必须用“绝对路径”来引用Spring中的资源。

假如你使用FileSystemXmlApplicationContext来访问资源,你必须使用绝对路径来访问文件或目录资源。这妨碍了应用程序在不同系统中部署的自由。因为在不同的系统中,例如Windows和Linux,文件的绝对路径是不同的。为了系统管理的需要,有时也需要将文件或目录放在不同于开发环境的地方。

即便是访问WEB应用下的资源,或者是classpath下的资源,你也必须明确指出它们的位置,例如:WEB-INF/myFile.xmlclasspath:myFile.xml等。如果我希望把classpath:myFile.xml挪到另一个物理位置,就必须修改所有的引用。

无扩展性

我无法在Spring ResourceLoader机制中增加一种新的装载资源的方法。例如,我希望把资源文件保存在数据库中,并用ResourceLoader来取得它。用Spring很难做到这点。

5.3. Resource Loading服务

5.3.1. 替换Spring ResourceLoader

Webx Resource Loading服务可作为Spring ResourceLoader机制的替代品(Drop-in Replacement,投入既替换):

  • 当你不使用它时,Spring原有的ResourceLoader功能不受影响;

  • 当你在spring配置文件中添加Resource Loading服务时,ResourceLoader即被切换到新的机制。新的机制可兼容原有的Spring配置和代码,但支持更多的资源装载方式,以及更多的功能,如资源重命名、资源重定向等。

你只需要在配置文件中增加以下内容,就可以将Spring ResourceLoader机制替换成Webx的Resource Loading服务:

例 5.18. Resource Loading服务的基本配置(/WEB-INF/webx.xml

<resource-loading
        xmlns="http://www.alibaba.com/schema/services"
        xmlns:res-loaders="http://www.alibaba.com/schema/services/resource-loading/loaders">

    <resource-alias pattern="/" name="/webroot" /> 

    <resource pattern="/webroot" internal="true"> 
        <res-loaders:webapp-loader /> 
    </resource>
    <resource pattern="/classpath" internal="true"> 
        <res-loaders:classpath-loader /> 
    </resource>

</ resource-loading>

关于这段配置的具体含义,请参见本章其它小节:

请参见:第 5.3.3 节 “重命名资源”

请参见:第 5.3.2 节 “定义新资源”

请参见:第 5.4.2 节 “WebappResourceLoader

请参见:第 5.4.3 节 “ClasspathResourceLoader

这段配置使得Resource Loading服务的行为和原来的Spring ResourceLoader完全兼容:

  • 仍然支持classpath:classpath*:前缀所定义的资源。

  • 如不加前缀,则代表访问WEB应用根目录下的文件。例如:

    • /myFile.xml代表着Web应用根目录下的/myFile.xml

    • /WEB-INF/myFile.xml代表着Web应用根目录下的/WEB-INF/myFile.xml

加上这段配置以后,虽然功能和原来相比并没有变化,然而它已经准备好向系统中添加新的资源装载的功能了。

5.3.2. 定义新资源

定义一种新资源,需要回答两个问题:

  1. 资源的名称是什么?

  2. 资源在哪里(或如何装载资源)?

下面的例子定义了一种新的资源,它的名称是“/jdk/*”,通过“file-loader”从文件系统${java.home}文件夹中装载。

例 5.19. 定义新资源:/jdk/*

<resource-loading
        xmlns="http://www.alibaba.com/schema/services"
        xmlns:res-loaders="http://www.alibaba.com/schema/services/resource-loading/loaders">
    ...

    <resource pattern="/jdk"> 
        <res-loaders:file-loader basedir="${java.home}" /> 
    </resource>

</resource-loading>

定义新资源,资源名以/jdk为前缀。

<file-loader>表示从文件系统中装载资源。详见:第 5.4.1 节 “FileResourceLoader

${java.home}是Java提供的system property,它的值指向当前Java运行环境的根目录。

前文讲过,Spring可以直接把资源注入到对象中。使用Resource Loading服务以后,你仍然可以这样做。下面的配置把JDK目录下的tools.jar文件(如果存在的话)的URL注入到myBean中:

例 5.20. 注入JAVA_HOME/lib/tools.jar

<bean id="myBean" class="MyBean">
    <property name="location" value="/jdk/lib/tools.jar" />
</bean>

5.3.3. 重命名资源

重命名资源是指对于即有的资源,改变其名字。

为什么需要修改资源的名字?理由是:取消资源名称和环境的关联性。有一些资源的名称,具有明显的环境相关性,比如:

  • classpath:myFile.xml或者/classpath/myFile.xml —— 从资源的名称就可以看出,这些资源是从classpath中装载的。

  • /WEB-INF/myFile.xml或者/webroot/WEB-INF/myFile.xml —— 从资源的名称可以看出,这些资源是从web应用中装载的。

使用和环境相关的资源名称有什么问题?问题就是,当环境改变时,应用代码会受到影响。最常见的一种状况是:单元测试时,用于测试的资源文件往往被放在专供测试的目录中,这些目录和应用运行时的环境是不同的 —— 你可能希望将classpath:myFile.xml/WEB-INF/myFile.xml改成/src/test/config/myFile.xml

对资源重命名就可以解决这类问题:

  • classpath:myFile.xml或者/WEB-INF/myFile.xml重命名成:myapp/conf/myFile.xml

  • 在测试环境中,将myapp/conf/myFile.xml名称指向另一个物理地址src/test/config/myFile.xml

重命名资源是通过alias别名实现的:

例 5.21. 重命名资源

<resource-loading
        xmlns="http://www.alibaba.com/schema/services"
        xmlns:res-loaders="http://www.alibaba.com/schema/services/resource-loading/loaders">
    ...

    <resource-alias pattern="/myapp/conf" name="/webroot/WEB-INF" /> 

    <resource pattern="/webroot" internal="true"> 
        <res-loaders:webapp-loader /> 
    </resource>

</resource-loading>

定义了一个资源的别名:/myapp/conf

当你查找/myapp/conf/myFile.xml时,Resource Loading服务实际上会去找/webroot/WEB-INF/myFile.xml。而/webroot/*则是由 所定义的资源。

定义以/webroot为前缀的新资源。

其中,attribute internal=true是一个可选项,当它的值为true时,代表它所修饰的资源是不能被外界所直接访问的。例如,你想直接在myBean中注入/webroot/WEB-INF/myFile.xml是不行的。把internal选项设成true,可以让强制用户转向新的资源名称。Internal参数的默认值为false,意味着,新旧两种名称同时可用。

<webapp-loader>表示从Web应用中装载资源。详见:第 5.4.2 节 “WebappResourceLoader

5.3.4. 重定向资源

重定向资源的意思是,将部分资源名称,指向另外的地址。

一个常见的需求是这样的:通常我们会把页面模板保存在WEB应用的/templates目录下。但是有一批模板是由外部的CMS系统生成的,这些模板文件不可能和WEB应用打包在一起,而是存放在某个外部的目录下的。我们希望用/templates/cms来引用这些模板。

由于/templates/cms只不过是/templates的子目录,所以如果没有Resource Loading服务所提供的重定向功能,是不可能实现上述功能的。用Resource Loading服务重定向的配置如下:

例 5.22. 重定向资源

<resource-loading
        xmlns="http://www.alibaba.com/schema/services"
        xmlns:res-loaders="http://www.alibaba.com/schema/services/resource-loading/loaders">
    ...

    <resource-alias pattern="/templates" name="/webroot/templates" /> 

    <resource pattern="/templates/cms"> 
        <res-loaders:file-loader basedir="${cms_root}" />
    </resource>

    <resource pattern="/webroot" internal="true">
        <res-loaders:webapp-loader />
    </resource>

    ...
</resource-loading>

定义了一个资源的别名:/templates,指向internal资源:/webroot/templates

/templates的子目录/templates/cms重定向到某个外部的文件目录${cms_root}中。

其中cms_root是启动服务器时所指定的system property(-Dcms_root=...)或者spring所定义的placeholder。

通过上述配置,可以达到如下效果:

表 5.3. 访问/templates目录下的资源

资源名如何装载?
/templates/xxx.vm不受重定向影响。访问/webroot/templates/xxx.vm,继而通过webapp-loader访问Web应用根目录下的/templates/xxx.vm文件。
/templates/cms/yyy.vm被重定向。通过file-loader访问${cms_root}目录下的文件:${cms_root}/yyy.vm
/templates/subdir/zzz.vm不受重定向影响。访问/webroot/templates/subdir/zzz.vm,继而通过webapp-loader访问Web应用根目录下的/templates/subdir/zzz.vm文件。

最重要的是,访问/templates目录的应用程序并不知道这个资源重定向的存在,当cms所对应的实际目录被改变时,应用程序也不会受到任何影响 —— 这个正是Resource Loading服务的“魔法”。

5.3.5. 匹配资源

无论是定义新资源(<resource>)或是重命名资源(资源别名、<resource-alias>),都需要指定一个pattern attribute来匹配资源的名称。

5.3.5.1. 匹配绝对路径和相对路径

资源或资源别名的pattern支持对绝对路径和相对路径的匹配:

表 5.4. 资源或别名的pattern格式

pattern类型格式说明
匹配绝对路径

/absolute/path

/开头的pattern代表一个绝对路径的匹配。

例如:pattern="/absolute/path"可以匹配资源名/abslute/path/xxx/yyy,但不能匹配资源名/xxx/abslute/path/yyy

匹配相对路径

relative/path

不以/开头的pattern代表一个相对路径的匹配。

例如:pattern="relative/path"可以匹配资源名/relative/path/xxx/yyy,也可以匹配资源名/xxx/relative/path/yyy

5.3.5.2. 匹配通配符

表 5.5. 通配符格式

格式说明
星号 *匹配0-n个字符,但不包括“/”。即,“*”只匹配一级目录或文件中的零个或多个字符。
双星号 **匹配0-n个字符,包括“/”。即,“**”匹配多级目录或文件。
问号 ?匹配0-1个字符,但不包括“/”。即,“?”匹配一级目录或文件中的零个或一个字符。

所有被通配符匹配的内容,将被按顺序赋给变量“$1”、“$2”、“$3”、“$4”、……。这些变量可以在其它地方被引用。

通配符匹配的名称既可以是绝对路径,也可以是相对路径。把相对路径和通配符结合起来的最常见用法,就是匹配文件名后缀,例如:pattern="*.xml"

下面是一些使用通配符的例子:

例 5.23. 用通配符来匹配资源名称或资源别名

重命名WEB-INF及其子目录下的所有的xml文件

例如,将/myapp/conf/my/file.xml转换成/webroot/WEB-INF/my/file.xml

<resource-alias pattern="/myapp/conf/**/*.xml" name="/webroot/WEB-INF/$1/$2.xml" />
修改文件名后缀

例如,将/myapp/conf/myfile.conf转换成/webroot/WEB-INF/myfile.xml

<resource-alias pattern="/myapp/conf/*.conf" name="/WEB-INF/$1.xml"/>
按首字母划分子目录

a开头的文件名放到a子目录下,b开头的文件名放到b子目录下,以此类推。

例如,将/profiles/myname转换成文件路径${profile_root}/m/myname;将/profiles/othername转换成文件路径${profile_root}/o/othername

<resource pattern="/profiles/?*">
    <res-loaders:file-loader basedir="${profile_root}">
        <res-loaders:path>$1/$1$2</res-loaders:path>
    </res-loaders:file-loader>
</resource>

5.3.6. 在多个ResourceLoader中查找

假如,在我的Web应用中,我有一些配置文件放在/WEB-INF目录中,另外一部分配置放在classpath中。我可以这样做:

例 5.24. 在多个ResourceLoader中查找

<resource-loading
        xmlns="http://www.alibaba.com/schema/services"
        xmlns:res-loaders="http://www.alibaba.com/schema/services/resource-loading/loaders">
    ...

    <resource pattern="/myapp/conf">
        <res-loaders:super-loader name="/webroot/WEB-INF" /> 
        <res-loaders:super-loader name="/classpath" /> 
    </resource>

    <resource pattern="/webroot" internal="true"> 
        <res-loaders:webapp-loader />
    </resource>
    <resource pattern="/classpath" internal="true"> 
        <res-loaders:classpath-loader />
    </resource>

    ...
</resource-loading>

依次尝试两个loaders。

定义internal资源/webroot/*,从Web应用中装载资源。详见第 5.4.2 节 “WebappResourceLoader

定义internal资源/classpath/*,从classpath中装载资源。详见第 5.4.3 节 “ClasspathResourceLoader

Resource Loading服务根据上面的配置,会这样查找资源“/myapp/conf/myFile.xml”:

  1. 先查找:/webroot/WEB-INF/myFile.xml,如果找不到,

  2. 则再查找:/classpath/myFile.xml,如果找不到,则放弃。

在上例中,<super-loader>(详见第 5.4.4 节 “SuperResourceLoader)是一种特殊的ResourceLoader,它等同于<resource-alias>。下面的两种写法是完全等同的:

例 5.25. <super-loader>和等效的<resource-alias>

<resource pattern="/myapp/conf">
    <res-loaders:super-loader name="/webroot/WEB-INF" />
</resource>

<resource-alias pattern="/myapp/conf " name="/webroot/WEB-INF" />

但是用<resource-alias>没有办法实现上面所述的多重查找的功能。

5.3.7. 装载parent容器中的资源

在Webx中,Spring容器被安排成级联的结构。

Spring容器的级联结构

图 5.3. Spring容器的级联结构

如图所示,每个Spring容器都可以配置自己的Resource Loading服务。当调用子容器的Resource Loading服务时,遵循这样的逻辑:

  1. 先在子容器的Resource Loading服务中查找资源,如果找不到,

  2. 则再到parent容器的Resource Loading服务中查找,如果找不到,则放弃。

运用这种级联装载资源的方法,子应用可以把共享的资源定义在root context中,而把自己独享的资源定义在自己的容器当中。

前文所述的<super-loader>也支持级联装载资源。<super-loader>会先在当前容器的Resource Loading服务中查找,如果找不到,就到parent容器的Resource Loading服务中查找。利用<super-loader>,你甚至可以改变资源搜索的顺序。例如,你可以命令Resource Loading服务先查找parent容器中的Resource Loading服务,再查找当前容器中的ResourceLoaders:

例 5.26. 利用<super-loader>改变资源搜索的顺序

<resource pattern="...">
    <res-loaders:super-loader /> 
    <res-loaders:file-loader /> 
</resource>

先找parent容器中的Resource Loading服务。

再找当前容器中的ResourceLoaders。

5.3.8. 修改资源文件的内容

Resource Loading服务支持内容过滤 —— 你可以在获取资源以前读取甚至修改资源文件的内容。一种常见的情形是,将XML格式的资源文件用XSLT转换格式:

例 5.27. 将XML格式的资源文件用XSLT转换格式

<resource-loading
        xmlns="http://www.alibaba.com/schema/services"
        xmlns:res-loaders="http://www.alibaba.com/schema/services/resource-loading/loaders">
        xmlns:res-filters="http://www.alibaba.com/schema/services/resource-loading/filters">
    ...

    <resource-filters pattern="test-*.xml">
        <res-filters:xslt-filter xslt="/stylesheet.for.test/test.xsl" saveTo="/tempdir" /> 
    </resource-filters>

    <resource pattern="/tempdir"> 
        <loaders:file-loader basedir="${project.home}/target/test" />
    </resource>

</resource-loading>

所有目录下(因为是相对路径)的名称为test-*.xml文件,用指定的XSL文件进行转换。

这里引进了一种新的扩展点:ResourceFilterResourceFilter可以在应用获取资源之前,取得控制,以便对资源做一点事。

<xslt-filter>是对ResourceFilter的扩展,它能够把XML资源用指定的xsl文件转换成新的格式。假如指定了saveTo参数,就可以把转换的结果保存下来,避免每次访问都重新转换。

此处定义tempdir目录资源,以便保存xslt转换的结果。

[注意]注意

<xslt-filter>的参数xslt所指向的xsl文件,以及参数saveTo所指向的目录,它们本身也是由Resource Loading服务装载的。

有哪些情况需要这种内容过滤的功能呢?

  • 单元测试 —— 我们可能需要对单元测试的资源文件进行特殊的转换。

  • 高速缓存 —— 有一些ResourceLoader可能会有性能的开销,例如:从数据库中装载资源。利用ResourceFilter功能,就可以把装载的资源缓存在高速cache中,以提高系统的性能。

5.3.9. 直接使用ResourceLoadingService

前面所讲的Resource Loading服务的用法,对应用程序而言,是完全透明的。也就是说,应用程序并不需要关心Resource Loading服务的存在,而是按照Spring ResourceLoader的老用法,就可以工作。

但是你也可以直接注入ResourceLoadingService对象,以取得更多的功能。

例 5.28. 注入ResourceLoadingService对象

public class MyClass {
    @Autowired
    private ResourceLoadingService resourceLoadingService;
}

下面列举了可通过ResourceLoadingService接口实现的功能。

取得资源

例 5.29. 通过ResourceLoadingService接口取得资源

Resource resource = resourceLoadingService.getResource("/myapp/conf/myFile.xml"); 
Resource resource = resourceLoadingService.getResource("/myapp/conf/myFile.xml",
                                                       ResourceLoadingService.FOR_CREATE); 

和Spring不同的是,如果你直接调用ResourceLoadingService取得资源,当资源文件不存在时,你会得到一个ResourceNotFoundException。而Spring无论如何都会取得Resource对象,但随后你需要调用Resource.exists()方法来判断资源存在于否。

ResourceLoadingService.getResource()方法还支持一个选项:FOR_CREATE。如果提供了这个选项,那么对于某些类型的资源(如文件系统的资源),即使文件或目录不存在,仍然会返回结果。这样,你就可以创建这个文件或目录 —— 这就是FOR_CREATE参数的意思。

取得特定类型的资源

例 5.30. 通过ResourceLoadingService接口取得特定类型的资源

// 取得资源文件
File file = resourceLoadingService.getResourceAsFile("/myapp/conf/myFile.xml");

// 取得资源URL
URL url = resourceLoadingService.getResourceAsURL("/myapp/conf/myFile.xml");

// 取得资源输入流
InputStream stream = resourceLoadingService.getResourceAsStream("/myapp/conf/myFile.xml");
判断资源存在于否

例 5.31. 通过ResourceLoadingService接口判断资源存在于否

if (resourceLoadingService.exists("/myapp/conf/myFile.xml")) {
    ...
}
列举子资源

例 5.32. 通过ResourceLoadingService接口列举子资源

String[] resourceNames = resourceLoadingService.list("/myapp/conf");
Resource[] resources = resourceLoadingService.listResources("/myapp/conf");

相当于列出当前目录下的所有子目录和文件。

不是所有的ResourceLoader都支持这个操作 —— FileResourceLoaderWebappResourceLoader支持列举子资源,ClasspathResourceLoader则不支持。

跟踪取得资源的过程

例 5.33. 通过ResourceLoadingService接口跟踪取得资源的过程

ResourceTrace trace = resourceLoadingService.trace("/myapp/conf/webx.xml");

for (ResourceTraceElement element : trace) {
    System.out.println(element);
}

这是用来方便调试的功能。有点像Throwable.getStackTrace()方法,可以得到每一个方法调用的历史记录 —— ResourceLoadingService.trace()方法可以将取得资源的步骤记录下来。上面代码会在console中输出类似下面的内容:

"/myapp/conf/webx.xml" matched [resource-alias pattern="/myapp/conf"], at "resources.xml", beanName="resourceLoadingService"
"/webroot/WEB-INF/webx.xml" matched [resource pattern="/webroot"], at "resources.xml", beanName="resourceLoadingService"
列出所有可用的资源定义和别名的pattern

例 5.34. 通过ResourceLoadingService接口列出所有可用的资源定义和别名的pattern

String[] patterns = resourceLoadingService.getPatterns(true);

5.3.10. 在非Web环境中使用Resource Loading服务

在非Web环境中使用的ResourceLoadingXmlApplicationContext

图 5.4. 在非Web环境中使用的ResourceLoadingXmlApplicationContext

在非Web环境中使用Resource Loading服务的最好方法,是创建ResourceLoadingXmlApplicationContext作为Spring容器。

例 5.35. 创建ResourceLoadingXmlApplicationContext容器

ApplicationContext context = new ResourceLoadingXmlApplicationContext(
                                                          new FileSystemResource("beans.xml"));

只要beans.xml中包含<resource-loading>的配置,就会自动启用Resource Loading服务,并取代Spring原来的ResourceLoader机制。

5.4. ResourceLoader参考

Resource Loading服务的核心是ResourceLoader。和Spring ResourceLoader不同,Resource Loading服务的ResourceLoader是可扩展的轻量级对象,担负着装载某一种类型的资源的具体任务。例如FileResourceLoader负责装载文件系统的资源;WebappResourceLoader负责装载WEB应用中的资源等等。

当你需要新的资源装载方式时,你所要做的,就是实现一种新的ResourceLoader。例如,你想从数据库中装载资源,那么就可以实现一个DatabaseResourceLoader

5.4.1. FileResourceLoader

FileResourceLoader的功能是:从文件系统中装载资源。

基本用法

例 5.36. FileResourceLoader的基本用法

<resource pattern="/my/virtual">
    <res-loaders:file-loader />
</resource>

这样,file-loader会从哪里装载资源呢?

答案是:从当前配置文件所在的目录中装载。假如上述资源配置所在的配置文件是c:/myapp/conf/resources.xml,那么file-loader就会从c:/myapp/conf/myFile.xml文件中装载/my/virtual/myFile.xml资源。

这样做的思路源自于Apache的一个项目:Ant。Ant是一个广为使用的build工具。每一个Ant项目,都有一个build.xml脚本,在里面定义了很多target,诸如编译项目、打包等。通常我们都会把build.xml这个文件放在项目的根目录中,然后build.xml中的命令全是使用相对于build.xml所在的项目根目录计算出来的相对路径。例如:

例 5.37. Ant脚本(build.xml

<project basedir=".">
    ...
    <target ...>
        <copy todir="bin">
            <fileset dir="src"/>
        </copy>
    </target>
    ...
</project>

在上面的Ant脚本中,binsrc目录全是相对于build.xml所在目录的相对目录。这样做的好处是,当你把项目移到不同的环境中,你也无需改变配置文件和脚本。

FileResourceLoader采用了和Ant完全类似的想法。

指定basedir

例 5.38. 在FileResourceLoader中指定basedir

<resource pattern="/my/virtual">
    <res-loaders:file-loader basedir="${my.basedir}" />
</resource>

FileResourceLoader当然也支持指定basedir根目录。这样,它就会从指定的basedir的子目录中查找资源。

一般来说,我们需要利用Spring Property Placeholder来设置basedir。在上面的例子中,我们可以在系统启动时,指定JVM参数:-Dmy.basedir=c:/mydata。在不同的系统环境中,必须指定正确的basedir,否则,<file-loader>有可能找不到资源。

搜索多个路径

例 5.39. 在FileResourceLoader中指定多个搜索路径

<resource pattern="/my/virtual">
    <res-loaders:file-loader basedir="...">
        <res-loaders:path>relativePathToBasedir</res-loaders:path> 
        <res-loaders:path type="absolute">c:/absolutePath</res-loaders:path> 
    </res-loaders:file-loader>
</resource>

搜索路径默认为相对路径,相对于指定的basedir。如果basedir未指定,则相对于当前resource-loading所在的配置文件的路径。

搜索路径也可以是绝对路径。

FileResourceLoader支持搜索多个路径,类似于操作系统在PATH环境变量所指定的路径中,搜索可执行文件;也类似于Java在CLASSPATH参数所指定的路径中,搜索classes。

5.4.2. WebappResourceLoader

WebappResourceLoader的功能是:从当前WEB应用中装载资源,也就是从ServletContext对象中装载资源。

例 5.40. 配置WebappResourceLoader

<resource pattern="/my/virtual">
    <res-loaders:webapp-loader />
</resource>

5.4.3. ClasspathResourceLoader

ClasspathResourceLoader的功能是:从classpath中装载资源,也就是从当前的ClassLoader对象中装载资源。

例 5.41. 配置ClasspathResourceLoader

<resource pattern="/my/virtual">
    <res-loaders:classpath-loader />
</resource>

5.4.4. SuperResourceLoader

SuperResourceLoader的功能是:调用Resource Loading服务来取得资源。它有点像Java里面的super操作符。

取得新名字所代表的资源

例 5.42. 用SuperResourceLoader取得新名字所代表的资源

<resource pattern="/my/virtual">
    <res-loaders:super-loader basedir="/webroot/WEB-INF" />
</resource>

这个操作类似于<resource-alias>

如果在当前context的Resource Loading服务中找不到资源,它会前往parent context中查找。

在parent context中查找资源

例 5.43. 用SuperResourceLoader查找parent context中的资源

<resource pattern="/my/virtual">
    <res-loaders:super-loader />
</resource>

如果你不指定name参数,那么SuperResourceLoader会直接去parent context中查找资源,而不会在当前context的Resource Loading服务中找。

5.4.5. 关于ResourceLoader的其它考虑

以上所有的ResourceLoader都被设计成可以在任何环境中工作,即使当前环境不适用,也不会报错。

WebappResourceLoader可以兼容非WEB环境

在非WEB环境中,例如单元测试环境、你直接通过XmlApplicationContext创建的Spring环境,WebappResourceLoader也不会出错 —— 只不过它找不到任何资源而已。

SuperResourceLoader可以工作于非级联的环境

也就是说,即使parent context不存在,或者parent context中没有配置Resource Loading服务,SuperResourceLoader也是可以工作的。

这样,同一套资源配置文件,可以被用于所有环境。

5.5. 本章总结

Resource Loading服务提供了一套高度可扩展的、强大的资源装载机制。这套机制和Spring ResourceLoader无缝连接。使用它并不需要特殊的技能,只要掌握Spring的风格即可。