转载

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

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

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

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

写在最前~

距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 等也未必不可)

【修炼内功】[spring-framework] [2] 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

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

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

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

  • 橙色 部分为验证、读取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模块中找到

  • spring-beans.xsd spring-beans.jar!/org/springframework/beans/factory/xml/spring-beans.xsd
  • spring-beans.dtd spring-beans.jar!/org/springframework/beans/factory/xml/spring-beans.dtd

至此,可在离线的环境中获取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.xsd ,后者的命名空间为==aop==,XSD文件在 spring-tx.jar!org/springframework/aop/config/spring-aop.xsd

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

// 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的实现有多种,各中关系后续文章详解

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

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

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

小结

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

    DocumentLoader
    BeanDefinitionDocumentReader
    
  • xml中的标签分为两大类,默认标签及自定义标签,其解析逻辑不同

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

    xxxBeanDefinitionRegistry
    xxxAliasRegistry
    

问题遗留点

  • bean及别名的注册是如何实现的
  • bean的获取(初始化、依赖注入)是如何实现的
  • 使用xml配置文件可以加载基于注解配置的bean(如 <component-scan> ),使用注解同样可以加载基于xml配置的bean(如 @Import({"classpath:xxx.xml"}) ),两者之间是如何相融合的

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

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

原文  https://segmentfault.com/a/1190000021458901
正文到此结束
Loading...