本文已收录【修炼内功】跃迁之路
写在最前~距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.
所以,我希望各位不要将重点放在如何解析xml配置上,而是将其作为“药引子”,引出其背后更为重要的概念(BeanDefinition
、BeanFactory
、BeanRegistry
)
基于xml配置的ApplicationContext常见的有ClassPathXmlApplicationContext
、FileSystemXmlApplicationContext
(见名识意),基于xml配置的BeanFactory则是XmlBeanFactory
(ApplicationContext与BeanFactory的关系会放到后文中),而所有这些在解析bean的时候使用的都是XmlBeanDefinitionReader
(具体如何使用会在以后的篇幅中介绍,本篇请将重点暂时放在BeanDefinitionReader上)
XmlBeanDefinitionReader
实现自BeanDefinitionReader
,而在BeanDefinitionReader
的实现里,除了XmlBeanDefinitionReader
之外还有PropertiesBeanDefinitionReader
及GroovyBeanDefinitionReader
,一个解析properties文件、一个解析groovy脚本,由于过于小众、原理相通,此处不表(如果诸君有兴趣及能力,可以开发自己的YamlBeanDefinitionReader或KotlinBeanDefinitionReader等也未必不可)
看似复杂的一张类关系图,理解起来其实并不复杂
解析
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
在解析的过程中主要借助了两种接口,DocumentLoader
及BeanDefinitionDocumentReader
-
DocumentLoader
主要负责xml的验证和读取(默认实现为DefaultDocumentLoader
) -
BeanDefinitionDocumentReader
主要负责Document的解析和注册(默认实现为DefaultBeanDefinitionDocumentReader
)
如果觉得上图过于简单、不够细节,可以参考下图(由于Spring源码的复杂性,依然不能覆盖所有的细节)
- 橙色 部分为验证、读取xml(Document、Element)
-
蓝色 部分为Document的解析(
BeanDefinition
、BeanDefinitionHolder
) -
绿色 部分为bean的注册(
BeanDefinitionRegistry
、AliasRegistry
)
以上,引出了几个重要的新概念(BeanDefinition
、BeanDefinitionHolder
、BeanFactory
、BeanRegistry
),客官莫急,待我慢慢道来
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.xsd1
- spring-beans.dtd spring-beans.jar!/org/springframework/beans/factory/xml/spring-beans.dtd2
至此,可在离线的环境中获取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的name和alias,并注册(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.handlers
及spring.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的实现有多种,各中关系后续文章详解
xxxBeanDefinitionRegistry
与xxxBeanFactory
的实现也是错综复杂,篇幅有限,统一放到后续文章中讲解
小结
- Spring bean的注册大体分为两步:解析;注册
- Spring bean的解析方式有很多种,但无外乎三大类:文件配置;注解配置;硬编码
-
XmlBeanDefinitionReader
负责xml配置的bean解析及注册-
DocumentLoader
负责xml文件的校验及加载 -
BeanDefinitionDocumentReader
负责bean的解析及注册
-
-
xml中的标签分为两大类,默认标签及自定义标签,其解析逻辑不同
- 所有标签的解析由
BeanDefinitionParserDelegate
提供统一入口 - Spring提供了扩展xml自定义标签的能力,需要实现
BeanDefinitionParser
、NamespaceHandlerSupport
- 所有标签的解析由
-
bean的元数据被存放在
BeanDefinition
中,并同beanName、别名alias封装在BeanDefinitionHolder
里- bean的注册由
xxxBeanDefinitionRegistry
实现 - 别名的注册由
xxxAliasRegistry
实现
- bean的注册由
问题遗留点
- bean及别名的注册是如何实现的
- bean的获取(初始化、依赖注入)是如何实现的
- 使用xml配置文件可以加载基于注解配置的bean(如
<component-scan>
),使用注解同样可以加载基于xml配置的bean(如@Import({"classpath:xxx.xml"})
),两者之间是如何相融合的 - bean的注册都伴随事件发送(
ReaderContext#fireXXX
),事件在哪里使用
写在最后~2019结束了,希望2020能有新的突破!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。