揭开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源码的辅助工具罢了,至于能走多远还是要靠你自己的努力!

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

相关推荐
风象南2 分钟前
SpringBoot 自研「轻量级 API 防火墙」:单机内嵌,支持在线配置
后端
刘一说14 分钟前
CentOS 系统 Java 开发测试环境搭建手册
java·linux·运维·服务器·centos
Victor35619 分钟前
Redis(14)Redis的列表(List)类型有哪些常用命令?
后端
Victor35620 分钟前
Redis(15)Redis的集合(Set)类型有哪些常用命令?
后端
卷福同学21 分钟前
来上海三个月,我在马路边上遇到了阿里前同事...
java·后端
bingbingyihao2 小时前
多数据源 Demo
java·springboot
在努力的前端小白7 小时前
Spring Boot 敏感词过滤组件实现:基于DFA算法的高效敏感词检测与替换
java·数据库·spring boot·文本处理·敏感词过滤·dfa算法·组件开发
bobz9659 小时前
小语言模型是真正的未来
后端
一叶飘零_sweeeet9 小时前
从繁琐到优雅:Java Lambda 表达式全解析与实战指南
java·lambda·java8
DevYK10 小时前
企业级 Agent 开发实战(一) LangGraph 快速入门
后端·llm·agent