浅聊Spring的资源管理 (Resource)
Spring 为什么引入资源管理?
Java 中有各种各样的资源,资源的位置包括本地文件系统、网络、类路径等,资源的形式可以包括文件、二进制流、字节流等,针对不同的资源又有不同的加载形式。本地文件系统中的文件在 Java 中使用 File 表示,使用 FileInputStream 读取。网络上的资源使用 URL 表示,使用 URLConnection 获取 InputStream 进行读取。而类路径下的资源使用 ClassLoader 进行读取。为了使用统一的方式访问资源,Spring 将资源抽象为 Resource,将资源的加载抽象为 ResourceLoader。Spring 配置文件的读取以及扫描包中的 bean 都会通过 Resource 访问资源。
资源抽象 Resource
Resource 是 Spring 对资源抽象的一个接口,具体的资源可以有不同的实现类。Resource 相关方法如下:
public interface Resource extends InputStreamSource {
// 资源是否以物理的形式真实存在
boolean exists();
// 资源是否可以通过 #getInputStream() 方法进行读取
default boolean isReadable() {
return exists();
}
// 资源是否已经被打开
default boolean isOpen() {
return false;
}
// 资源是否为文件系统中的资源
default boolean isFile() {
return false;
}
// 获取资源 URL 的表示形式
URL getURL() throws IOException;
// 获取资源 URI 的表示形式
URI getURI() throws IOException;
// 获取资源文件的表示形式
File getFile() throws IOException;
// 获取资源 Channel 的表示形式
default ReadableByteChannel readableChannel() throws IOException {
return Channels.newChannel(getInputStream());
}
// 获取资源的内容长度
long contentLength() throws IOException;
// 获取资源最后修改的时间戳
long lastModified() throws IOException;
// 创建一个位置相对于当前资源的资源
Resource createRelative(String relativePath) throws IOException;
// 获取资源的文件名称
@Nullable
String getFilename();
// 获取资源的描述信息
String getDescription();
}
Resource 接口继承了接口 InputStreamSource ,InputStreamSource 源码如下:
public interface InputStreamSource {
// 获取输入流
InputStream getInputStream() throws IOException;
}
因此,每个 Resource 都可以获取到 InputStream。常见的 Resource 如下面的类图所示:
每个 Resource 的实现都封装了具体的资源。Resource 由 AbstractResource 进行主要的抽象实现,其子类可以根据封装的资源进行重写,由于源码比较简单,这里不再进行分析,感兴趣的朋友可以自行查看相关源码。 主要的 Resource 包括如下:
- FileSystemResource:对文件系统中 File 及 Path 的封装,除了可以读取资源,还可以对资源进行写操作。
- ClassPathResource:类路径下资源的封装。
- UrlResource:URL 资源的封装。
- InputStreamResource:输入流资源的封装。
- ByteArrayResource:字节数组的封装。
- ServletContextResource:对 Servlet 上下文的封装。
资源加载抽象 ResourceLoader
与 Java 中的类加载相似,Java 使用 ClassLoader 加载类,而 Spring 抽象出 ResourceLoader 加载 Resource。ResourceLoader 也是一个接口,根据不同的资源可以有不同的实现。ResourceLoader 源码如下:
public interface ResourceLoader {
//类资源位置的前缀 classpath:
String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;
// 根据指定的资源位置获取资源
Resource getResource(String location);
// 获取当前类加载器中使用的 ClassLoader
@Nullable
ClassLoader getClassLoader();
}
ResourceLoader 中定义了根据资源位置获取资源的方法,相关类图见下图:
DefaultResourceLoader 是 ResourceLoader 的默认实现,其根据资源路径的协议进行解析为不同的 Resource 实现,但是它只能够根据资源路径获取一个 Resource。其获取资源的方法源码如下:
@Override
public Resource getResource(String location) {
Assert.notNull(location, "Location must not be null");
// 先根据保存的协议解析器解析支持协议的资源
for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
Resource resource = protocolResolver.resolve(location, this);
if (resource != null) {
return resource;
}
}
// 使用 Class 或 ClassLoader 获取资源
if (location.startsWith("/")) {
return getResourceByPath(location);
}
else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
// 获取类路径下的资源
return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
}
else {
try {
// 尝试获取 URL 资源
// Try to parse the location as a URL...
URL url = new URL(location);
return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
}
catch (MalformedURLException ex) {
// No URL -> resolve as resource path.
return getResourceByPath(location);
}
}
}
DefaultResourceLoader 先根据协议解析器获取资源,因此我们可以定义自己的协议解析器解析自定义的协议的资源。如果路径以 / 开头,它会获取到一个 ClassPathContextResource 资源,否则如果以资源位置以 classpath: 开头,会获取到一个 ClassPathResource 资源,最后会尝试获取 UrlResource 资源。
如果想要根据资源路径的模式字符串获取多个 Resource ,则只能通过 ResourcePatternResolver,ResourcePatternResolver 源码如下:
public interface ResourcePatternResolver extends ResourceLoader {
// 类路径下资源文件的前缀
String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
// 根据资源路径模式字符串获取资源
Resource[] getResources(String locationPattern) throws IOException;
}
ResourcePatternResolver 只有一个实现 PathMatchingResourcePatternResolver,它会根据 ant 风格的路径去查找资源。实现源码如下:
// ant 风格的路径匹配
private PathMatcher pathMatcher = new AntPathMatcher();
@Override
public Resource[] getResources(String locationPattern) throws IOException {
Assert.notNull(locationPattern, "Location pattern must not be null");
if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
// 处理 classpath*: 开头类路径下的资源
// a class path resource (multiple resources for same name possible)
if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
// 查询符合模式的资源
// a class path resource pattern
return findPathMatchingResources(locationPattern);
}
else {
// 查询不匹配模式的资源
// all class path resources with the given name
return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
}
}
else {
// 处理非类路径下的资源
// Generally only look for a pattern after a prefix here,
// and on Tomcat only after the "*/" separator for its "war:" protocol.
int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 :
locationPattern.indexOf(':') + 1);
if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
// 查找匹配模式的资源
// a file pattern
return findPathMatchingResources(locationPattern);
}
else {
// 查询不匹配模式的资源
// a single resource with the given name
return new Resource[] {getResourceLoader().getResource(locationPattern)};
}
}
}
获取资源时会先判断资源路径是否为类路径,然后再判断路径是否为支持的模式,默认支持 ant 风格的路径匹配,对类路径下的资源和非类路径下的资源具有不同的处理。
如何在 Spring 中获取 Resource 和 ResourceLoader
Spring 的内部有关资源的加载大量使用了 Resource 和 ResourceLoader,自然我们也同样可以使用 Resource 获取资源。
由于 Resource 与具体的资源进行绑定,Spring 并未把它作为 bean 注入到容器中,为了获取 Resource ,我们可以通过在 bean 的 成员变量中通过 @Value 注入 Resource 及其数组对象。示例如下:
// 类路径下创建文件 META-INF/dev.properties 内容为 profile=dev
// 类路径下创建文件 META-INF/prod.properties 内容为 profile=prod
public class Main {
@Value("classpath:/META-INF/prod.properties")
private Resource resource;
@Value("classpath*:/META-INF/*.properties")
private Resource[] resources;
public static void main(String[] args) throws IOException {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Main.class);
Main bean = context.getBean(Main.class);
String content = FileReader.create(bean.resource.getFile()).readString();
System.out.println(content);
System.out.println("=========");
for (Resource resource : bean.resources) {
System.out.println(FileReader.create(resource.getFile()).readString());
System.out.println("=========");
}
}
}
执行结果如下:
profile=prod
=========
profile=dev
=========
profile=prod
=========
通过 @Value 注入 Resource ,成功读取到了类路径下的资源文件。
ResourceLoader 作为可能会被经常使用的组件,Spring 已经将其注册为 bean,因此可以直接通过 @Autowire 注入,另外由于 ApplicationContext 继承了 ResourceLoader 接口,因此也可以直接通过 @Autowire 注入 ApplicationContext 来使用 ResourceLoader,此外 Spring 还提供了 ResourceLoaderAware 接口,在 bean 的生命周期中,如果 bean 实现了接口 ResourceLoaderAware ,则 Spring 会调用 setResourceLoader 方法,这样就拿到了 ResourceLoader,拿到后我们就可以直接用来加载资源。示例代码如下:
public class Main implements ResourceLoaderAware {
@Autowired
private ResourceLoader resourceLoader;
@Autowired
private ApplicationContext applicationContext;
private ResourceLoader awareResourceLoader;
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.awareResourceLoader = resourceLoader;
}
public static void main(String[] args) throws IOException {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Main.class);
Main bean = context.getBean(Main.class);
System.out.println(bean.resourceLoader);
System.out.println(bean.applicationContext);
System.out.println(bean.awareResourceLoader);
}
}
执行结果如下:
org.springframework.context.annotation.AnnotationConfigApplicationContext@6aaa5eb0, started on Wed Sep 02 22:36:40 CST 2020
org.springframework.context.annotation.AnnotationConfigApplicationContext@6aaa5eb0, started on Wed Sep 02 22:36:40 CST 2020
org.springframework.context.annotation.AnnotationConfigApplicationContext@6aaa5eb0, started on Wed Sep 02 22:36:40 CST 2020
三种方式都打印出来了结果,说明这三种方式都可以正常获取 ResourceLoader,并且这三种方式获取到的对象为同一个。
总结
Resource 和 ResourceLoader 作为 Spring 中资源和加载资源的抽象,在底层加载资源的地方都会被用到,通过对这两者的熟悉,在阅读 Spring 源码时,可以把精力放在其他地方,并且我们也可以使用 Resource 获取我们自己的资源。
参考: |