Spring 容器的初始化

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: 读完这篇文章你将会收获到• 了解到 Spring 容器初始化流程• ThreadLocal 在 Spring 中的最佳实践• 面试中回答 Spring 容器初始化流程

读完这篇文章你将会收获到

  • 了解到 Spring 容器初始化流程
  • ThreadLocal 在 Spring 中的最佳实践
  • 面试中回答 Spring 容器初始化流程


引言


我们先从一个简单常见的代码入手分析

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN" "https://www.springframework.org/dtd/spring-beans-2.0.dtd">
<beans>
 <bean class="com.demo.data.Person">
  <description>
   微信搜一搜:CoderLi(不妨关注➕一下?这次一定?)
  </description>
 </bean>
</beans>
复制代码
public static void main(String[] args) {
  Resource resource = new ClassPathResource("coderLi.xml");
  DefaultListableBeanFactory defaultListableBeanFactory = new DefaultListableBeanFactory();
  XmlBeanDefinitionReader xmlBeanDefinitionReader = new XmlBeanDefinitionReader(defaultListableBeanFactory);
  xmlBeanDefinitionReader.loadBeanDefinitions(resource);
 }
复制代码

上面这段 Java 代码主要做了

  • 资源的获取(定位)
  • 创建一个 beanFactory
  • 根据 beanFactory (实现了 BeanDefinitionRegistry 接口) 创建一个 beanDefinitionReader
  • 装载资源并 registry 资源里面的 beanDefinition

所以总体而言就是资源的加载、加载、注册三个步骤

  • 对于资源的加载可以看看我另一篇文章 Spring-资源加载(源码分析)
  • 加载的过程则是将 Resource 对象转为一系列的 BeanDefinition 对象
  • 注册则是将 BeanDefinition 注入到 BeanDefinitionRegistry 中


组件介绍


在分析源码流程之前我们一起先对一些重要的组件混个眼熟


DefaultListableBeanFactory

defaultListableBeanFactory 是整个 bean 加载的核心部分,是 bean 注册及加载 bean 的默认实现

defaultListableBeanFactory 类图

对于 AliasRegistry 可以参考我另一篇文章 Spring-AliasRegistry 。关于这个类我们只要记住两点,一个是它是一个 beanFactory、一个是它是一个 BeanDefinitionRegistry


XmlBeanDefinitionReader

从 XML 资源文件中读取并转换为 BeanDefinition 的各个功能

XmlBeanDefinitionReader 类图


DocumentLoader

对 Resource 文件进行转换、将 Resource 文件转换为 Document 文件

DocumentLoader 类图


BeanDefinitionDocumentReader

读取 Document 并向 BeanDefinitionRegistry 注册

BeanDefinitionDocumentReader


源码分析

loadBeanDefinitions(Resource)

XmlBeanDefinitionReader#loadBeanDefinitions(Resource) 我们先从这个入口方法开始进去

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

EncodedResource 是 Resource 的子类, Spring-资源加载(源码分析)

public class EncodedResource implements InputStreamSource {
 private final Resource resource;
 @Nullable
 private final String encoding;
 @Nullable
 private final Charset charset;
..........
..........
  public Reader getReader() throws IOException {
  if (this.charset != null) {
   return new InputStreamReader(this.resource.getInputStream(), this.charset);
  }
  else if (this.encoding != null) {
   return new InputStreamReader(this.resource.getInputStream(), this.encoding);
  }
  else {
   return new InputStreamReader(this.resource.getInputStream());
  }
 }
}
复制代码

只是一个简单的 Wrapper 类,针对不同的字符集和字符编码返回不一样的 Reader


loadBeanDefinitions(EncodedResource)

public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
  // 从Thread Local 中获取正在加载的的资源
  Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
  // 判断这个资源是否已经加载过了、主要是为了是否是 资源的循环依赖 import
  if (!currentResources.add(encodedResource)) {
   throw new BeanDefinitionStoreException("");
  }
  try (InputStream inputStream = encodedResource.getResource().getInputStream()) {
   InputSource inputSource = new InputSource(inputStream);
   // 有encode 就设置进去
   if (encodedResource.getEncoding() != null) {
    inputSource.setEncoding(encodedResource.getEncoding());
   }
   // 真正的加载
   return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
  }
  catch (IOException ex) {
   throw new BeanDefinitionStoreException("");
  }
  finally {
   // ThreadLocal的最佳实践
   currentResources.remove(encodedResource);
   if (currentResources.isEmpty()) {
    this.resourcesCurrentlyBeingLoaded.remove();
   }
  }
 }
