专栏首页星月小站Spring框架源码分析(IoC):Resource、ResourceLoader和容器之间的关系

Spring框架源码分析(IoC):Resource、ResourceLoader和容器之间的关系

系列文章主页

Spring框架源码脉络分析系列文章

Resource和ResourceLoader

  • Java中资源可以被抽象成URL,Spring中将对物理资源的访问方式抽象成了Resource,Spring中的Resource访问策略有多种实现。
  • Spring可以利用一种利器来自动选择资源加载方式,这就是——ResourceLoader。

Resource接口体系

  • Resource接口是Spring中访问资源的抽象,Resource接口本身只提供了规定,下面有很多的实现类。都是从实际的系统底层资源进行抽象的资源描述符。一般来说在Spring中是将资源描述为URL格式和Ant风格带通配符的资源地址。
  • Resource接口的家族体系类图如下图所示:
  • 资源描述的顶级接口——Resource接口

Resource接口定义了资源常见的操作,抽象出了一些通用方法,再由不同的实现类去自定义。直接上Resource源码:

/**
 * 继承自InputStreamSource,即继承了getInputStream()
 * 可以获取InputStreamSource类型的输入流
 */
public interface Resource extends InputStreamSource {

	// 判断某个资源是否以物理形式存在
	boolean exists();

	// 判断资源是否可读,只有返回true了,才可以getInputStream()
	default boolean isReadable() {
		return exists();
	}

	// 判断资源是否打开,如果已经打开就不能重复多次读写,资源应该在读取完毕之后关闭
	default boolean isOpen() {
		return false;
	}

	// 判断资源是否为系统文件
	default boolean isFile() {
		return false;
	}

	// 获取资源对象的URL,不能表示为URL就抛异常
	URL getURL() throws IOException;

	// 获取资源对象的URI,不能表示为URI就抛异常
	URI getURI() throws IOException;

	// 获取资源的File表示对象,不能表示为File就抛异常
	File getFile() throws IOException;
	
	// 返回一个可以读取字节的通道
	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接口中定义的方法,只是通用型,并不是每一种实现类资源类型都必须实现所有方法,比如getFile()方法是基于文件类型的资源实现类才需要实现,如果不是基于文件的资源一般不需要实现此方法。

  • 可写的资源操作接口——WritableResource接口

由于Resource接口继承自InputStreamSource,而且并没有扩展资源的写操作,因为对于大部分资源来说,不一定是可写的,或者是不需要写操作,但一定是需要可读的。Spring针对需要进行写操作的资源单独定义了一个WritableResource接口,其中定义了三个写操作相关的方法:

public interface WritableResource extends Resource {

	// 表示资源是否支持写操作
	default boolean isWritable() {
		return true;
	}

	// 用来获取OutputStream输出流
	OutputStream getOutputStream() throws IOException;

	// 获取一个可以写入字节的通道
	default WritableByteChannel writableChannel() throws IOException {
		return Channels.newChannel(getOutputStream());
	}

}

可写的资源一般也包含可读资源的各种特性,因此WritableResource接口也继承了Resource接口。

  • 抽象公共实现类——AbstractResource抽象类

和其他接口体系一样,Spring在Resource接口的基础上实现了一个通用的抽象公共类AbstractResource,该抽象类实现了一些与资源类型无关的基础操作,如exists()方法等。

AbstractResource的实现中,是默认认为资源不是以URL或者File形式表现的,这样就可以适应一些费文件体系的资源,如字节流体系的资源等,对这些类型的操作,一般只支持读取操作。子类还可以通过覆盖isOpen()方法来标识当前资源是否支持多次读取。

由于AbstractResource只是简单实现了一些逻辑,这里就不展示源码了,本系列文章意在快速纵览全局,这里学习类在体系中的地位即可,可以自行去阅读源码。

