2-XML 配置文件读取——2-1 准备工作及外层处理

Spring 读取 XML 的机制

综述

本文主要记录 Spring 读取 XML 并完成 BeanDefinition 注册的一些基本步骤。

介绍思路

我们从一个简单的例子开始走,逐步完成对整个流程的记录。

源码解析

总览

public class MyTest {
    public static void main(String[] args){
        XmlBeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource(""));
        beanFactory.getBean("");
    }
}

以上是一个基本的基于 XML 的 Spring 框架使用配置方法。我们从 XmlBeanFactory开始入手,首先需要了解XmlBeanFactory的继承关系:

1.png

其中,各接口定义的主要功能罗列如下:

对别名注册相关操作

  • AliasRegistry: 定义对 alias 的简单增删改查
  • BeanDefinitionRegistry:定义 bd 注册相关操作

对单例相关操作

  • SingletonBeanRegistry: 定义对单例 Bean 的注册、获取

Bean 工厂相关操作:

  • BeanFactory: 定义获取 Bean 及 Bean 的各种属性
  • ListableBeanFactory: 定义中增加按条件获得相关 bean 配置的接口
  • HierarchicalBeanFactory: 定义中增加对 BeanFactory 分层的支持【parentFactory
  • AutowireCapableBeanFactory: 定义中增加对 FactoryBean 的创建、自动注入、初始化、调用注册在 Factory中的后处理器钩子的支持

综合:

  • ConfigurableBeanFactory:结合分层Bean工厂和单例注册的功能。提供支持单例、原型生命周期的 Bean 工厂,并提供一些其他的 Factory 配置属性
  • ConfigurableListableBeanFactory: 定义中支持多种生命周期,支持Bean创建、自动注入、初始化、后处理器钩子,按条件筛选 Factory中注册的bean

上面仅介绍了各个接口的功能定义,总体来说思路还是比较清晰的:

分别定义了 Factory 工厂、别名相关、单例注册相关接口,然后各自完善信息,最后由一两个接口开始陆续将功能集合起来。

我们接下来看 class 类的功能扩展,class 通过继承来得到父类的逻辑,同时通过实现接口,增加对接口功能的支持,得益于Java的单继承,这条线还是非常清晰的。

2.png

XmlBeanFactory内部原理

private final XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this);

XmlBeanFactory在内部创建了一个 XmlBeanDefinitionReader类实例,并将自身传给它,在调用构造函数时,会委托 reader读取xml文件并将BeanDefinition保存至XmlBeanFactory中,等待后面调用getBean()进行实例化。

public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {
        super(parentBeanFactory);
        this.reader.loadBeanDefinitions(resource);
}

解析 XML 之前的准备工作

XmlBeanFactory将读取 XML 并注册的工作交给了 XmlBeanDefinitionReaderloadBeanDefinitions(),我们接下来看看他的逻辑。

其实XmlBeanDefinitionReader本身不进行 xml 文件的读取,它将 xml 文件的解析委托给了 JDK 的xml读取框架,它的主要作用是在拿到了解析之后的Document节点树之后进行解析并生成对应的 BeanDefinition

loadBeanDefinitions(Resource)

/**
 * Load bean definitions from the specified XML file.
 *
 * @param resource the resource descriptor for the XML file
 * @return the number of bean definitions found
 * @throws BeanDefinitionStoreException in case of loading or parsing errors
 */
@Override
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
  return loadBeanDefinitions(new EncodedResource(resource));
}

Spring 考虑到逻辑的模块划分,往往不会在一个函数中做过多的逻辑操作,每个函数都只做属于自己的一部分逻辑,如此使程序的可读性大大提升,且后续增加条件限制、进行代码复用会更加方便。

此处主要对 Resource进行了一层包转,以避免在读配置内容时因编码问题引起的乱码。

loadBeanDefinitions(EncodedResource)

