【修炼内功】[spring-framework] [2] BeanDefinitionReader

本文已收录【修炼内功】跃迁之路

spring-framework.jpg

林中小舍.png

写在最前~

距spring-framework开篇的那篇文章已经一个月了,如果再照这样的速度下去,这个flag估计大概率又要呵呵~
最近发生了一些事情让我迷茫于应该坚持什么,为什么还要痴迷于工作两三年本就应该掌握的东西上~
‘年’(夕兽)就要来了,总要准备点儿什么才能有资本‘除夕’不是~
言归正传,切入正题!

上一篇介绍了Spring中的Resource,一个类似于Linux中“一切皆文件”的概念,屏蔽了对不同类型资源操作的差异性,本篇继续深入,但一定没有各位想象或者期待中的那么深

平时接触Spring一定离不开两个基本的核心概念:容器和控制反转,然而这两个概念都不是这篇文章要展开讨论的;平时使用Spring都是从各种ApplicationContext开始(如最常见的ClassPathXmlApplicationContext或者SpringBoot中的SpringApplication),然而,ApplicationContext也不是本篇的重点~(大写调皮,后文详解)

在使用Spring的过程中,我们会(通过xml <bean class='xxx'></bean>、注解@Bean@Component等等方式)创建大量的<u>bean</u>,众所周知,bean是被容器所管理的,bean之间的依赖注入依靠的是控制反转,那在我们使用bean之前,其是如何被注册到容器中的呢?

要解释这个问题其实很简单:step 1 解析;step 2 注册。本篇,我们将重心放在bean的解析上(但不包含所有方式的解析)

Spring在注册bean之前的解析方式有很多种,但无外乎三大类:1. 文件形式的配置;2. 注解形式的配置;3. 硬编码方式。本篇只谈第一类中的一种形式 -- xml配置文件的解析

为什么是xml这种千年老古董,因为市面上的书从来都只讲xml配置的解析啊(手动调皮),以上为玩笑(以下严肃脸),因为xml文件对于理解bean的解析更为容易,在注解大行其道的当下,希望我能坚持到某一天来撰写一篇文章解释注解在bean解析、注册上的运行机制

除了一些老项目(不可抗拒的历史原因)或者特殊场景依然在使用xml配置外,我相信大多数的程序猿都是向前看的,毕竟Spring(Boot)也在力推零xml配置

Absolutely no code generation and no requirement for XML configuration.

https://docs.spring.io/spring...