  • 具体实现的资源类型——UrlResourceFileSystemResourceClassPathResourceServletContextResource
    • UrlResource类:符合URL规范的资源类型都可以表示为UrlResource对象。其中URL既可以访问网络资源,也可以访问本地资源,加不同的前缀即可。
    • ClassPathResource类:该资源类型在Spring中是非常常用的一种资源类型,用来访问类加载路径下的资源,相对于其他的Resource类型,该种类型在Web应用中可以自动搜索位于WEB-INF/classes下的资源文件,而无需使用绝对路径访问。
    • FileSystemResource类:访问文件系统资源的资源和类型,可以访问本地操作系统的文件系统。和Java提供的Feil文件类访问文件系统作用接近,但FileSystemResource可以消除操作系统底层差异,对不同的操作系统使用同一的API来访问。FileSystemResource类同时还实现了WritableResource接口,支持对资源的写操作。
    • ServletContextResource类:是ServletContext资源的Resource实现,用来访问相对于ServletContext路径下的资源。支持以流和URL的方式进行访问,但只有在扩展Web应用程序存档且资源实际位于文件系统上时才允许java.io.File访问。
  • 处理资源编码的实现类——EncodedResource

在Resource接口下,有一个特殊的实现类是EncodedResource,该类主要实现了对文件编码类型的处理。

关于Reource资源接口体系的介绍就到这里,如果读者对其中某种资源类型比较感兴趣,可以去阅读Spring的源码,了解其具体的实现逻辑。

在这里Spring对于资源类的设计中,其实用到了策略模式的设计思想,资源的类型在运行时是动态变化的,Spring将这几种资源实现类的选择进行了策略封装,让资源可以根据用户传入的类型自动选择资源实现类。

这就是下面要讲的,Spring中自动根据资源地址,选择资源实现类的利器:ResourceLoader接口体系。

ResourceLoader接口体系

  • ResourceLoader接口,顾名思义,设计的目的是为了快速返回(也就是加载)Resource实例的对象,其接口体系类图如下:
  • 在上图的类图当中,我们可以看到几个熟悉的面孔——ApplicationContext,上下文容器。这说明高级容器(应用上下文容器)也是实现了ResourceLoader接口的,其本身就是一个ResourceLoader,也就是说高级容器都可以根据资源地址类型快速获取对应的Resource实例。
  • 资源获取策略的顶级接口——ResourceLoader接口

Resource主要提供了获取资源实例和ClassLoader的方法,其源码如下:

public interface ResourceLoader {

	// 支持 classpath: 类型的路径匹配
	String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;

	// 根据路径信息返回一个对应的Resource资源实例
	Resource getResource(String location);

	// 获取当前使用的ClassLoader,对外暴露,让ResourceLoader可以使用自定义的ClassLoader
	@Nullable
	ClassLoader getClassLoader();

}
  • ResourceLoader接口的默认实现类——DefaultResourceLoader

DefaultResourceLoader类实现了ResourceLoader接口加载资源的方法,同时也扩展了一些通用的基本操作方法,其中最重要的就是实现了getResource(String location);方法,其实现逻辑源码如下:

	@Override
	public Resource getResource(String location) {
		Assert.notNull(location, "Location must not be null");
		// ProtocolResolver:用户自定义协议资源解决策略,看看用户是否有提前自定义
		for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
			Resource resource = protocolResolver.resolve(location, this);
			if (resource != null) {
				return resource;
			}
		}
		// 如果用户没有自定义策略,就用以下自定义资源解析策略
		// 如果是"/"打头,就构造并返回ClassPathResource
		if (location.startsWith("/")) {
			return getResourceByPath(location);
		}
		else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
			// 以"classpath:"打头也会构造并返回ClassPathResource在构造资源时还会获取类加载器
			return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
		}
		else {
			// 如果上述方式无法构造。则构造URL地址,并尝试通过URL进行资源定位,若没有就抛出异常
			// 然后判断是否为FileURL,如果是就返回FileUrlResource,否则就构造UrlResource
			// 若是在加载的过程中抛出MalformedURLException,就委派getResourceByPath来实现资源定位和加载
			try {
				// 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) {
				// 不符合URL规范就当做普通路径处理
				return getResourceByPath(location);
			}
		}
	}
  • ResourceLoader接口的增强——ResourcePatternResolver接口

Spring在提供了ResourceLoader接口之后,还提供了一个对其进行了增强的ResourcePatternResolver接口来扩展ResourceLoader。除了继承自Resource的方法外,ResourcePatternResolver额外扩充了一个方法,用来批量加载Resource资源对象,其源码如下:

public interface ResourcePatternResolver extends ResourceLoader {

	// 支持classpath*:形式路径匹配,即Ant风格
	String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
	
	// 批量加载Resource资源类型的实现
	Resource[] getResources(String locationPattern) throws IOException;

}

可以看出,ResourcePatternResolver支持了"classpath*:"开头的资源路径形式,即Ant风格路径,允许使用通配符来对路径进行匹配。"classpath*:"可以返回路径下所有满足条件的资源实例,而ResourceLoader本身只能一次返回一个资源。

  • ResourcePatternResolver接口的默认实现类——PathMatchingResourcePatternResolver

