SpringIOC源码解析(4)—— Resource、ResourceLoader、容器之间的微妙关系

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: Resource家族EncodedResourceAbstractResourceWritableResource根据资源地址自动选择正确的ResourceResourceLoaderResourceLoader 的使用者BeanDefinitionReader利器的使用者BeanDefinitionReader体系结构XmlBeanDefinitionReader

58.png


配置就是xml,spring将对物理资源的访问方式抽象成Resource。


Resource家族


Resource是个接口,继承了InputStreamSource,定义了资源的基本操作(全是读操作)


59.png


InputStreamSource有唯一一个方法getInputStream


60.png


主要是根据不同的资源,定义了不同类的实现。


ServletContextResource负责以相对于Web应用程序根目录的路径加载资源,支持以流或url的形式进行访问,在war包解压出来的情况下,也可以通过file的形式访问

ClassPathResource用于访问类加载路径下的资源,对于web应用来说,可以自动加载WEB-INF/classes目录下的资源文件,无需使用绝对路径访问

FileSystemResource用于访问文件系统资源,优势不明显,java的File类也可以做到

EncodedResource


主要实现对资源文件的编码处理,其具体的逻辑实现在getReader


61.png

当我们给资源设置了编码属性之后,Spring会使用相应的编码作为输入流的编码


AbstractResource


主要提供了Resource方法的大部分的默认公共实现,如果想要自定义Resource,不推荐直接继承Resource接口,而更应该继承这个抽象类。


WritableResource


FileSystemResource为了能实现写操作,继承了WritableResource,其中有返回输出流实例的方法

62.png


根据资源地址自动选择正确的Resource


强大的加载资源的方式:


自动识别"classpath:"、"file:"等资源地址前缀

支持自动解析Ant风格带通配符的资源地址


Ant:


路径匹配表达式,用来对URI进行匹配


?匹配任何单字符

*匹配0或者任意数量的字符

**匹配0或者更多的目录


63.png


ResourceLoader


实现不同的Resource加载策略,按需返回特定类型的Resource:


 是个接口,Resource getResource(String location);方法可以根据传入的location自动返回一个Resource实例(前面说的三个具体实现类)。


 还提供了ClassLoader getClassLoader();方法暴露出来类加载器


64.png


DefaultResourceLoader.java提供了ResourceLoader的实现,最关键的是getResource方法


// 获取Resource的具体实现类实例
@Override
public Resource getResource(String location) {
   Assert.notNull(location, "Location must not be null");
   // ProtocolResolver用户自定义协议资源解决策略
   // 有的话就拿过来用一下,去解析location(看用户是否提前指定好了根据不同的location去解析resource实例)
   for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
      Resource resource = protocolResolver.resolve(location, this);
      if (resource != null) {
         return resource;
      }
   }
   // 如果是以/开头,则构造ClassPathContextResource返回
   if (location.startsWith("/")) {
      return getResourceByPath(location);
   }
   // 若以classpath:开头,则构造 ClassPathResource 类型资源并返回,在构造该资源时,通过 getClassLoader()获取当前的 ClassLoader
   else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
      return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
   }
   else {
      // 构造URL,尝试通过它进行资源定位,若没有抛出MalformedURLException异常,
      //    判断是否为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) {
         // No URL -> resolve as resource path.
         // 返回的是ClassPathContextResource
         return getResourceByPath(location);
      }
   }
}


这里使用的是策略模式,Resource是策略接口,以及很多的实现子类是策略类,通过DefaultResourceLoader,决定返回的是哪个具体的策略(实现类)。策略模式需要用户明确知道策略,即什么样的资源用什么样的Resource,工厂模式用户不必知道细节。


ResourcePatternResolver提供了根据路径匹配模式,返回多个Resource实例,


Resource[] getResources(String locationPattern) throws IOException;

同时新增了一个协议前缀,该前缀由子类负责实现


String CLASSPATH_ALL_URL_PREFIX = "classpath*:";

 在继承关系表中可以发现ApplicationContext也间接实现了ResourceLoader接口


 AbstractApplicationContext继承了DefaultResourceLoader,并且里面有getResourcePatternResolver方法,用于生成PathMatchingResourcePatternResolver这个实例。


 这个方法是在 AbstractApplicationContext 的构造函数里用的,也就意味着AbstractApplicationContext 可以调用 ResourcePatternResolver 的 getResources 方法,来根据路径返回多个Resource实例。


 综上所述,AbstractApplicationContext 的实现完全可以支持ResourceLoader和ResourcePatternResolver,这也是高级容器为什么支持统一资源加载的原因。委托给了PathMatchingResourcePatternResolver 和 DefaultResourceLoader 来执行。


ResourceLoader 的使用者BeanDefinitionReader


利器的使用者


读取BeanDefinition