所以,我希望各位不要将重点放在如何解析xml配置上,而是将其作为“药引子”,引出其背后更为重要的概念(BeanDefinitionBeanFactoryBeanRegistry

基于xml配置的ApplicationContext常见的有ClassPathXmlApplicationContextFileSystemXmlApplicationContext(见名识意),基于xml配置的BeanFactory则是XmlBeanFactory(ApplicationContext与BeanFactory的关系会放到后文中),而所有这些在解析bean的时候使用的都是XmlBeanDefinitionReader (具体如何使用会在以后的篇幅中介绍,本篇请将重点暂时放在BeanDefinitionReader上)

XmlBeanDefinitionReader实现自BeanDefinitionReader,而在BeanDefinitionReader的实现里,除了XmlBeanDefinitionReader之外还有PropertiesBeanDefinitionReaderGroovyBeanDefinitionReader,一个解析properties文件、一个解析groovy脚本,由于过于小众、原理相通,此处不表(如果诸君有兴趣及能力,可以开发自己的YamlBeanDefinitionReaderKotlinBeanDefinitionReader等也未必不可)

Beans.BeanDefinitionReader

看似复杂的一张类关系图,理解起来其实并不复杂

解析

BeanDefinitionReader定义了一系列bean初始化的接口,以下列出几个比较直观的

public interface BeanDefinitionReader {
  // 加载单个配置文件
  int loadBeanDefinitions(String location) throws BeanDefinitionStoreException;
  int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException;
  
  // 加载多个配置文件
  int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException;
  int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException;
}

接口中定义了针对单个配置文件及多个配置文件的加载方法

Q:类名明明是Reader,但方法名为什么是load而不是parse(加载和解析貌似不是一个概念)?

AbstractBeanDefinitionReader实现了公共的处理逻辑

XmlBeanDefinitionReader在解析的过程中主要借助了两种接口,DocumentLoaderBeanDefinitionDocumentReader

  • DocumentLoader主要负责xml的验证和读取(默认实现为DefaultDocumentLoader
  • BeanDefinitionDocumentReader主要负责Document的解析和注册(默认实现为DefaultBeanDefinitionDocumentReader

Beans.Active.XmlBeanDefinitionReader

如果觉得上图过于简单、不够细节,可以参考下图(由于Spring源码的复杂性,依然不能覆盖所有的细节)

Beans.Sequence.XmlBeanDefinitionReader

  • 橙色 部分为验证、读取xml(Document、Element)
  • 蓝色 部分为Document的解析(BeanDefinitionBeanDefinitionHolder
  • 绿色 部分为bean的注册(BeanDefinitionRegistryAliasRegistry

以上,引出了几个重要的新概念(BeanDefinitionBeanDefinitionHolderBeanFactoryBeanRegistry),客官莫急,待我慢慢道来

XML的验证和读取

xml的验证和读取可以跟踪到代码XmlBeanDefinitionReader#doLoadDocument

protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
    return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
            getValidationModeForResource(resource), isNamespaceAware());
}

documentLoader.loadDocument内部实现则是常规的xml文件到Document的读取逻辑,这里简要介绍一下EntityResolver

一般Spring配置文件如下

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
                        http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="" class="">
        <property name="" value=""/>
        <property name="" ref=""/>
    </bean>
</beans>

在解析之前需要对xml的内容进行校验,校验需要依赖XSD或DTD声明文件,如上http://www.springframework.or...(浏览器中可直接打开)

这里有一个问题,基于Spring的应用不可能全部运行在可连通互联网的环境中,如果没有网络环境,基于xml配置的Spring初始化均会失败

EntityResolver的作用是提供一个寻找本地XSD或DTD声明文件的方法,具体的过程不再详解,可以跟到XmlBeanDefinitionReader#getEntityResolver查看

spring-beans的声明文件均可在spring-beans模块中找到

至此,可在离线的环境中获取XSD或DTD文件,以验证并解析xml配置文件,为后续bean的解析做足准备

Document的解析

Document的解析、注册主要使用了BeanDefinitionDocumentReader(由DefaultBeanDefinitionDocumentReader#doRegisterBeanDefinitions跟入)

Profiles

在进行bean的解析之前,首先需要匹配==profiles==

什么是profiles?

https://docs.spring.io/spring...

Spring Profiles provide a way to segregate parts of your application configuration and make it be available only in certain environments.

简单来讲,我们可以为不同的环境设置不同的配置,有选择性的让Spring加载

如何使用profiles?

xml文件中

<beans profile="dev"></beans>

SpringBoot properties文件名中

application-dev.properties

SpringBoot yaml文件中

server:
    port: 80
---
spring:
    profiles: dev
server:
    port: 8080

注解中

@Bean
@Profile({"dev"})
public MyBean myDevBean() {
    return new MyDevBean();
}

@Bean
@Profile({"production"})
public MyBean myProductionBean() {
    return new MyProductionBean();
}

等等方式...

如何激活profile?

java -jar xxx.jar -Dspring.profiles.active=dev

profile的配置会被读取并记录在Environment中,在进行bean的解析之前,第一步需要判断当前beans的profile是否与Environment中记录的profile相匹配,只有匹配的才会被加载,并进入解析、注册流程,这也便做到了配置的环境隔离(具体的处理过程参见DefaultBeanDefinitionDocumentReader#doRegisterBeanDefinitions

解析

在spring-beans模块的范畴内,默认的命名空间只包含四种标签:<beans><bean><alias><import>,但在实际使用过程中,我们用到的远远不止这三种,比如<tx:advice><aop:config>等等,这些标签的命名空间并不在==beans==内,前者的命名空间为==tx==,XSD文件在spring-tx.jar!org/springframework/transaction/config/spring-tx.xsd3,后者的命名空间为==aop==,XSD文件在spring-tx.jar!org/springframework/aop/config/spring-aop.xsd4

除了默认命名空间内的标签外,其他命名空间的标签都是以一种扩展(自定义)的形式存在

// DefaultBeanDefinitionDocumentReader.java
/**
 * Parse the elements at the root level in the document:
 * "import", "alias", "bean".
 * @param root the DOM root element of the document
 */
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);
    }
}

无论默认标签还是自定义标签,实际的标签解析过程均由BeanDefinitionParserDelegate提供

默认命名空间(标签)解析

默认命名空间的标签无外乎<bean><alias><import>(以及<beans>嵌套)

// DefualtBeanDefinitionDocumentReader.java
private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
    if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
        // 解析 <import>
        importBeanDefinitionResource(ele);
    }
    else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
        // 解析 <alias>
        processAliasRegistration(ele);
    }
    else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
        // 解析 <bean>
        processBeanDefinition(ele, delegate);
    }
    else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
        // 递归解析 <beans> 嵌套
        doRegisterBeanDefinitions(ele);
    }
}

对于<import>标签,解析出location,处理占位符,递归调用loadBeanDefinitions进行解析注册

