统一资源加载策略

ApplicationContext与ResourceLoader

ApplicationContext继承了ResourcePatternResolver,当然就间接实现了ResourceLoader接口。所以,任何的ApplicationContext实现都可以看作是一个ResourceLoader甚至ResourcePatternResolver。而这就是ApplicationContext支持Spring内统一资源加载策略的真相。

通常,所有的ApplicationContext实现类会直接或者间接地继承org.springframework.context.support.AbstractApplicationContext,从这个类上,我们就可以看到ApplicationContextResourceLoader之间的所有关系。

AbstractApplicationContext继承了DefaultResourceLoader,那么,它的getResource(String)当然就直接用DefaultResourceLoader的了。剩下需要它“效劳”的,就是ResourcePatternResolverResource[] getResources(String),当然,AbstractApplicationContext也不负众望,当即拿下。

AbstractApplicationContext类的内部声明有一个resourcePatternResolver,类型是ResourcePatternResolver,对应的实例类型为PathMatchingResourcePatternResolver。之前我们说过PathMatchingResourcePatternResolver构造的时候会接受一个ResourceLoader,而AbstractApplicationContext本身又继承自DefaultResourceLoader,当然就直接把自身给“贡献”了。

这样,整个ApplicationContext的实现类就完全可以支持ResourceLoader或者ResourcePatternResolver接口,你能说ApplicationContext不支持Spring的统一资源加载吗?说白了,ApplicationContext的实现类在作为ResourceLoader或者ResourcePatternResolver时候的行为,完全就是委派给了PathMatchingResourcePatternResolverDefaultResourceLoader来做。

图5-2给出了AbstractApplicationContextResourceLoaderResourcePatternResolver之间的类层次关系。

image-20220405130755950

有了这些做前提,让我们看看作为ResourceLoader或者ResourcePatternResolverApplicationContext,到底因此拥有了何等神通吧!

1. 扮演 ResourceLoader 的角色

既然ApplicationContext可以作为ResourceLoader或者ResourcePatternResolver来使用,那么,很显然,我们可以通过ApplicationContext来加载任何Spring支持的Resource类型。与直接使用ResourceLoader来做这些事情相比,很明显,ApplicationContext的表现过于“谦虚”了。下方代码演示的正是“大材小用”后的ApplicationContext

 
    ResourceLoader resourceLoader = new ClassPathXmlApplicationContext("配置文件路径");
    // 或者
    // ResourceLoader resourceLoader = new FileSystemXmlApplicationContext("配置文件路径");
 
    Resource fileResource = resourceLoader.getResource("D:/spring21site/README");
    assertTrue(fileResource instanceof ClassPathResource);
    assertFalse(fileResource.exists());
 
    Resource urlResource2 = resourceLoader.getResource("http://www.spring21.cn");
    assertTrue(urlResource2 instanceof UrlResource);
 

2. ResourceLoader 类型的注入

在大部分情况下,如果某个bean需要依赖于ResourceLoader查找定位资源 ,我们可以为其注入容器中声明的某个具体的ResourceLoader实现,该bean也无需实现任何接口,直接通过构造方法注入或者setter方法注入规则声明依赖即可,这样处理是比较合理的。

不过,如果你不介意你的bean定义依赖于Spring的API,那不妨考虑用一下Spring提供的便利。

上一章曾经提到几个对ApplicationContext特定的Aware接口,这其中就包括ResourceLoaderAwareApplicationContextAware接口。

假设我们有类定义如下方代码所示。

    public class FooBar {
        private ResourceLoader resourceLoader;
 
        public void foo(String location) {
            System.out.println(getResourceLoader().getResource(location).getClass());
        }
 
        public ResourceLoader getResourceLoader() {
            return resourceLoader;
        }
 
        public void setResourceLoader(ResourceLoader resourceLoader) {
            this.resourceLoader = resourceLoader;
        }
    }

该类出于什么目的要依赖于ResourceLoader,我们暂且不论,要为其注入什么样的ResourceLoader实例才是我们当下该操心的事情。姑且先给它注入DefaultResourceLoader。这样也就有了如下配置:

 
    <bean id="resourceLoader" class="org.springframework.core.io.DefaultResourceLoader">
    </bean>
 
    <bean id="fooBar" class="...FooBar">
      <property name="resourceLoader">
    		<ref bean="resourceLoader"/>
      </property>
    </bean>