BeanDefinitionRegistry

 BeanDefinitionReader会利用ResourceLoader或者ResourcePatternResolver将配置信息解析成一个个的BeanDefinition,并最终借助BeanDefinitionRegistry的注册接口,将BeanDefinition给注册到容器里。


 BeanDefinitionReader定义了一系列加载BeanDefinition的接口,针对单个配置文件:


int loadBeanDefinitions(String location) throws BeanDefinitionStoreException;


针对多个配置文件:

int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException;

针对单个Resource资源实例:


int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException;


针对多个Resource资源实例:

int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException;


最终的目的就是将配置文件的配置转换成一个个的BeanDefinition

还有其他方法:

/**
 * 获取BeanDefinitionRegistry对象,这个类的主要作用将BeanDefinition注册到BeanDefinition的注册表中
 * @return
 */
BeanDefinitionRegistry getRegistry();


/**
 * Bean的名字生成器,为匿名bean生成一个名字,就是id
 * @return
 */
BeanNameGenerator getBeanNameGenerator();

BeanDefinitionReader体系结构

65.png


AbstractBeanDefinitionReader实现了公共处理逻辑,主要的一个方法是loadBeanDefinitions,方法内先获取ResourceLoader,再判断获取的实例是哪一个实例,如果是ResourcePatternResolver的话则代表需要加载多个资源,else里面是只加载单个资源,最终都会调用loadBeanDefinitions方法做进一步的加载。


public int loadBeanDefinitions(String location, @Nullable Set<Resource> actualResources) throws BeanDefinitionStoreException {
   // 获取资源加载器,主要的功能就是根据路径和类加载器获取Resource对象
   ResourceLoader resourceLoader = getResourceLoader();
   // 判断资源加载器是否为空
   if (resourceLoader == null) {
      throw new BeanDefinitionStoreException(
            "Cannot load bean definitions from location [" + location + "]: no ResourceLoader available");
   }
   // ResourcePatternResolver 用于加载多个文件或者能够加载Ant风格路径的文件资源
   if (resourceLoader instanceof ResourcePatternResolver) {
      // Resource pattern matching available.
      try {
         Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);
         int count = loadBeanDefinitions(resources);
         if (actualResources != null) {
            Collections.addAll(actualResources, resources);
         }
         if (logger.isTraceEnabled()) {
            logger.trace("Loaded " + count + " bean definitions from location pattern [" + location + "]");
         }
         return count;
      }
      catch (IOException ex) {
         throw new BeanDefinitionStoreException(
               "Could not resolve bean definition resource pattern [" + location + "]", ex);
      }
   }
   else {
      // Can only load single resources by absolute URL.
      // 加载单个文件资源
      // 直接使用ResouceLoader加载
      Resource resource = resourceLoader.getResource(location);
      int count = loadBeanDefinitions(resource);
      if (actualResources != null) {
         actualResources.add(resource);
      }
      if (logger.isTraceEnabled()) {
         logger.trace("Loaded " + count + " bean definitions from location [" + location + "]");
      }
      return count;
   }
}


XmlBeanDefinitionReader


从xml文件中读取bean definitions的方法

@Override
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
   return loadBeanDefinitions(new EncodedResource(resource));
}

又主要调用了上面的loadBeanDefinitions的if里面的逻辑


66.png


会根据location获取到Resource实例。


loadBeanDefinitions方法会先指定编码以解析xml资源:

new EncodedResource(resource)


 之后执行XmlBeanDefinitionReader的loadBeanDefinitions,这个方法返回一个int类型的数据,表示本次加载并注册到容器里面的bean definition的个数。


 为了支持多线程加载,加载是一个耗时过程,方法里面用到了resourcesCurrentlyBeingLoaded,是一个ThreadLocal类型的本地变量来存储正在加载的资源。(使用ThreadLocal修饰的变量只能当前线程对其修改,key是线程号,value是线程对应的资源)。

public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
   Assert.notNull(encodedResource, "EncodedResource must not be null");
   if (logger.isTraceEnabled()) {
      logger.trace("Loading XML bean definitions from " + encodedResource);
   }
   // 从本地线程变量中获取当前的正在加载的资源
   Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
   // 如果本地线程变量中不存在正在加载的资源,那么将其添加进去
   if (currentResources == null) {
      currentResources = new HashSet<>(4);
      this.resourcesCurrentlyBeingLoaded.set(currentResources);
   }
   // 如果encodedResource添加进入currentResources失败,表明其中已经存在这个资源,只不过没有加载完成
   if (!currentResources.add(encodedResource)) {
      throw new BeanDefinitionStoreException(
            "Detected cyclic loading of " + encodedResource + " - check your import definitions!");
   }
   try {
      // 获取文件的输入流
      InputStream inputStream = encodedResource.getResource().getInputStream();
      try {
         // 封装成InputSource,其中指定了输入流和编码格式
         InputSource inputSource = new InputSource(inputStream);
         // 如果存在编码,那么将其添加进入InputSource中
         if (encodedResource.getEncoding() != null) {
            inputSource.setEncoding(encodedResource.getEncoding());
         }
         // 调用同类的方法继续解析
         return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
      }
      finally {
         // 关闭输入流
         inputStream.close();
      }
   }
   catch (IOException ex) {
      throw new BeanDefinitionStoreException(
            "IOException parsing XML document from " + encodedResource.getResource(), ex);
   }
   finally {
      currentResources.remove(encodedResource);
      if (currentResources.isEmpty()) {
         this.resourcesCurrentlyBeingLoaded.remove();
      }
   }
}