/**
 * Load bean definitions from the specified XML file.
 * 此方法向外暴露,仅进行一些前置校验及处理工作,具体逻辑会委托给 doXXXX 函数
 *
 * @param encodedResource the resource descriptor for the XML file,
 *                        allowing to specify an encoding to use for parsing the file
 * @return the number of bean definitions found
 * @throws BeanDefinitionStoreException in case of loading or parsing errors
 */
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
  Assert.notNull(encodedResource, "EncodedResource must not be null");
  if (logger.isInfoEnabled()) {
    logger.info("Loading XML bean definitions from " + encodedResource);
  }

  Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
  if (currentResources == null) {
    currentResources = new HashSet<>(4);
    this.resourcesCurrentlyBeingLoaded.set(currentResources);
  }
  // currentResources 是线程安全的 Set 【不考虑反射啥的极端情况】,添加失败的原因是该对象已经存在,所以无法添加
  // 所以,如果添加失败,说明这个正在被此线程解析时被第二次解析了,有循环引用
  if (!currentResources.add(encodedResource)) {
    throw new BeanDefinitionStoreException(
      "Detected cyclic loading of " + encodedResource + " - check your import definitions!");
  }
  try {
    // 获得输入流
    InputStream inputStream = encodedResource.getResource().getInputStream();
    try {
      // 用 SAX 框架的 InputSource 包一下,方便后面直接调用该框架进行 xml 格式文件的解析
      // 详见 https://www.cnblogs.com/nerxious/archive/2013/05/03/3056588.html
      // 思路还是很简单的,就是把 xml 的那些 "闭合标签 + 属性" 翻译成了一棵用数据结构表示的树
      InputSource inputSource = new InputSource(inputStream);
      if (encodedResource.getEncoding() != null) {
        inputSource.setEncoding(encodedResource.getEncoding());
      }
      // 将具体实现委托给 doXXXXX
      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();
    }
  }
}

此函数的思路很简单,和 Spring 在创建 Bean 实例的思路一样:

  • 先在创建前记录要开始读取资源文件
  • 从资源文件中读到输入流
  • 将输入流委托给 doLoadBeanDefinitions()进行解析、注册
  • 完成解析,删除之前对开始读文件的记录

在整个解析过程中都用resourcesCurrentlyBeingLoaded 进行了记录,也因此避免了循环加载。【注意,这里采用的 ThreadLocal我认为可能是因为在某些框架中,可能有多个上下文环境,各自注册自己的 Bean ,不同上下文可以重复个自创建个自的 Bean ,不会互相冲突;同时,同一个上下文是不允许循环依赖的,这样加载起来就没完没了了

doLoadBeanDefinitions(InputSource,Resource)

/**
 * 从指定的 xml 中加载 BeanDefinition
 *
 * @param inputSource 用来读取 xml 信息的输入流
 * @param resource    上面输入流的描述,我们在生成 BeanDefinition 时要把 Resource 一起放进去,这样
 *         在后面 Bean 生成实例出问题时方便排查,可能有些 BeanDefinition 还需要从 Resource 获得
 *         一些属性                   
 * @return 新创建的 BeanDefinition 数量
 * @throws BeanDefinitionStoreException in case of loading or parsing errors
 * @see #doLoadDocument
 * @see #registerBeanDefinitions
 */
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
  throws BeanDefinitionStoreException {
  try {
    // 将 xml 中的元素解析成 Document 对象中的数据
    Document doc = doLoadDocument(inputSource, resource);

    // 注册 bean
    return registerBeanDefinitions(doc, resource);
  } catch (BeanDefinitionStoreException ex) {
    throw ex;
  } catch (SAXParseException ex) {
    throw new XmlBeanDefinitionStoreException(resource.getDescription(),
                                              "Line " + ex.getLineNumber() + " in XML document from " + resource + " is invalid", ex);
  } catch (SAXException ex) {
    throw new XmlBeanDefinitionStoreException(resource.getDescription(),
                                              "XML document from " + resource + " is invalid", ex);
  } catch (ParserConfigurationException ex) {
    throw new BeanDefinitionStoreException(resource.getDescription(),
                                           "Parser configuration exception parsing XML from " + resource, ex);
  } catch (IOException ex) {
    throw new BeanDefinitionStoreException(resource.getDescription(),
                                           "IOException parsing XML document from " + resource, ex);
  } catch (Throwable ex) {
    throw new BeanDefinitionStoreException(resource.getDescription(),
                                           "Unexpected exception parsing XML document from " + resource, ex);
  }
}

整体思路明确:

  1. 使用第三方的 XML 框架,完成 XML 的解析
  2. 根据解析之后的树,完成 BeanDefinition 的生成、注册【当然,此处默认命名空间的 Spring 做了,也留出足够的配置空间给那些基于 Spring 的框架来自行定义命名控价、解析规则】

读取 XML 并转化成 DOM 树

上面我们发现最后做的只有两件事:读 XML 和生成 BeanDefinition。我们先看一下是怎么读取 XML 生成 DOM 树的。

Document doc = doLoadDocument(inputSource, resource);

此处没有像 Spring 往常那样继续玩一下 loadDocument()doLoadDocument()的函数分层,直接委托给了doLoadDocument()

doLoadDocument()

