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

时间:2022-07-22
本文章向大家介绍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体系和作用已经讲解完毕,水平有限,有错误烦请指出。