不过,ApplicationContext容器本身就是一个ResourceLoader,我们为了该类还需要单独提供一个resourceLoader实例就有些多于了,直接将当前的ApplicationContext容器作为ResourceLoader注入不就行了?

ResourceLoaderAwareApplicationContextAware接口正好可以帮助我们做到这一点,只不过现在的FooBar需要依赖于Spring的API了。不过,在我看来,这没有什么大不了,因为我们从来也没有真正逃脱过依赖(这种依赖也好,那种依赖也罢)。

现在,修改我们的FooBar定义,让其实现ResourceLoaderAware或者ApplicationContextAware接口,修改后的定义如下方代码清单所示。

   public class FooBar implements ResourceLoaderAware {
        private ResourceLoader resourceLoader;
 
        public void foo(String location) {
            System.out.println(getResourceLoader().getResource(location).getClass());
        }
 
        public ResourceLoader getResourceLoader() {
            return resourceLoader;
        }
 
        public void setResourceLoader(ResourceLoader resourceLoader) {
            this.resourceLoader = resourceLoader;
        }
    }
 
 

或者

 
    public class FooBar implements ApplicationContextAware {
        private ResourceLoader resourceLoader;
 
        public void foo(String location) {
            System.out.println(getResourceLoader().getResource(location).getClass());
        }
 
        public ResourceLoader getResourceLoader() {
            return resourceLoader;
        }
 
        public void setApplicationContext(ApplicationContext ctx) throws BeansException {
            this.resourceLoader = ctx;
        }
    }
 

剩下的就是直接将一个FooBar配置到bean定义文件即可,如下所示:

 
    <bean id="fooBar" class="...FooBar">
    </bean>
 

现在,容器启动的时候,就会自动将当前ApplicationContext容器本身注入到FooBar中,因为ApplicationContext类型容器可以自动识别Aware接口。

当然,如果应用场景仅使用ResourceLoader类型即可满足需求,那么,还是使用ResourceLoaderAware比较合适,ApplicationContextAware相对来说过于宽泛了些(当然,使用也未尝不可)。

3. Resource 类型的注入

我们之前讲过, 容器可以将bean定义文件中的字符串形式表达的信息,正确地转换成具体对象定义的依赖类型。对于那些Spring容器提供的默认的PropertyEditors无法识别的对象类型,我们可以提供自定义的PropertyEditor实现并注册到容器中,以供容器做类型转换 的时候使用。

默认情况下,BeanFactory容器不会为org.springframework.core.io.Resource类型提供相应的PropertyEditor,所以,如果我们想注入Resource类型的bean定义,就需要注册自定义的PropertyEditorBeanFactory容器。不过,对于ApplicationContext来说,我们无需这么做,因为ApplicationContext容器可以正确识别Resource类型并转换后注入相关对象。

假设有一个XMailer类,它依赖于一个模板来提供邮件发送的内容,我们声明模板为Resource类型,那么,最终的XMailer定义也就如下方代码所示。

 
    public class XMailer {
        // Resource类型的模板
        private Resource template;
 
        // 发送邮件
        public void sendMail(Map mailCtx) {
            // String mailContext = merge(getTemplate().getInputStream(),mailCtx);
            //...
        }
 
        // 获取模板
        public Resource getTemplate() {
            return template;
        }
 
        // 设置模板
        public void setTemplate(Resource template) {
            this.template = template;
        }
    }

该类定义与平常的bean定义没有什么差别,我们直接在配置文件中以String形式指定template所在位置,ApplicatonContext就可以正确地转换类型并注入依赖,配置内容如下:

    <bean id="mailer" class="...XMailer">
    <property name="template" value="..resources.default_template.vm"/>
      ...
    </bean>

ApplicationContext启动时,会通过一个org.springframework.beans.support.ResourceEditorRegistrar来注册Spring提供的针对Resource类型的PropertyEditor实现到容器中,这个PropertyEditor叫做org.springframework.core.io.ResourceEditor