/**
 * 用指定的 documentLoader 读取 xml 文件
 *
 * @param inputSource  SAX 框架用来读 xml 的输入流
 * @param resource    xml 文件的信息
 * @return DOM 树
 * @throws Exception when thrown from the DocumentLoader
 * @see #setDocumentLoader
 * @see DocumentLoader#loadDocument
 */
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
  return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,getValidationModeForResource(resource), isNamespaceAware());
}

这里直接将读取操作委托给了 DocumentLoader,给我的感觉有点像适配器的样子,将复杂入参的接口中确定的入参配置好,仅将需要变化的入参暴露出去。

loadDocument()

这个是 DocumentLoader接口中定义的方法,由于 SAX 框架的调用是模版样式的,所以我们直接粘进来:

/**
 * 从 inputSource 中加载出一个 Document 
 *
 * @param inputSource    要解析成 DOM 树的输入流
 * @param entityResolver 这个是一些用来解析内部实体的解析器,为了那些基于 Spring 框架的其他框架定
 *                制自己的命名空间后需要有指定的解析器才能实现对应的逻辑
 * @param errorHandler   一个报警处理器,专为 SAX 设置
 * @param validationMode XML 的定义是 DTD 还是 XSD 【其实根据情况 XSD 居多】
 * @param namespaceAware SAX的一个参数,设置成 true ,解析出的 Document 会带着命名空间
 * @return the loaded {@link Document document}
 * @throws Exception if an error occurs
 */
@Override
public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
                             ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {

    DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
    if (logger.isDebugEnabled()) {
        logger.debug("Using JAXP provider [" + factory.getClass().getName() + "]");
    }
    DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
    return builder.parse(inputSource);
}


protected DocumentBuilderFactory createDocumentBuilderFactory(int validationMode, boolean namespaceAware)
        throws ParserConfigurationException {

    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    factory.setNamespaceAware(namespaceAware);

    if (validationMode != XmlValidationModeDetector.VALIDATION_NONE) {
        factory.setValidating(true);
        if (validationMode == XmlValidationModeDetector.VALIDATION_XSD) {
            // Enforce namespace aware for XSD...
            factory.setNamespaceAware(true);
            try {
                factory.setAttribute(SCHEMA_LANGUAGE_ATTRIBUTE, XSD_SCHEMA_LANGUAGE);
            } catch (IllegalArgumentException ex) {
                ParserConfigurationException pcex = new ParserConfigurationException(
                        "Unable to validate using XSD: Your JAXP provider [" + factory +
                                "] does not support XML Schema. Are you running on Java 1.4 with Apache Crimson? " +
                                "Upgrade to Apache Xerces (or Java 1.5) for full XSD support.");
                pcex.initCause(ex);
                throw pcex;
            }
        }
    }

    return factory;
}


protected DocumentBuilder createDocumentBuilder(DocumentBuilderFactory factory,
                                                @Nullable EntityResolver entityResolver, @Nullable ErrorHandler errorHandler)
        throws ParserConfigurationException {

    DocumentBuilder docBuilder = factory.newDocumentBuilder();
    if (entityResolver != null) {
        docBuilder.setEntityResolver(entityResolver);
    }
    if (errorHandler != null) {
        docBuilder.setErrorHandler(errorHandler);
    }
    return docBuilder;
}

都是一些模版代码,后面自己写的demo基本就明白了。

根据读取的 DOM 树进行 BeanDefinition 的生成、注册

上一节在介绍 Document doc = doLoadDocument(inputSource, resource);的内部逻辑,接下来了解后面的registerBeanDefinitions(doc, resource),本节介绍 BeanDefinition的生成。

/**
 * 生成 BeanDefinition ,并注册。
 * 返回注册前后的差值,也就是此次注册的 BeanDefinition 个数
 *
 * @param doc      the DOM document
 * @param resource the resource descriptor (for context information)
 * @return the number of bean definitions found
 * @throws BeanDefinitionStoreException in case of parsing errors
 * @see #loadBeanDefinitions
 * @see #setDocumentReaderClass
 * @see BeanDefinitionDocumentReader#registerBeanDefinitions
 */
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
    BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
    int countBefore = getRegistry().getBeanDefinitionCount();
    documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
    return getRegistry().getBeanDefinitionCount() - countBefore;
}

createBeanDefinitionDocumentReader()

protected BeanDefinitionDocumentReader createBeanDefinitionDocumentReader() {
  return BeanUtils.instantiateClass(this.documentReaderClass);
}

创建一个BeanDefinitionDocumentReader实例,将 Document 解析成对应的 BD 。至于每次都重新创建的原因,猜测是因为这个类内部有状态。

createReaderContext()

