揭开Spring资源加载的秘密(二)——透彻剖析资源加载器设计精髓


思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。

作者:毅航😜


揭开Spring资源加载的秘密(一)------资源的统一化表示中,我们详细的对Spring中的资源的统一表示进行了分析。总的来说Spring内部在对资源进行处理时,会将资源抽象为二进制流的形式,并在此基础上在对资源进行抽象为Resource接口。而本文我们将对Spring中的资源加载器ResourceLoader进行分析。

前言

在介绍Spring的资源加载之前,我们首先对Spring中资源加载的关键类进行一个基本介绍。在Spring内部,与资源加载相关的类主要有两个,一个是我们之前介绍过的Resoure接口,其主要对资源统一化表示。另一个重要的接口就是ResourceLoader,该接口内部如下所示:

java 复制代码
public interface ResourceLoader {

   Resource getResource(String location);

   ClassLoader getClassLoader();

}

不难发现, ResourceLoader 接口包含两个关键方法:getResourcegetClassLoader。其中 getResource 方法用于获取指定位置的资源,并返回一个 Resource 对象。而getClassLoader() 方法用于获取与此资源加载器关联的类加载器。

对于类加载器你可会感到陌生,不过也不要慌。这里获取类加载器的主要作用在于获得与当前 ResourceLoader 相关联的类加载器,以便顺利进行资源加载的相关操作

知晓了ResourceLoader 的相关作用后,接下来我们来对Spring中的默认资源加载器DefaultResourceLoader进行分析。

Spring中的默认资源加载器

DefaultResourceLoader作为Spring中资源加载器的核心实现,它提供了一种从不同来源加载资源的机制。由于该类实现自ResourceLoader,因此如果要深入了解Spring中有关资源加载的相关逻辑,只需重点分析其内部的getResource方法进行分析即可。这里之所以不分析getClassLoader()方法,主要是因为其逻辑很简单,无非就是获取当前类的类加载罢了,逻辑非常简单,故不花费篇幅来进行分析。

接下来,我们来看DefaultResourceLoadergetResource方法实现的相关细节。其方法内部的逻辑大致如下:

java 复制代码
public Resource getResource(String location) {
   // <1> 判断资源协议信息,对资源协议进行解析
   for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
      Resource resource = protocolResolver.resolve(location, this);
      if (resource != null) {
         return resource;
      }
   }
   <2> 如果以/ 开头、则创建 ClassPathContextResource、其实也是一个 ClassPathResource 是他的子类、 类路径的资源

   if (location.startsWith("/")) {
      return getResourceByPath(location);
   }
   else if (location.startsWith(CLASSPATH_URL_PREFIX)) {

      // <3> 如果以 classpath 开头 则 创建ClassPathResource
      return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
   }
   else {
      try {
         // 尝试从网络与文件中获取
         URL url = new URL(location);
         return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
      }
     // 省略异常相关逻辑
   }
}

(注:这里有关类路径信息classpath:的描述在揭开Spring资源加载的秘密(一)------资源的统一化表示 中有详细描述,在此我们便不赘述)

getResource方法内部,其首先会对类内部ProtocolResolver进行遍历,并调用其resolve方法,如果可以返回一个Resource就不再继续执行。对于这里提到的ProtocolResolver我们有必要进行一下解释。

Spring内部ProtocolResolver 主要用于解析特定协议的资源。其主要作用是在资源加载过程中,根据指定的协议提供自定义的资源解析策略。 明白了这一点后,再回看<1>处代码的逻辑是不是就很清晰了?

其实<1>处代码的逻辑无非就是调用相关的资源解析器,并对资源进行解析,如果可以将传入的路径进行解析,就返回一个Resurce并结束遍历。

如果你再深入分析DefaultResourceLoader内部实现,你会发现DefaultResourceLoader内部似乎没有初始化protocolResolvers的地方。换句话,在DefaultResourceLoader创建时,并不会初始化protocolResolvers的内容。即如果我们如果不手动调用addProtocolResolver方法向protocolResolvers添加内容,那么protocolResolvers便不会有内容。

讲到这里,不知道你是否切实感受到了Spring内部在扩展性方面所作的设计。正如我们之前说的,对于资源我们常见的路径信息有classPath,url,file:等几种不同的类型,但我们谁都不确定未来的某一天是否会有一个新的资源加载地址。

