Spring核心——资源管理

全文共 2772 个字

Resource——资源

对于一个联机事务型系统(业务系统)来说,所依赖的外部运行信息主要有2个来源:数据项资源项。数据项的存放位置通常是使用各种关系性或NoSql数据库,而资源项通常是使用文件、网络信息的方式来存储。

早在JDK1.0的时代Java就已经提供了本地资源和网络资源的读取功能——java.net.URL。他可以同时管理本地资源(操作系统资源)以及网络资源,如下面这个例子:

(文中的代码仅仅用于说明问题,源码请到案例gitee库下载,对应的代码在包chkui.springcore.example.hybrid.resource中。)

public class ResourceApp {
	public static void main(String[] args) throws MalformedURLException {
		//读取本地资源
		URL url = ResourceApp.class.getResource("/extend.properties");
		print(url);
		//读取互联网资源
		url = new URL("http", "www.baidu.com", 80, "");
		print(url);
		url = new URL("https", "www.chkui.com", 443, "/174870bb04.js");
		print(url);
	}
}
// 输出
// file:/work/chkui/spring-core-sample/bin/main/extend.properties
// http://www.baidu.com:80
// https://www.chkui.com:443/174870bb04.js

对于每一个类来说getResource方法可以获取当前类所在的系统路径(getResource("")),以及classpath的路径(getResource("/")),利用这个功能我们可以获取操作系统上所知的任何资源。除了本地文件,URL也可以通过域名规则来获取网络上的资源。

注意输出内容中的开头file:http:以及https:,他们表示资源的协议,除了以上这三者,还有ftp:、mailto:等协议。关于URL的详细解释可以看ITEF标准

URL指向某一个资源之后,可以使用URL::openStream或URL::getFile等方法进一步获取文件中的内容:

public class ResourceApp {
	public static void main(String[] args) throws MalformedURLException {
		Url url = new URL("https", "www.chkui.com", 443, "/174870bb04.js");
		try(InputStream is = url.openStream()){
			byte[] buffer = new byte[1024*1024];
			is.read(buffer);
			String content = new String(buffer, Charset.forName("UTF-8"));
			print("Content :", content);
		} catch (IOException e) {
		}
	}
}

Spring中的资源管理

Spring的资源管理在JDK的基础功能上进行了强大的扩展,即使你不用Spring的整个生态或者容器,你也可以将其资源管理作为一个工具整合到自己的系统中而提高效率。它扩展了以下内容:

  1. 隐藏底层实现。对于各种各样的资源Spring都使用了不同的实现类来管理,但是他利用适配器模式让使用者仅仅需要了解org.springframework.core.io.Resource接口即可。
  2. 新增资源存在判断、资源操作权限相关的功能,相对于java.net.URL资源不存在则设置为null更友好。
  3. 支持通配符来获取资源,例如 :classpath:a/b/**/applicationContext-*.xml。

协议与路径

在前面的内容中就提到了多个协议,spring的资源管理功能除了标准的协议,还增加了一个——classpath:协议,他表示从当前的classpath根路径开始获取资源。对于Spring的资源管理功能而言,主要有以下几种协议:

协议  例子 说明
classpath: classpath:res/extend.properties 从当前jvm的classpath根路径开始获取资源。
file: file:///tmp/myfile.data 从操作系统(文件路径)的路径获取资源。
http(s): http(s)://www.chkui.com/ 从互联网获取资源。
(无标记) /data/extend.data 根据应用上下文获取资源。

classpath:file:http(s):这三个协议都很明确的指明了获取资源的路径,但是没有声明协议的情况就比较特殊,他需要根据上下文来判定适用的路径。

上下文与IoC这篇文章中已经介绍过,经过层层继承和实现,Spring提供容器实现功能的主要是ClassPathXmlApplicationContextFileSystemXmlApplicationContext两个类,这两个Context本质上都是实现了相同的Context功能,最明显的区别之一就是加载文件的路径不同。比如下面的情况:

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

ClassPathXmlApplicationContext默认启用的是ClassPathResource来管理资源,所以上面的路径配置相当于"classpath:config/ctx.xml"。但是如果修改为以下形式:

ApplicationContext ctx = new ClassPathXmlApplicationContext("file:///config/ctx.xml");

通过协议明确告知路径规则,那么在ApplicationContext会使用对应的FileSystemResource来加载管理资源。