PathMatchingResourcePatternResolver中实例化了一个DefaultResourceLoader,继承来的ResourceLoader中的方法,就委托给了内部的DefaultResourceLoader对象去处理,而PathMatchingResourcePatternResolver只负责处理实现ResourcePatternResolver中的方法,主要是实现getResources(String locationPattern)方法。

PathMatchingResourcePatternResolver中还引入了一个新的组件PathMatcherPathMatcher负责对对基于字符串的路径和指定的模式符号进行匹配。

翻译一下这个接口的名字,可以将其翻译为路径匹配模板解释器,顾名思义,这个接口就是先用模板解释器对路径进行解析,分解成多个资源配置文件,将资源信息提供给资源加载器,后者根据不同策略将配置文件形成不同类型的资源(Resource)。

  • 高级容器和ResourceLoader之间微妙的关系:实现了ResourceLoader接口的ApplicationContext体系

关于高级容器的分析可以看这一篇:BeanFactory和ApplicationContext容器家族

ApplicationContext的源码中,我们可以看到,这个接口确实继承了ResourcePatternResolver接口,也就是ApplicationContext本身也是个模板解释器和资源加载器,是模板解释器的具体实现,是支持Ant风格路径匹配和批量加载资源的一个资源加载器。

作为高级容器的顶级接口,ApplicationContext继承了ResourcePatternResolver接口,这也就代表了高级容器下面整个体系都间接地继承了ResourcePatternResolver接口。

例如ApplicationContext的抽象实现类AbstractApplicationContext在实现ConfigurableApplicationContext的基础上,同时还继承了ResourceLoader的实现类DefaultResourceLoader

其实实现ConfigurableApplicationContext也就意味着实现了ResourcePatternResolver,但是Spring开发人员显然不想将资源解释的逻辑直接暴露在容器中,于是继承了DefaultResourceLoader,将解析逻辑与容器的实现进行了解耦。

AbstractApplicationContext的构造函数源码中可以看到,在这里将ResourcePatternResolver接口的实例——PathMatchingResourcePatternResolver实例进行了初始化,并且传入的resourceLoader实例,就是容器本身(容器继承了DefaultResourceLoader),也就是将容器进行了献祭,来实现资源解释器。

所以,Resource、ResourceLoader和容器之间的关系可以用下图来表示:

至此,Spring中Resource、ResourceLoader体系和作用已经讲解完毕,水平有限,有错误烦请指出。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java对象的结构与对象在内存中的结构

    当我们在Java中使用new这个指令创建一个对象的时候,对象的创建到底经过了什么样的一个过程呢?

    星如月勿忘初心
  • Java后端面试学习知识总结——JVM

    JVM就是Java虚拟机,Java的跨平台机制就是建立在强大的Java虚拟机的基础上。Java是一种先编译后解释型的语言,当我们写了一段Java代码,在运行之前...

    星如月勿忘初心
  • Spring框架源码脉络分析(一):IoC与容器、Bean和BeanDefinition

    IoC:即控制反转机制。在Spring中的实现表现为IoC容器,属于Spring Core模块最核心的部分。

    星如月勿忘初心
  • 经典面试题-构造方法注入和设值注入有什么区别?

    cwl_java
  • 手把手教你在NVIDIA Jetson Xavier上安装Deepstream 3.0!

    今天栏主向大家介绍了如何在NVIDIA Jetson Xavier上安装Deepstream 3.0.

    GPUS Lady
  • Nginx学习日志(三)配置SSL证书(网站由http转成https)

    Nginx学习日志(一)简单入门 Nginx学习日志(二)通过反向代理将不同域名映射到不同的端口

    海加尔金鹰
  • 机器学习人工学weekly-2018/4/15

    注意下面很多链接需要科学上网,无奈国情如此 1. DeepMind的新工作,不用地图在城市里导航 Learning to navigate in cities ...

    windmaple
  • 针对云原生转型的6个关键数据策略

    静一
  • 肠道菌群数据库

    肠道菌群在最近的几年也开始成为了我们的研究热点。对于肠道菌群的研究,基本上就是先通过宏基因组测序来分析不同分组之间的肠道菌群的变化。之前刚开始研究的时候,都是自...

    医学数据库百科
  • 架构设计的"贫血模型"与"充血模型" 原

        以前就听别人说过这俩种模型。它们描述的对象是面向对象设计中的实体,构建领域模型(Domain model)时,贫血模型与充血模型给出了俩种不同的方案:

    克虏伯

扫码关注云+社区

领取腾讯云代金券