复制代码


首先从 ThreadLocal 中获取正在加载的 Resource,这里主要是检查 import 标签带来的循环引用问题。

从这里我们可以看到在 finally 中,对已经完成加载的资源进行移除,并且检查 Set 是否还有元素了,如果没有则直接调用 ThreadLocalremove 方法。这个就是 ThreadLocal 的最佳实践了,最后的 remove 方法的调用可以避免 ThreadLocal 在 ThreadLocalMap 中作为 WeakReference 而带来的内存泄露问题。

这个方法里基本做啥事情、最主要的事情就是调用了 doLoadBeanDefinitions 这个方法,而这个方法才是真正干活的。(在 Spring 中,很有意思的是、真正干活的方法前缀都是带有 do 的,这个可以留意下)


doLoadBeanDefinitions(InputSource  Resource)

// 获取 document 对象
Document doc = doLoadDocument(inputSource, resource);
// 注册 bean definition
int count = registerBeanDefinitions(doc, resource);
return count;
复制代码

doLoadDocument 这个方法就是将 Resource 转化为 Document,这里涉及到 xml 文件到验证,建立对应的 Document Node ,使用到的就是上面提及到的 DocumentLoader 。这个不展开来探讨。

我们直接进入到 registerBeanDefinitions 方法中


registerBeanDefinitions(Document,Resource)

public int registerBeanDefinitions(Document doc, Resource resource)  {
  // 创建一个 bean definition 的 reader
  BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
  // 注册之前已经有的 bean definition 的个数 return this.beanDefinitionMap.size();
  int countBefore = getRegistry().getBeanDefinitionCount();
  documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
  return getRegistry().getBeanDefinitionCount() - countBefore;
 }
复制代码


上面代码中出现了一个我们提及到的 BeanDefinitionDocumentReader 组件,他的功能就是读取 Document 并向 BeanDefinitionRegistry 注册

public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
   this.readerContext = readerContext;
   doRegisterBeanDefinitions(doc.getDocumentElement());
}
复制代码


这里又来了、do 才是真正干活的大哥

protected void doRegisterBeanDefinitions(Element root) {
   BeanDefinitionParserDelegate parent = this.delegate;
   this.delegate = createDelegate(getReaderContext(), root, parent);
   if (this.delegate.isDefaultNamespace(root)) {
      // 处理 profiles
      String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
      if (StringUtils.hasText(profileSpec)) {
         String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
               profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
         if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
            return;
         }
      }
   }
   // 解释前的处理 这里默认为空实现、子类可以覆盖此方法在解释 Element 之前做些事情
   preProcessXml(root);
   // 解释
   parseBeanDefinitions(root, this.delegate);
   // 解释后处理 这里默认为空实现
   postProcessXml(root);
   this.delegate = parent;
}
复制代码


这里主要的方法就是 parseBeanDefinitions

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
   if (delegate.isDefaultNamespace(root)) {
      NodeList nl = root.getChildNodes();
      for (int i = 0; i < nl.getLength(); i++) {
         Node node = nl.item(i);
         if (node instanceof Element) {
            Element ele = (Element) node;
            if (delegate.isDefaultNamespace(ele)) {
               // spring 默认标签解释
               parseDefaultElement(ele, delegate);
            } else {
               // 自定义 标签解释
               delegate.parseCustomElement(ele);
            }
         }
      }
   } else {
      delegate.parseCustomElement(root);
   }
}
复制代码


Spring 的默认标签有 import , beans , bean , alias

private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
   // 解释 import 标签
   if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
      importBeanDefinitionResource(ele);
   } else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
      processAliasRegistration(ele);
   } else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
      processBeanDefinition(ele, delegate);
   } else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
      doRegisterBeanDefinitions(ele);
   }
}
复制代码


解释 import 标签调用 importBeanDefinitionResource 最终会调用到我们最开始处理 Resource 循环依赖的那个方法 loadBeanDefinitions

我们直接进入到 processAliasRegistration 方法中