如果我们做设计,对于这些新增的资源加载方式,我们大概率通过if-else来进行判断,然后每种决策对应不同的处理逻辑就能做到所谓的适配。这样虽然能解决问题,但是其实是违反代码开闭原则的,而Spring作为一款框架,其在设计上肯定要考虑开闭原则,尽量将修改范围缩小。

因此,其当发现某种情况无法进行资源加载后,其肯定不能通过在框架中加入if-else来进行判断。为此Spring内部将处理权交给我们,如果某一天我们真的希望从一些特殊的地方加载资源,我们只需集成protocolResolvers接口,并通过addProtocolResolver方法将其加入到DefaultResourceLoader中的protocolResolvers即可。或许或许

这样的设计巧妙吗?十分巧妙!更进一步,你能想到这是那种设计模式的应用吗?

这在某种程度上可以认为是责任链模式的扩展实现。而有关责任链相关的分析可参考笔者之前责任链模式在SpringMVC中的应用进行了解。

具体到这个例子来看,资源加载器DefaultResourceLoader内部可能持有多个protocolResolver,用于对不同的协议进行解析,每一种资源本身只能被一种 ProtocolResolver进行解析,而这里如果资源无法被 ProtocolResolver解析,则会继续交给下一个 ProtocolResolver进行解析。其最终的结果无非就是,解析为Resource 或是没有一个 ProtocolResolver 能处理这个请求并返回一个 Resource,这点其实很类似于SpringMVC中拦截器的设计。

说到这里不知道你是否有这样的感觉,即感觉到源码之间很多设计其实是相同的,只要你掌握一处的设计思想,很多类似的设计都能触类旁通!这其实就是我们读源码,研究框架的意义所在。我们并不是为了去造轮子,而是期待在阅读源码中潜移默化的提升我们的编码、设计的能力。

往后<2>、<3>处代码的逻辑其实也就非常简单了,大致逻辑无非就是判断资源前缀信息,如果资源路径以 classpath: 开头,它会创建一个 ClassPathResource 对象;如果以 file: 开头,则创建一个 FileSystemResource 对象。换句话,在默认情况下DefaultResourceLoader 初始化时就具备了对 classpath:URL 协议的资源加载支持。

总结

至此,我们也就对Spring内部的资源加载器有了一个宏观的认知,接下来我们来对本文内容进行一个简短的回顾和总结。首先,我们以Spring中资源处理为切入点,分析了Spring中的资源加载器的相关设计,并重点对其内部的DefaultResourceLoader进行了详细的分析。

总的来看,DefaultResourceLoader的工作原理大致如下:

  • 当请求加载资源时,DefaultResourceLoader 会根据资源的路径或标识符来确定资源类型,并创建相应类型的 Resource 实例。
  • 而这些 Resource 对象本身便提供了统一的方法来访问资源,即getInputStream()方法,这样无论的做法上可以接在存放在不同位置的资源。无论实际资源是位于文件系统、类路径还是网络上都可以进行加载。

至此,我们也就花费了两章的内容了来对Spring内部的资源加载进行了分析。Spring的源码浩如烟海,所以不要期望能通过一两篇文章便能掌握Spring框架的源码,笔者的文章一定程度上只能算作是帮你入门Spring源码的辅助工具罢了,至于能走多远还是要靠你自己的努力!

最后,希望文章对你有所帮助!

相关推荐
阿里技术1 小时前
1 行命令引发的 Go 应用崩溃
开发语言·后端·golang
m0_748244833 小时前
创建一个简单的spring boot+vue前后端分离项目
vue.js·spring boot·后端
yang_shengy4 小时前
【JavaEE】Spring(1)
java·后端·spring·java-ee
m0_748254885 小时前
芋道源码(无遮羞布版)Spring Boot 全景指南
java·spring boot·后端
千里马学框架7 小时前
安卓java端service如何在native进程进行访问-跨进程通讯高端知识
android·java·开发语言·安卓framework开发·车机·跨进程·安卓窗口系统
程序研7 小时前
适配器模式
java·设计模式
NULL->NEXT8 小时前
Java(面向对象进阶——接口)
android·java·开发语言
步、步、为营8 小时前
解锁新技能:Windows Forms与ASP.NET API的梦幻联动
windows·后端·asp.net
雨 子8 小时前
Spring Boot 日志
java·spring boot·后端·log4j
技术的探险家8 小时前
R语言的文件操作
开发语言·后端·golang