b621f56a3f2e492ab28e99c9433e8fb7.png

相关文章
|
23天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
62 2
|
11天前
|
负载均衡 网络协议 算法
Docker容器环境中服务发现与负载均衡的技术与方法,涵盖环境变量、DNS、集中式服务发现系统等方式
本文探讨了Docker容器环境中服务发现与负载均衡的技术与方法,涵盖环境变量、DNS、集中式服务发现系统等方式,以及软件负载均衡器、云服务负载均衡、容器编排工具等实现手段,强调两者结合的重要性及面临挑战的应对措施。
29 3
|
24天前
|
存储 安全 Linux
Golang的GMP调度模型与源码解析
【11月更文挑战第11天】GMP 调度模型是 Go 语言运行时系统的核心部分,用于高效管理和调度大量协程(goroutine)。它通过少量的操作系统线程(M)和逻辑处理器(P)来调度大量的轻量级协程(G),从而实现高性能的并发处理。GMP 模型通过本地队列和全局队列来减少锁竞争,提高调度效率。在 Go 源码中,`runtime.h` 文件定义了关键数据结构,`schedule()` 和 `findrunnable()` 函数实现了核心调度逻辑。通过深入研究 GMP 模型,可以更好地理解 Go 语言的并发机制。
|
1月前
|
消息中间件 缓存 安全
Future与FutureTask源码解析,接口阻塞问题及解决方案
【11月更文挑战第5天】在Java开发中,多线程编程是提高系统并发性能和资源利用率的重要手段。然而,多线程编程也带来了诸如线程安全、死锁、接口阻塞等一系列复杂问题。本文将深度剖析多线程优化技巧、Future与FutureTask的源码、接口阻塞问题及解决方案,并通过具体业务场景和Java代码示例进行实战演示。
46 3
|
2月前
|
缓存 前端开发 JavaScript
前端的全栈之路Meteor篇(二):容器化开发环境下的meteor工程架构解析
本文详细介绍了使用Docker创建Meteor项目的准备工作与步骤,解析了容器化Meteor项目的目录结构,包括工程准备、环境配置、容器启动及项目架构分析。提供了最佳实践建议,适合初学者参考学习。项目代码已托管至GitCode,方便读者实践与交流。
|
2月前
|
存储 应用服务中间件 云计算
深入解析:云计算中的容器化技术——Docker实战指南
【10月更文挑战第14天】深入解析:云计算中的容器化技术——Docker实战指南
71 1
|
2月前
|
存储
让星星⭐月亮告诉你,HashMap的put方法源码解析及其中两种会触发扩容的场景(足够详尽,有问题欢迎指正~)
`HashMap`的`put`方法通过调用`putVal`实现,主要涉及两个场景下的扩容操作:1. 初始化时,链表数组的初始容量设为16,阈值设为12;2. 当存储的元素个数超过阈值时,链表数组的容量和阈值均翻倍。`putVal`方法处理键值对的插入,包括链表和红黑树的转换,确保高效的数据存取。
59 5
|
2月前
|
Java Spring
Spring底层架构源码解析(三)
Spring底层架构源码解析(三)
135 5
|
2月前
|
XML Java 数据格式
Spring IOC容器的深度解析及实战应用
【10月更文挑战第14天】在软件工程中,随着系统规模的扩大,对象间的依赖关系变得越来越复杂,这导致了系统的高耦合度,增加了开发和维护的难度。为解决这一问题,Michael Mattson在1996年提出了IOC(Inversion of Control,控制反转)理论,旨在降低对象间的耦合度,提高系统的灵活性和可维护性。Spring框架正是基于这一理论,通过IOC容器实现了对象间的依赖注入和生命周期管理。
76 0
|
2天前
|
监控 NoSQL 时序数据库
《docker高级篇(大厂进阶):7.Docker容器监控之CAdvisor+InfluxDB+Granfana》包括:原生命令、是什么、compose容器编排,一套带走
《docker高级篇(大厂进阶):7.Docker容器监控之CAdvisor+InfluxDB+Granfana》包括:原生命令、是什么、compose容器编排,一套带走
117 77

推荐镜像

更多