protected void processAliasRegistration(Element ele) {
   String name = ele.getAttribute(NAME_ATTRIBUTE);
   String alias = ele.getAttribute(ALIAS_ATTRIBUTE);
   boolean valid = true;
   if (!StringUtils.hasText(name)) {
      getReaderContext().error("Name must not be empty", ele);
      valid = false;
   }
   if (!StringUtils.hasText(alias)) {
      getReaderContext().error("Alias must not be empty", ele);
      valid = false;
   }
   if (valid) {
      try {
        // 最重要的一行代码
         getReaderContext().getRegistry().registerAlias(name, alias);
      } catch (Exception ex) {
         getReaderContext().error("Failed to register alias '" + alias +
               "' for bean with name '" + name + "'", ele, ex);
      }
      getReaderContext().fireAliasRegistered(name, alias, extractSource(ele));
   }
}
复制代码


最重要的一行代码就是将 name 和 alias 进行注册(这里注册的是 alias 标签中的 name 和 alias 之间的关系),可以参考这篇文章进行了解 Spring-AliasRegistry

我们来到最主要的 processBeanDefinition

protected void processBeanDefinition(Element ele,
BeanDefinitionParserDelegate delegate) {
  // 这里获得了一个 BeanDefinitionHolder 
  BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
  if (bdHolder != null) {
   bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
   try {
    BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
   } catch (BeanDefinitionStoreException ex) {
    .....
   }
   getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
  }
 }
复制代码


我们先分析 BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry()) 这句代码

public static void registerBeanDefinition(
   BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
   throws BeanDefinitionStoreException {
  // 注册 bean Name
  String beanName = definitionHolder.getBeanName();
  registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());
  // 注册 alias .
  String[] aliases = definitionHolder.getAliases();
  if (aliases != null) {
   for (String alias : aliases) {
    registry.registerAlias(beanName, alias);
   }
  }
 }
复制代码


这个方法的作用很简单、就是使用一开始我们传给 XmlBeanDefinitionReader

BeanDefinitionRegistry 对 bean 和 beanDefinition 的关系进行注册。并且也对 beanName 和 alias 的关系进行注册(这里是对 bean 标签中配置的 id 和 name 属性关系进行配置)

delegate.parseBeanDefinitionElement(ele) 我们再把眼光返回到这个方法、这个方法就是创建 BeanDefinition 的地方了

@Nullable
 public BeanDefinitionHolder parseBeanDefinitionElement(Element ele) {
  return parseBeanDefinitionElement(ele, null);
 }
复制代码
@Nullable
public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) {
   String id = ele.getAttribute(ID_ATTRIBUTE);
   String nameAttr = ele.getAttribute(NAME_ATTRIBUTE);
   List<String> aliases = new ArrayList<>();
   // 判断是否配置了 name 属性、对name 进行分割 
  // 在 bean 标签中 name 就是 alias 了
   if (StringUtils.hasLength(nameAttr)) {
      String[] nameArr = StringUtils.tokenizeToStringArray(...);
      aliases.addAll(Arrays.asList(nameArr));
   }
   String beanName = id;
   // 没有配置id 并且 alias 列表不为空、则选取第一个 alias 为 bean Name
   if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) {
      beanName = aliases.remove(0);
   }
   if (containingBean == null) {
     // 检查 beanName 和alias 的唯一性
      checkNameUniqueness(beanName, aliases, ele);
   }
   // 怎么生成一个BeanDefinition 尼
   AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean);
   if (beanDefinition != null) {
      // 如果 beanName 为 空
      if (!StringUtils.hasText(beanName)) {
         try {
            if (containingBean != null) {
               beanName = BeanDefinitionReaderUtils.generateBeanName(
                     beanDefinition, this.readerContext.getRegistry(), true);
            } else {
               // 没有配置 beanName 和 alias的话、那么这个类的第一个实例、将拥有 全类名的alias
               // org.springframework.beans.testfixture.beans.TestBean 这个是别名(TestBean#0 才拥有这个别名、其他的不配拥有)
               // org.springframework.beans.testfixture.beans.TestBean#0 这个是 beanName
               beanName = this.readerContext.generateBeanName(beanDefinition);
               String beanClassName = beanDefinition.getBeanClassName();
               if (beanClassName != null &&
                     beanName.startsWith(beanClassName) && beanName.length() > beanClassName.length() &&
                     !this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) {
                  aliases.add(beanClassName);
               }
            }
         } catch (Exception ex) {
           .........
         }
      }
      String[] aliasesArray = StringUtils.toStringArray(aliases);
      return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray);
   }
   // nothing
   return null;
}
复制代码