FileSystemXmlApplicationContextClassPathXmlApplicationContext相互对应——默认使用的FileSystemResource,可以通过声明协议来指定对应的资源加载类。

上面的内容提到了ClassPathResource和FileSystemResource。Spring为不同类型、协议的资源指定了各种各种的org.springframework.core.io.Resource实现类,主要有 UrlResource、ClassPathResource、FileSystemResource、ServletContextResource、InputStreamResource、ByteArrayResource。从字面上看大概能了解对应的功能。在使用的时候我们并不需要了解他们的具体实现,只要知道不同的协议对应的资源路径即可。

获取资源的方法

直接使用ApplicationContext

在明确所支持的协议之后,我们就可以用ResourcePatternResolver::getResources方法来获取资源。ApplicationContext继承了ResourcePatternResolver接口,所以我们通常使用以下方法获取资源:

package chkui.springcore.example.resource;

public class ResourceApp {
	public static void main(String[] args){
		ApplicationContext ctx = new AnnotationConfigApplicationContext(ResourceApp.class);
		Resource res = ctx.getResource("classpath:extend.properties");
		print("Resource :", res);
		res = ctx.getResource("https://www.chkui.com");
		print("Resource :", res);
	}
}

ResourceLoaderAware注入

除了直接使用ApplicationContext,还可以通过继承ResourceLoaderAware的方式来获取资源加载接口:

package chkui.springcore.example.resource;
public class LoadResourceBean implements ResourceLoaderAware{

	@Override
	public void setResourceLoader(ResourceLoader resourceLoader) {
		Resource res = resourceLoader.getResource("classpath:extend.properties");
		System.out.println("Bean load Resource :" + res);
	}
}

实际上这里传入进来的ResourceLoader就是ApplicationContext,所以用ApplicationContextAware也可以实现对应的功能。但是为了明确功能的用途,这里最好还是实现ResourceLoaderAware比较合理。

Autowired注入

在2.5.x之后,spring可以使用@Autowired注解引入ResourceLoader(ApplicationContext):

package chkui.springcore.example.resource;
public class LoadResourceBean implements ResourceLoaderAware{
	@Autowired
	ResourceLoader resourceLoader;

	@Override
	public void setResourceLoader(ResourceLoader resourceLoader) {
		System.out.println("Is ApplicationContext? " + (this.resourceLoader == resourceLoader));
		Resource res = this.resourceLoader.getResource("classpath:extend.properties");
		System.out.println("Bean load Resource :" + res);
	}
}

和普通的Bean一样,还可以通过构造方法和setter方法注入ResourceLoader。

XML配置获取资源

我们可以直接在XML中指定资源路径,然后在setter或构造方法中获取到对应的资源,看下面的例子。

XMLConfigBean的Set方法直接获取一个Resource

package chkui.springcore.example.hybrid.resource;

public class XMLConfigBean {
	public void setResource(Resource res) throws IOException {
		System.out.println("XML load Resource :" + res);
		Properties p = new Properties();
		p.load(res.getInputStream());
		System.out.println("Properties Info: " + p.getProperty("info"));
	}
}

我们只需要在XML配置文件中指定资源路径位置,Spring会自动帮我们完成转换:

<beans>
	<bean class="chkui.springcore.example.hybrid.resource.XMLConfigBean">
		<property name="resource" value="classpath:extend.properties"/>
	</bean>
</beans>

在XMLConfigBean::setResource方法中我们拿到的是"classpath:extend.properties"这一项资源。

通配符指定资源

除了使用指定固定路径的方式获取一项资源,我们还可以使用"?"、"*"等通配符使用匹配规则来获取资源,例如:

Resource[] resList = ctx.getResources("classpath:hybrid/**/*.xml");

Spring官网将这种资源匹配规则称为“Ant-style匹配”,虽然并不知道源自什么地方(应该是源自Apache Ant项目,但是我在Ant项目文档中还没看到对应的说明,心细致的码友可以再找找),但是Spring官方文档对其有详细的说明,详见AntPathMatcher的说明。Ant-style的匹配规则大致如下:

  1. "?":匹配一个字符。例如"classpath:conf?g.xml"匹配"classpath:config.xml"也匹配"classpath:conf1g.xml"但是不匹配"classpath:conf12g.xml"
  2. "*":匹配0到多个字符。例如"classpath:*.xml"匹配classpath根目录下所有.xml文件。而"classpath:config/*.xml"匹配config文件夹中所有.xml文件。
  3. "**":匹配0到多个目录。例如"classpath:**/*.xml"匹配整个classpath下所有*.xml文件。"classpath:config/**/*.xml"匹配config文件夹以及所有子文件夹的.xml文件。
  4. {arg1:{a-z}+}:匹配任意多个a-z的字符,并将匹配到的内容赋值到变了arg1中。该条规则实用于AntPathMatcher,当无法在ApplicationContext的资源匹配规则中使用。

