likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 14

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


前言

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

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方法实现的相关细节。其方法内部的逻辑大致如下:


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));
      }
     // 省略异常相关逻辑
   }
}

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即可。或许或许

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

具体到这个例子来看,资源加载器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源码的辅助工具罢了,至于能走多远还是要靠你自己的努力!

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

转载自:https://juejin.cn/post/7314558811112013874
评论
请登录