在 bean 标签中 name 属性对应的就是 alias ,id 属性对应的就是 beanName 了

当我们没有配置 id 属性但是配置了 name 属性、那么第一个 name 属性就会成为我们的 id


当我们既没有配置 id 属性 也没有配置 name 属性,那么 Spring 就会帮我们生成具体可看看 Spring-AliasRegistry


然后就创建了一个 BeanDefinitionHolder 返回了

上面的代码我们看到有这个关键的方法 parseBeanDefinitionElement(ele, beanName, containingBean) 这个方法生成了我们期待的 BeanDefinition ,但是里面的内容都是比较枯燥的

// 解释class 属性
String className = null;
if (ele.hasAttribute(CLASS_ATTRIBUTE)) {
  className = ele.getAttribute(CLASS_ATTRIBUTE).trim();
}
// 是否指定了 parent bean
String parent = null;
if (ele.hasAttribute(PARENT_ATTRIBUTE)) {
  parent = ele.getAttribute(PARENT_ATTRIBUTE);
}
  // 创建 GenericBeanDefinition
  AbstractBeanDefinition bd = createBeanDefinition(className, parent);
  // 解释各种默认的属性
  parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);
  // 提取describe
  bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT));
  // 解释元数据
  parseMetaElements(ele, bd);
  // look up 方法
  parseLookupOverrideSubElements(ele, bd.getMethodOverrides());
  // replacer
  parseReplacedMethodSubElements(ele, bd.getMethodOverrides());
  // 解析构造函数参数
  parseConstructorArgElements(ele, bd);
  // 解释property子元素
  parsePropertyElements(ele, bd);
  // 解释qualifier
  parseQualifierElements(ele, bd);
  bd.setResource(this.readerContext.getResource());
  bd.setSource(extractSource(ele));
复制代码

都是去解析 bean 标签里面的各种属性

那么我们整个 Spring 容器初始化流程就介绍完了


总结


public static void main(String[] args) {
  Resource resource = new ClassPathResource("coderLi.xml");
  DefaultListableBeanFactory defaultListableBeanFactory = new DefaultListableBeanFactory();
  XmlBeanDefinitionReader xmlBeanDefinitionReader = new XmlBeanDefinitionReader(defaultListableBeanFactory);
  xmlBeanDefinitionReader.loadBeanDefinitions(resource);
 }
复制代码
  1. 调用 XmlBeanDefinitionReader 的方法 loadBeanDefinitions
  2. 将 Resource 包裹成 EncodeResource
  3. 通过 ThreadLocal 判断是否 Resource 循环依赖
  4. 使用 DocumentLoader 将 Resource 转换为 Document
  5. 使用 BeanDefinitionDocumentReader 解释 Document 的标签
  6. 解释 Spring 提供的默认标签/自定义的标签解释
  • 解释 import 标签的时候会回调到步骤2中
  • 解释 alias 标签会向 AliasRegistry 注册
  • 解释 bean 标签会向 BeanDefinitionRegistry 注册 beanName 和 BeanDefinition ,也会注册 bean 标签里面 id 和 name 的关系(其实就是 alias )


大致的流程就是如此了,面试的时候大致说出 XmlBeanDefinitionReader,DocumentLoader,BeanDefinitionDocumentReader,BeanDefinitionRegistry,AliasRegistry 这几个组件,面试官大概率会认为你是真的看过 Spring 的这部分代码的



