Spring 容器的初始化

Stella981
• 阅读 385

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 的默认实现

Spring 容器的初始化                                   defaultListableBeanF actory 类 图

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

XmlBeanDefinitionReader

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

Spring 容器的初始化 XmlBeanDefinitionReader 类 图

DocumentLoader

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

Spring 容器的初始化                                          Documen tLoader 类 图

BeanDefinitionDocumentReader

读取 Document 并向 BeanDefinitionRegistry 注册

Spring 容器的初始化                                     BeanDefinitionDocumentReader

源码分析

Spring 容器的初始化

loadBeanDefinitions(Resource)

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

@Overridepublic 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 definitionint 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);   }  } }

这个方法的作用很简单、就是使用一开始我们传给 XmlBeanDefinitionReaderBeanDefinitionRegistry 对 bean 和 beanDefinition 的关系进行注册。并且也对 beanName 和 alias 的关系进行注册(这里是对 bean 标签中配置的 id 和 name 属性关系进行配置)

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

@Nullable public BeanDefinitionHolder parseBeanDefinitionElement(Element ele) {  return parseBeanDefinitionElement(ele, null); }

@Nullablepublic 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 beanString 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 的这部分代码的

往期相关文章

Spring-资源加载(源码分析)

Spring-AliasRegistry

编译Spring5.2.0源码

Spring 容器的初始化

Spring 容器的初始化

本文分享自微信公众号 - CoderLi(CopyLi666)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Stella981 Stella981
2年前
Android So动态加载 优雅实现与原理分析
背景:漫品Android客户端集成适配转换功能(基于目标识别(So库35M)和人脸识别库(5M)),导致apk体积50M左右,为优化客户端体验,决定实现So文件动态加载.!(https://oscimg.oschina.net/oscnet/00d1ff90e4b34869664fef59e3ec3fdd20b.png)点击上方“蓝字”关注我
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Docker 部署SpringBoot项目不香吗?
  公众号改版后文章乱序推荐,希望你可以点击上方“Java进阶架构师”,点击右上角,将我们设为★“星标”!这样才不会错过每日进阶架构文章呀。  !(http://dingyue.ws.126.net/2020/0920/b00fbfc7j00qgy5xy002kd200qo00hsg00it00cj.jpg)  2
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这