这样,ApplicationContext就可以正确地识别Resource类型的依赖了。至于ResourceEditor怎么实现我就不用说了吧?你想啊,把配置文件中的路径让ApplicationContext作为ResourceLoader给你定位一下不就得了。

如果应用对象需要依赖一组Resource,与ApplicationContext注册了ResourceEditor类似,Spring提供了org.springframework.core.io.support.ResourceArrayPropertyEditor实现,我们只需要通过CustomEditorConfigurar告知容器即可。

4. 在特定情况下,ApplicationContext的Resource加载行为

特定的ApplicationContext容器实现,在作为ResourceLoader加载资源时,会有其特定的行为。我们下面主要讨论两种类型的ApplicationContext容器,即ClassPathXmlApplicationContextFileSystemXmlApplicationContext。其他类型的ApplicationContext容器,会在稍后章节中提到。

我们知道,对于URL所接受的资源路径来说,通常开始都会有一个协议前缀,比如file:http:ftp:等。既然Spring使用UrlResource对URL定位查找的资源进行了抽象,那么,同样也支持这样类型的资源路径,而且,在这个基础上,Spring还扩展了协议前缀的集合。

ResourceLoader中增加了一种新的资源路径协议——classpath:ResourcePatternResolver又增加了一种——classpath*:。这样,我们就可以通过这些资源路径协议前缀,明确地告知Spring容器要从classpath中加载资源,如下所示:

 
    // 代码中使用协议前缀
    ResourceLoader resourceLoader = new FileSystemXmlApplicationContext("classpath:conf/container-conf.xml");
 
 
 
    <!-- 配置中使用协议前缀 -->
    <bean id="..." class="...">
    	<property name="...">
      	<value>classpath:resource/template.vm</value>
    	</property>
    </bean>
 
 

classpath*:classpath:的唯一区别就在于,如果能够在classpath中找到多个指定的资源,则返回多个。我们可以通过这两个前缀改变某些ApplicationContext实现类的默认资源加载行为。

ClassPathXmlApplicationContextFileSystemXmlApplicationContext在处理资源加载的默认行为上有所不同。当ClassPathXmlApplicationContext在实例化的时候,即使没有指明classpath:或者classpath*:等前缀,它会默认从classpath中加载bean定义配置文件,以下代码中演示的两种实例化方式效果是相同的:

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

以及

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

FileSystemXmlApplicationContext则有些不同,如果我们像如下代码那样指定conf/appContext.xml,它会尝试从文件系统中加载bean定义文件:

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

不过,我们可以像如下代码所示,通过在资源路径之前增加classpath:前缀,明确指定FileSystemXmlApplicationContext从classpath中加载bean定义的配置文件:

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

这时,FileSystemXmlApplicationContext就是从Classpath中加载配置,而不是从文件系统中加载。也就是说,它现在对应的是ClassPathResource类型的资源,而不是默认的FileSystemResource类型资源。FileSystemXmlApplicationContext之所以如此,是因为它与org.springframework.core.io.FileSystemResourceLoader一样,也覆写了DefaultResourceLoadergetResourceByPath(String)方法,逻辑跟FileSystemResourceLoader一模一样。

当实例化相应的ApplicationContext时,各种实现会根据自身的特性,从不同的位置加载bean定义配置文件。当容器实例化并启动完毕,我们要用相应容器作为ResourceLoader来加载其他资源时,各种ApplicationContext容器的实现类依然会有不同的表现。

对于ClassPathXmlApplicationContext来说,如果我们不指定路径之前的前缀,它会从Classpath中加载这种没有路径前缀的资源。如类似如下指定的资源路径,ClassPathXmlApplicationContext依然尝试从Classpath加载:

 
 
    <bean id="..." class="...">
    	<property name="..." value="conf/appContext.xml"/>
    </bean>
 

如果当前容器类型为FileSystemXmlApplicationContext,它将从文件系统中给我们加载该文件。但是,就跟实例化时可以通过classpath:前缀覆盖掉FileSystemXmlApplicationContext的默认加载行为一样,我们也可以在这个时候用classpath:前缀强制指定它从Classpath中加载该文件,如以下代码所示:

 
 
    <bean id="..." class="...">
    	<property name="..." value="classpath:conf/appContext.xml"/>
    </bean>