目录
相关文章
|
4月前
|
XML Java 数据格式
Spring5入门到实战------7、IOC容器-Bean管理XML方式(外部属性文件)
这篇文章是Spring5框架的实战教程,主要介绍了如何在Spring的IOC容器中通过XML配置方式使用外部属性文件来管理Bean,特别是数据库连接池的配置。文章详细讲解了创建属性文件、引入属性文件到Spring配置、以及如何使用属性占位符来引用属性文件中的值。
Spring5入门到实战------7、IOC容器-Bean管理XML方式(外部属性文件)
|
4月前
|
缓存 Java 数据库连接
Spring Boot奇迹时刻:@PostConstruct注解如何成为应用初始化的关键先生?
【8月更文挑战第29天】作为一名Java开发工程师,我一直对Spring Boot的便捷性和灵活性着迷。本文将深入探讨@PostConstruct注解在Spring Boot中的应用场景,展示其在资源加载、数据初始化及第三方库初始化等方面的作用。
90 0
|
22天前
|
缓存 Java Spring
实战指南:四种调整 Spring Bean 初始化顺序的方案
本文探讨了如何调整 Spring Boot 中 Bean 的初始化顺序,以满足业务需求。文章通过四种方案进行了详细分析: 1. **方案一 (@Order)**:通过 `@Order` 注解设置 Bean 的初始化顺序,但发现 `@PostConstruct` 会影响顺序。 2. **方案二 (SmartInitializingSingleton)**:在所有单例 Bean 初始化后执行额外的初始化工作,但无法精确控制特定 Bean 的顺序。 3. **方案三 (@DependsOn)**:通过 `@DependsOn` 注解指定 Bean 之间的依赖关系,成功实现顺序控制,但耦合性较高。
实战指南:四种调整 Spring Bean 初始化顺序的方案
|
19天前
|
Kubernetes 容器 Perl
【赵渝强老师】K8s中Pod中的初始化容器
Kubernetes的Pod包含业务容器、基础容器、初始化容器和临时容器。初始化容器在业务容器前运行,用于执行必要的初始化任务。本文介绍了初始化容器的作用、配置方法及优势,并提供了一个示例。
|
2月前
|
Java 测试技术 Windows
咦!Spring容器里为什么没有我需要的Bean?
【10月更文挑战第11天】项目经理给小菜分配了一个紧急需求,小菜迅速搭建了一个SpringBoot项目并完成了开发。然而,启动测试时发现接口404,原因是控制器包不在默认扫描路径下。通过配置`@ComponentScan`的`basePackages`字段,解决了问题。总结:`@SpringBootApplication`默认只扫描当前包下的组件,需要扫描其他包时需配置`@ComponentScan`。
|
2月前
|
XML Java 数据格式
Spring IOC容器的深度解析及实战应用
【10月更文挑战第14天】在软件工程中,随着系统规模的扩大,对象间的依赖关系变得越来越复杂,这导致了系统的高耦合度,增加了开发和维护的难度。为解决这一问题,Michael Mattson在1996年提出了IOC(Inversion of Control,控制反转)理论,旨在降低对象间的耦合度,提高系统的灵活性和可维护性。Spring框架正是基于这一理论,通过IOC容器实现了对象间的依赖注入和生命周期管理。
73 0
|
3月前
|
XML Java 开发者
经典面试---spring IOC容器的核心实现原理
作为一名拥有十年研发经验的工程师,对Spring框架尤其是其IOC(Inversion of Control,控制反转)容器的核心实现原理有着深入的理解。
135 3
|
4月前
|
XML Java 数据格式
Spring5入门到实战------3、IOC容器-Bean管理XML方式(一)
这篇文章详细介绍了Spring框架中IOC容器的Bean管理,特别是基于XML配置方式的实现。文章涵盖了Bean的定义、属性注入、使用set方法和构造函数注入,以及如何注入不同类型的属性,包括null值、特殊字符和外部bean。此外,还探讨了内部bean的概念及其与外部bean的比较,并提供了相应的示例代码和测试结果。
Spring5入门到实战------3、IOC容器-Bean管理XML方式(一)
|
4月前
|
XML Java 数据格式
Spring5入门到实战------5、IOC容器-Bean管理(三)
这篇文章深入探讨了Spring5框架中IOC容器的高级Bean管理,包括FactoryBean的使用、Bean作用域的设置、Bean生命周期的详细解释以及Bean后置处理器的实现和应用。
Spring5入门到实战------5、IOC容器-Bean管理(三)
|
4月前
|
XML Java 数据格式
Spring5入门到实战------4、IOC容器-Bean管理XML方式、集合的注入(二)
这篇文章是Spring5框架的实战教程,主题是IOC容器中Bean的集合属性注入,通过XML配置方式。文章详细讲解了如何在Spring中注入数组、List、Map和Set类型的集合属性,并提供了相应的XML配置示例和Java类定义。此外,还介绍了如何在集合中注入对象类型值,以及如何使用Spring的util命名空间来实现集合的复用。最后,通过测试代码和结果展示了注入效果。
Spring5入门到实战------4、IOC容器-Bean管理XML方式、集合的注入(二)