对于<alias>标签,解析出bean的namealias,并注册(AliasRegistry

对于<bean>标签,情况则会复杂很多,但也无外乎<bean> 属性解析、<constructor-arg>解析、<property>解析等等

具体的解析细节不再赘述,大家可以自行查看源码

// BeanDefinitionParserDelegate#parseBeanDefinitionElement
try {
    // 创建BeanDefinition
    AbstractBeanDefinition bd = createBeanDefinition(className, parent);

    // <bean>属性
    parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);
    // <description>
    bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT));

    // <meta>
    parseMetaElements(ele, bd);
    // <lookup-method>
    parseLookupOverrideSubElements(ele, bd.getMethodOverrides());
    // <replaced-method>
    parseReplacedMethodSubElements(ele, bd.getMethodOverrides());

    // <constructor-arg>
    parseConstructorArgElements(ele, bd);
    // <property>
    parsePropertyElements(ele, bd);
    // <qualifier>
    parseQualifierElements(ele, bd);

    bd.setResource(this.readerContext.getResource());
    bd.setSource(extractSource(ele));

    return bd;
}

标签解析之后的结果存放到了那里?BeanDefinition

BeanDefinition只是bean的定义,存放构造该bean实例所需要的元信息,其中包含你能想到的一切有关bean的属性信息

// BeanDefinition.java
public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement {
    // class name
     String getBeanClassName();
    
    // 构造函数
    ConstructorArgumentValues getConstructorArgumentValues();

    // 属性
    MutablePropertyValues getPropertyValues();

    // 初始化的方法名
    String getInitMethodName();
    
    // 销毁的方法名
    String getDestroyMethodName();
    
    // ...
}

除此之外,还有很多的辅助类,如

  • RuntimeBeanReference存储ref信息
  • TypedStringValue存储value信息
  • ManagedMap存储<map>信息
  • ManagedList储存<list>信息
  • 等等

至此就结束了么?当然不是,我们在配置bean的时候一般都会使用id来指定beanName,但如果没有指定id呢?

<bean id="myBean" name="aliasBean" class=""></bean>

此时,则会使用BeanNameGenerator(默认DefaultBeanNameGenerator)生成默认的beanName(参见BeanDefinitionParserDelegate#parseBeanDefinitionElement

随后,会将beanDefinition、beanName及别名alias一同包装进BeanDefinitionHolder

BeanDefinitionHolder的定义只是简单捆绑了以上三者的关系

public class BeanDefinitionHolder implements BeanMetadataElement {
    // bean definition
    private final BeanDefinition beanDefinition;

    // bean name
    private final String beanName;

    // 别名alias
    private final String[] aliases;
}

最后,将该BeanDefinition进行注册(BeanDefinitionRegistry

// DefaultBeanDefinitionDocumentReader.java
protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
    // 解析BeanDefinitionHolder
    BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
    if (bdHolder != null) {
        // 注册BeanDefinition
        BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
        // 发送事件
        getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
    }
}

自定义命名空间(标签)解析

Spring提供了一种能力,来扩展并自定义xml配置文件中的标签

自定义标签的实现需要依赖:

  • XSD文件,用来描述(验证)自定义标签
  • BeanDefinitionParser实现,用来解析自定义标签
  • NamespaceHandlerSupport实现,用来注册自定义标签解析器
  • spring.handlersspring.schemas,用来发现自定义标签注册器(NamespaceHandlerSupport)及XSD文件

BeanDefinitionParser的定义同样很简单,将一个Document下的Element解析为BeanDefinition(s),并注册

public interface BeanDefinitionParser {
    BeanDefinition parse(Element element, ParserContext parserContext);
}

component-scan为例,其命名空间为context

BeanDefinitionParser的实现类为ComponentScanBeanDefinitionParser

public class ComponentScanBeanDefinitionParser implements BeanDefinitionParser {
    @Override
    @Nullable
    public BeanDefinition parse(Element element, ParserContext parserContext) {
        String basePackage = /* 解析basePackage */;
        // 扫描BeanDefinition
        ClassPathBeanDefinitionScanner scanner = configureScanner(parserContext, element);
        Set<BeanDefinitionHolder> beanDefinitions = scanner.doScan(basePackages);
        // 注册BeanDefinition
        registerComponents(parserContext.getReaderContext(), beanDefinitions, element);

        return null;
    }
}

其中定义了如何扫描指定package下的类,并根据注解注册bean的逻辑

这里有一个有趣的发现,通过配置文件注册bean的实现类通常为xxxBeanDefinitionLoader,而通过注解注册bean的实现类通常为xxxBeanDefinitionScanner

NamespaceHandlerSupport的实现类为ContextNamespaceHandler

public class ContextNamespaceHandler extends NamespaceHandlerSupport {
    @Override
    public void init() {
        // ...
        // 注册<component-scan>的解析器
        registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser());
        // ...
    }
}