public XmlReaderContext createReaderContext(Resource resource) {
  return new XmlReaderContext(resource, this.problemReporter, this.eventListener,
                              this.sourceExtractor, this, getNamespaceHandlerResolver());
}

创建 BeanDefinition 创建的上下文,主要用于将此次创建所需的资源、XmlBeanDefinitionReader的相关监听器传过去。由于 resource每次都不一样,所以每次都创建新的XmlReaderContext实例是可以理解的。

注意一个点,在创建实例时将 this穿进去了,而在本实例中由有对XmlBeanFactory的引用,也因此可以实现将创建的 BD 注册到对应的Factory中。

documentReader.registerBeanDefinitions()

public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
  this.readerContext = readerContext;
  logger.debug("Loading bean definitions");
  Element root = doc.getDocumentElement();
  doRegisterBeanDefinitions(root);
}

思路很清晰,设置上下文,然后从根节点开始调用 doRegisterBeanDefinitions()其实,根据我们的理解,每次新的上下文【resource不同】都会新创建 documentReader实例,我们直接在createBeanDefinitionDocumentReader()时也就是初始化时将上下文放进去,不是更加简洁么。。。。

doRegisterBeanDefinitions()具体逻辑

/**
 * 对 root 下的所有 符合条件的 DOM 节点进行处理,生成 BD 并注册
 */
protected void doRegisterBeanDefinitions(Element root) {
  //  根节点应该是 <beans />标签,根节点下也可以有 <beans />标签,标签的嵌套也意味着此函数的递归调用。
  //  
  //  为了保证递归之后在进行调用时仍能获得标签父节点的配置,我们使用 this.delegate 进行记录,先在操作前
  //  保存当前值,然后该怎么用怎么用,最后再归位。在单线程的情况下完成了整个切换,原理是借助函数调用栈。
  //
  // 保存之前的
  BeanDefinitionParserDelegate parent = this.delegate;
  // 生成自己的
  this.delegate = createDelegate(getReaderContext(), root, parent);

  // 如果是默认的命名空间,就配置一下
  if (this.delegate.isDefaultNamespace(root)) {
    String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE); //得到 xml 配置的 profile
    if (StringUtils.hasText(profileSpec)) { // 如果有值就需要判断一下
      //根据分隔符分开
      String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
        profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
      // profile 环境满足,就继续生成 BD并注册,否则直接跳过整个 <beans /> 标签
      if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
        if (logger.isInfoEnabled()) {
          logger.info("Skipped XML bean definition file due to specified profiles [" + profileSpec +
                      "] not matching: " + getReaderContext().getResource());
        }
        return;
      }
    }
  }

  // 钩子
  // 可以用来重写来将非标准的节点转化成标准的节点
  preProcessXml(root);
  
  parseBeanDefinitions(root, this.delegate);
  // 钩子
  // 可以用来重写来将非标准的节点转化成标准的节点
  postProcessXml(root);

  // 归位成之前的
  this.delegate = parent;
}

主要是对 <beans />标签的 profile进行一下条件判断,看是否需要注册、解析,并调用钩子对 xml 生成的节点进行一些处理工作。

parseBeanDefinitions(root, this.delegate)

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)) {
         parseDefaultElement(ele, delegate);
       } else {
         delegate.parseCustomElement(ele);
       }
     }
   }
 } else {
   delegate.parseCustomElement(root);
 }
}

对传入的节点及子节点 根据命名空间选择合适的解析方法。如果是默认命名空间就使用parseDefaultElement(),否则使用parseCustomElement()

到此,我们完成了 Spring 的 从 XML 配置的外层解析,后买呢我们会对默认命名空间的解析和自定义命名控价的解析专门介绍。

扩展:一些在读源码时遇到的数据结构

Resource

Resource是 Spring 创建的一个资源定位接口,Spring 没有直接使用 Java 中的 URL 框架,因为该框架实现机制比较复杂,提供的方法也不够全面。大概看一下,熟悉 Resource接口中的几个方法即可,不必看实现。

具体参见专门介绍这个的文章。

BeanDefinition

存储 bean 的定义,有问题参考专门介绍的文章。

DocumentLoader

此接口只有一个实现类,里面使用 SAX 框架进行了 xml 文件的解析。

扩展: SAX 框架了解

这个博客记录了一些 Java 读入 XML 文件的东西。就是 API 而已。不再赘述。

https://blog.csdn.net/jxufecodelong/article/details/16591369

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,122评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,070评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,491评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,636评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,676评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,541评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,292评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,211评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,655评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,846评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,965评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,684评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,295评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,894评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,012评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,126评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,914评论 2 355

推荐阅读更多精彩内容