classpath*:扩展

在通配符的基础上,spring扩展了一个classpath*:协议。

对于一个运行的Jvm来说,classpath的“根目录”一般有多个。比如在当前开发的工程有一个包含main方法的类文件——chkui/example/spinrg/app.class,此时引入一个jar包也包含一个一样的类文件chkui/example/spring/app.class(有空的码友可以自己试试Jvm到底运行哪个)。这种情况对于Jvm来说就引出"多个classpath"和"首选classpath"的概念,而classpath:和classpath*的差异就是,前者从首选classpath中优先获取资源,而后者会从所有classpath中寻找资源。而首先classpath一般是我们当前工程的编译文件(案例代码在[project-root]/bin/main)。

其实在Jvm的资源加载方式上已经对classpath:classpath*:提供了不同的实现,但是理解起来比较“绕”。一般情况下我们使用Class::getResource都是获取首选classpath路径下的资源,而使用ClassLoader::getResources(classPath)可以获取所有classpath下的资源。

下面的代码展示了这个过程,案例代码在chkui.springcore.example.hybrid.resource.ResourceApp::multiResourceLoad方法中。

为了演示这个过程我们引入了Google的Guava包(因为整个工程都没用到guava的内容,所以修改他的类不会产生影响),然后对应的在自己的工程中增加一个Guava包中相同的package和类:

package com.google.common.base;
public final class Preconditions {}

在编译之后,会在bin文件夹(如果是maven就是/target)中产生一个main/com/google/common/base/Preconditions.class文件。然后通过下面的代码测试资源加载:

public static void multiResourceLoad() throws IOException {
		final String classPath = "com/google/common/base/Preconditions.class";

		//class.getResource需要使用"/"表示root路径
        //首选路径的资源
		print("classpath: ", ResourceApp.class.getResource("/" + classPath));
		//Verify没有被覆盖,输出Jar包中的内容,注意jar:file: 协议的格式
		print("In Jar classpath: ", ResourceApp.class.getResource("/" + unMultiClassPath));

		//ClassLoader::getResource获取首选路径资源
		print("First classpath: ", Verify.class.getClassLoader().getResource(classPath));
		//ClassLoader::getResources获取所有资源
		Enumeration<URL> e = ResourceApp.class.getClassLoader().getResources(classPath);
		int count = 1;
		while (e.hasMoreElements()) {
			URL url = e.nextElement();
			print("classpath*[", count++ ,"]:", url);
		}
	}

运行之后,只有在最后的迭代器中输出了Guava包中的Preconditions.class的路径,而其余位置都输出的是我自行创建的Preconditions.class,也就是首选classpath下的Preconditions.class,首选的资源也就是ClassLoader::getResources获取的迭代器的第一个值。

Spring的classpath*:协议实际上底层也是用ClassLoader::getResources的方式实现的,不过扩展了支持通配符并将资源转换为org.springframework.core.io.Resource。上面用JDK演示的代码用spring的资源管理实现为下面的形式:

public static void multiResourceLoad(ApplicationContext ctx){
    ApplicationContext ctx = new AnnotationConfigApplicationContext();
    final String classPath = "com/google/common/base/Preconditions.class";
    final String unMultiClassPath = "com/google/common/base/Verify.class";
    print("classpath: ", Arrays.asList(ctx.getResources("classpath:" + classPath)));
    print("classpath*: ", Arrays.asList(ctx.getResources("classpath*:" + classPath)));
    print("unmulti-classpath*: ", Arrays.asList(ctx.getResources("classpath*:" + unMultiClassPath)));
}

Spring中的各项资源

不仅仅是ApplicationContext::getResources方法,实际上Spring中绝大部分外部资源加载都是通过前面介绍的规则使用同一个工具类完成的,所以我们可以在许多地方使用对应的"协议"来管理我们的资源,比如下面的例子:

@ImportResource("classpath:hybrid/resource/config-*.xml")
public class ResourceApp {}