spring.handlers文件部分内容为

http\://www.springframework.org/schema/context=org.springframework.context.config.ContextNamespaceHandler

spring.schemas文件部分内容为

http\://www.springframework.org/schema/context/spring-context.xsd=org/springframework/context/config/spring-context.xsd

同理,可以找到<tx:advice><aop:config>等标签的解析逻辑

回到BeanDefinitionParserDelegate,细心的会发现,对于自定义标签的解析逻辑,并没有出现注册的地方

// BeanDefinitionParserDelegate.java
public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
    // 获取命名空间
    String namespaceUri = getNamespaceURI(ele);
    // 获取解析器
    NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
    // 解析(Q: 并注册?)
    return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}

对于大多数自定义标签而言,其都是自注册的,即在BeanDefinitionParser.parse中完成注册的逻辑,如<component-scan>,那对于<tx:advice>之类的标签是如何完成自注册的?AbstractBeanDefinitionParser!

// AbstractBeanDefinitionParser
public abstract class AbstractBeanDefinitionParser implements BeanDefinitionParser {
    @Override
    @Nullable
    public final BeanDefinition parse(Element element, ParserContext parserContext) {
        // Element解析为BeanDefinition
        AbstractBeanDefinition definition = parseInternal(element, parserContext);
        // 解析id及aliases,包装为BeanDefinitionHolder    
        BeanDefinitionHolder holder = new BeanDefinitionHolder(definition, id, aliases);
        // 注册!!!
        registerBeanDefinition(holder, parserContext.getRegistry());
        // 发送事件
        if (shouldFireEvents()) {
            BeanComponentDefinition componentDefinition = new BeanComponentDefinition(holder);
            postProcessComponentDefinition(componentDefinition);
            parserContext.registerComponent(componentDefinition);
        }
        return definition;
    }
}

AbstractBeanDefinitionParser实现了类似于ComponentScanBeanDefinitionParser的逻辑,在解析出BeanDefinition后随即将其注册

注册

上文中多次提到了注册

  • 解析<alias>标签并注册
  • 解析<bean>标签并注册
  • 解析自定义标签并注册
注册的具体细节希望能和bean的初始化及获取放到一起来讲,本篇简单介绍并引出几个概念

bean的注册由xxxBeanDefinitionRegistry实现,并不是注册bean的实例对象,而是注册bean的元数据BeanDefinition
别名的注册由xxxAliasRegistry实现
bean的获取由xxxBeanFactory实现(包括bean的初始化、依赖注入等)

BeanDefinition的实现有多种,各中关系后续文章详解

Beans.BeanDefinition

xxxBeanDefinitionRegistryxxxBeanFactory的实现也是错综复杂,篇幅有限,统一放到后续文章中讲解

Beans.ListableBeanFactory

小结

  • Spring bean的注册大体分为两步:解析;注册
  • Spring bean的解析方式有很多种,但无外乎三大类:文件配置;注解配置;硬编码
  • XmlBeanDefinitionReader负责xml配置的bean解析及注册

    • DocumentLoader负责xml文件的校验及加载
    • BeanDefinitionDocumentReader负责bean的解析及注册
  • xml中的标签分为两大类,默认标签及自定义标签,其解析逻辑不同

    • 所有标签的解析由BeanDefinitionParserDelegate提供统一入口
    • Spring提供了扩展xml自定义标签的能力,需要实现BeanDefinitionParserNamespaceHandlerSupport
  • bean的元数据被存放在BeanDefinition中,并同beanName、别名alias封装在BeanDefinitionHolder

    • bean的注册由xxxBeanDefinitionRegistry实现
    • 别名的注册由xxxAliasRegistry实现

问题遗留点

  • bean及别名的注册是如何实现的
  • bean的获取(初始化、依赖注入)是如何实现的
  • 使用xml配置文件可以加载基于注解配置的bean(如<component-scan>),使用注解同样可以加载基于xml配置的bean(如@Import({"classpath:xxx.xml"})),两者之间是如何相融合的
  • bean的注册都伴随事件发送(ReaderContext#fireXXX),事件在哪里使用
写在最后~

2019结束了,希望2020能有新的突破!


订阅号


  1. https://github.com/spring-pro...
  2. https://github.com/spring-pro...
  3. https://github.com/spring-pro...
  4. https://github.com/spring-pro...
阅读 472

推荐阅读
林中小舍
用户专栏

工作中的坑点及经验

51 人关注
41 篇文章
专栏主页