《Spring源码深度分析》第3章 默认标签的解析

2023-11-17

前言

汇总:《Spring源码深度分析》持续更新中…

之前提到过 Spring 中的标签包括默认标签自定义标签两种,而两种标签的用法以及解析方式存在着很大的不同,本章节重点带领读者详细分析默认标签的解析过程。

一、Spring默认的四个标签

四个默认标签:

  1. beans
  2. import
  3. alias
  4. bean

二、bean标签的解析及注册

在4 种标签的解析中,对 bean 标签的解析最为复杂也最为重要,所以我们从此标签开始深入分析,如果能理解此标签的解析过程,其他标签的解析自然会迎刃而解。

1、BeanDefinition下的三个实现类

BeanDefinition 是一个接口,在 Spring 中存在三种实现:RootBeanDefinition、 ChildBeanDefinition 以及 GenericBeanDefinition。三种实现均继承 了 AbstractBeanDefiniton。

其中BeanDefinition 是配置文件<bean>元素标签在容器中的内部表示形式。<bean>元素标签拥有class、 scope、 lazy-init 等配置属性,BeanDefinition 则提供了相应的 beanClass、 scope、 lazyInit属性,BeanDefinition 和<bean>中的属性是一一对应的。其中 RootBeanDefinition 是最常用的实现类,它对应一般性的< bean>元素标签,GenericBeanDefinition 是自2.5 版本以后新加人的 bean文件配置属性定义类,是一站式服务类。

在配置文件中可以定义父< bean>和子<bean>,父用 RootBeanDefinition 表示,而子< bean>用 ChildBeanDefiniton 表示,而没有父< bean>的< bean>就使用 RootBeanDefinition 表示。
AbstractBeanDefinition 对两者共同的类信息进行抽象

Spring 通过 BeanDefinition 将配置文件中的<bean>配置信息转换为容器的内部表示,并将这些 BeanDefiniton 注册到 BeanDefinitonRegistry 中。Spring 容器的 BeanDefinitionRegistry 就像是 Spring 配置信息的内存数据库,主要是以 map 的形式保存,后续操作直接从 BeanDefinitionRegistry 中该取配置信息。它们之间的关系如图 3-2 所示。
在这里插入图片描述

2、解析BeanDefinition

1.processBeanDefinition

首先我们进入函数 processBeanDefinition(ele, delegate)。

	protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
		// 1、(重要)解析xml文件中的bean标签。把值赋予给BeanDefinitionHolder中的beanDefinition、beanName、aliases。
		BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
		if (bdHolder != null) {
			// 2、解析默认标签的自定义标签元素(BeanDefinition进行装饰)。场景:在默认的标签元素下,但是其中的'子元素标签'却使用了自定义的配置时,这句代码便会起作用了。
			bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
			try {
				// 3、Register the final decorated instance.
				BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
			}
			catch (BeanDefinitionStoreException ex) {
				getReaderContext().error("Failed to register bean definition with name '" +
						bdHolder.getBeanName() + "'", ele, ex);
			}
			// 4、发出响应事件,通知相关监听器,这个bean已经注册完成。这里的实现只为扩展,当程序开发人员需要对注册 BeanDefinition 事件进行监听时可以通过注册监听器的方式并将处理逻辑写人监听器中,目前在 Spring 中并没有对此事件做任何逻辑处理。
			getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
		}
	}

2.parseBeanDefinitionElement

我们先来分析下:processBeanDefinition方法中的:BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele); 进入BeanDefinitionDelegate类的parseBeanDefinitionElement方法:

	@Nullable
	public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) {
		// 对id属性进行解析
		String id = ele.getAttribute(ID_ATTRIBUTE);
		// 对name属性进行解析
		String nameAttr = ele.getAttribute(NAME_ATTRIBUTE);

		// 分割name属性,获取别名信息
		List<String> aliases = new ArrayList<>();
		if (StringUtils.hasLength(nameAttr)) {
			String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, MULTI_VALUE_ATTRIBUTE_DELIMITERS);
			aliases.addAll(Arrays.asList(nameArr));
		}

		// 将id设置成beanName
		String beanName = id;
		if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) {
			beanName = aliases.remove(0);
			if (logger.isTraceEnabled()) {
				logger.trace("No XML 'id' specified - using '" + beanName +
						"' as bean name and " + aliases + " as aliases");
			}
		}

		if (containingBean == null) {
			checkNameUniqueness(beanName, aliases, ele);
		}

		// 解析其他的属性(class、parent、singleton...) && child标签(lookup-method、replace-method、construct-arg、property、qualifier...) 并一致封装至GenericBeanDefinition类型实例中
		AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean);
		if (beanDefinition != null) {
			if (!StringUtils.hasText(beanName)) {
				try {
					// 如果不存在beanName,那么会根据spring默认的生成规则为该bean生成beanName。
					if (containingBean != null) {
						beanName = BeanDefinitionReaderUtils.generateBeanName(
								beanDefinition, this.readerContext.getRegistry(), true);
					}
					else {
						beanName = this.readerContext.generateBeanName(beanDefinition);
						// Register an alias for the plain bean class name, if still possible,
						// if the generator returned the class name plus a suffix.
						// This is expected for Spring 1.2/2.0 backwards compatibility.
						String beanClassName = beanDefinition.getBeanClassName();
						if (beanClassName != null &&
								beanName.startsWith(beanClassName) && beanName.length() > beanClassName.length() &&
								!this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) {
							aliases.add(beanClassName);
						}
					}
					if (logger.isTraceEnabled()) {
						logger.trace("Neither XML 'id' nor 'name' specified - " +
								"using generated bean name [" + beanName + "]");
					}
				}
				catch (Exception ex) {
					error(ex.getMessage(), ele);
					return null;
				}
			}
			String[] aliasesArray = StringUtils.toStringArray(aliases);
			return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray);
		}

		return null;
	}

当前层主要工作包括如下内容。
(1)提取元素中的id以及 name 属性。
(2)进一步解析其他所有属性并统一封装至 GenericBeanDefinition 类型的实例中。
(3) 如果检测到 bean 没有指定 beanName,那么使用默认规则为此 Bean 生成 beanName。
(4) 将获取到的信息封装到 BcanDefinitionHolder 的实例中。

3.AbstractBeanDefinition

至此我们便完成了对 XML 文档到 GenericBeanDefinition 的转换,也就是说到这里,XML中所有的配置都可以在 GenericBeanDefinition 的实例类中找到对应的配置

GenericBeanDefinition 只是子类实现,而大部分的通用属性都保存在了 AbstractBeanDefinition中,那么我们再次通过 AbstractBeanDefinition 的属性来回顾一下我们都解析了哪些对应的配置。

4.解析默认标签的自定义标签元素

场景:在默认的标签元素下,但是其中的’子元素标签’却使用了自定义的配置时,这句代码便会起作用了。
在这里插入图片描述

5.注册解析的 BeanDefinition

在这里插入图片描述

对于配置文件,解析也解析完了,装饰也装饰完了,对于得到的 beanDefinition 已经可以满足后续的使用要求了,唯一还剩下的工作就是注册了,也就是 processBeanDefinition 函数中的BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry()代码的解析了。该方法做了两件事情:

	public static void registerBeanDefinition(
			BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
			throws BeanDefinitionStoreException {

		// Register bean definition under primary name.
		String beanName = definitionHolder.getBeanName();
		//  1、注册BeanDefinition(DefaultListableBeanFactory中存储)。格式:Map<beanName,BeanDefinition>。
		registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());

		// 2、注册别名(AliasRegistry中存储)。格式:Map<alias,beanName>。
		String[] aliases = definitionHolder.getAliases();
		if (aliases != null) {
			for (String alias : aliases) {
				registry.registerAlias(beanName, alias);
			}
		}
	}

6.通知监听器解析及注册完成

通过代码 getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder))完成此工作,这里的实现只为扩展,当程序开发人员需要对注册 BeanDefinition 事件进行监听时可以通过注册监听器的方式并将处理逻辑写人监听器中,目前在 Spring 中并没有对此事件做任何逻辑处理。

三、alias标签的解析

在对 bean 进行定义时,除了使用 id 属性来指定名称之外,为了提供多个名称,可以使用alias 标签来指定

然而,在定义 bean 时就指定所有的别名并不是总是恰当的。有时我们期望能在当前位置为那些在别处定义的 bean 引入别名。在 XML 配置文件中,可用单独的< alias/>元素来完成bean 别名的定义。如配置文件中定义了一个 JavaBean:

<bean id = "testBean" class="com.test"/>

1、声明别名的两种方式

要给这个 JavaBean 增加别名,以方便不同对象来调用。我们就可以直接使用 bean 标签中的 name 属性:

<bean id="testBean" name= "testBean,testBean2" class="com.test"/>

同样,Spring 还有另外一种声明别名的方式:

<bean id="testBean" class="com. test"/>
<alias name="testBean" alias="testBean, testBean2" />

2、processAliasRegistration

protected void processAliasRegistration(Element ele) {
		// 获取beanName
		String name = ele.getAttribute(NAME_ATTRIBUTE);
		// 获取alias
		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 {
				// 注册alias
				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));
		}
	}

可以发现,跟之前讲过的 bean 中的 alias 解析大同小异,都是将别名与 beanName 组成一对注册至 registry 中。这里不再赘述。

四、import标签的解析

对于 Spring 配置文件的编写,我想,经历过庞大项目的人,都有那种恐惧的心理,太多的配置文件了。不过,分模块是大多数人能想到的方法,但是,怎么分模块,那就仁者见仁,智者见智了。使用 import 是个好办法,例如我们可以构造这样的 Spring 配置文件:

import resource= "customerContext.xml"/>
<import resource= "systemContext.xml" />

1、importBeanDefinitionResource

protected void importBeanDefinitionResource(Element ele) {
		String location = ele.getAttribute(RESOURCE_ATTRIBUTE);
		// 如果不存在resource属性则不做任何处理
		if (!StringUtils.hasText(location)) {
			getReaderContext().error("Resource location must not be empty", ele);
			return;
		}

		// 解析系统属性: e.g. "${user.dir}"
		location = getReaderContext().getEnvironment().resolveRequiredPlaceholders(location);

		Set<Resource> actualResources = new LinkedHashSet<>(4);

		// 判断location是绝对uri还是相对uri
		boolean absoluteLocation = false;
		try {
			absoluteLocation = ResourcePatternUtils.isUrl(location) || ResourceUtils.toURI(location).isAbsolute();
		}
		catch (URISyntaxException ex) {
			// cannot convert to an URI, considering the location relative
			// unless it is the well-known Spring prefix "classpath*:"
		}

		// Absolute or relative?
		// 如果是绝对uri则根据地址直接加载文件
		if (absoluteLocation) {
			try {
				int importCount = getReaderContext().getReader().loadBeanDefinitions(location, actualResources);
				if (logger.isTraceEnabled()) {
					logger.trace("Imported " + importCount + " bean definitions from URL location [" + location + "]");
				}
			}
			catch (BeanDefinitionStoreException ex) {
				getReaderContext().error(
						"Failed to import bean definitions from URL location [" + location + "]", ele, ex);
			}
		}
		else {
			// No URL -> considering resource location as relative to the current file.
			// 如果是相对uri则计算出绝对uri
			try {
				int importCount;
				//Resource 存在多个子实现类,如 VfsResource、 FileSystemResource 等,
				//而每个 resource 的 createRelative 方式实现都不一样,所以这里先使用子类的方法尝试解析
				Resource relativeResource = getReaderContext().getResource().createRelative(location);
				if (relativeResource.exists()) {
					importCount = getReaderContext().getReader().loadBeanDefinitions(relativeResource);
					actualResources.add(relativeResource);
				}
				else {
					//如果解析不成功,则使用默认的解析器 ResourcePatternResolver 进行解析
					String baseLocation = getReaderContext().getResource().getURL().toString();
					importCount = getReaderContext().getReader().loadBeanDefinitions(
							StringUtils.applyRelativePath(baseLocation, location), actualResources);
				}
				if (logger.isTraceEnabled()) {
					logger.trace("Imported " + importCount + " bean definitions from relative location [" + location + "]");
				}
			}
			catch (IOException ex) {
				getReaderContext().error("Failed to resolve current resource location", ele, ex);
			}
			catch (BeanDefinitionStoreException ex) {
				getReaderContext().error(
						"Failed to import bean definitions from relative location [" + location + "]", ele, ex);
			}
		}
		// 解析后进行监听器激活处理
		Resource[] actResArray = actualResources.toArray(new Resource[0]);
		getReaderContext().fireImportProcessed(location, actResArray, extractSource(ele));
	}

上面的代码不难,相信配合注释会很好理解,我们总结一下大致流程便于读者更好地梳理,在解析< import> 标签时,Spring 进行解析的步骤大致如下。
(1)获取 resource 属性所表示的路径。
(2)解析路径中的系统属性,格式如“${user.dir}”。
(3)判定 location 是绝对路径还是相对路径。
(4)如果是绝对路径则递归调用 bean 的解析过程,进行另一次的解析。
(5)如果是相对路径则计算出绝对路径并进行解析。
(6)通知监听器,解析完成。

五、嵌入式 beans 标签的解析

对于嵌人式的 beans 标签,相信大家使用过或者至少接触过,非常类似于 import 标签所提供的功能,使用如下:

<?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
	   		    https://www.springframework.org/schema/beans/spring-beans.xsd>

     <beans> 
    </beans>
    
</beans>

对于嵌入式 beans 标签来讲,并没有太多可讲,与单独的配置文件并没有太大的差别,无非是递归调用 beans 的解析过程,相信读者根据之前讲解过的内容已经有能力理解其中的奥秘了。

总结

待补充。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

《Spring源码深度分析》第3章 默认标签的解析 的相关文章

随机推荐

  • 【新版】系统架构设计师 - 案例分析 - 信息安全

    个人总结 仅供参考 欢迎加好友一起讨论 文章目录 架构 案例分析 信息安全 安全架构 安全模型 分类 BLP模型 Biba模型 Chinese Wall模型 信息安全整体架构设计 WPDRRC模型 各模型安全防范功能 网络安全体系架构设计
  • Linux中创建文件夹,删除文件夹

    Linux中创建目录 mkdir 文件夹 比如 mkdir test 删除文件夹 rm rf 文件夹 比如 rm rf soft vi强制不保存退出命令 q
  • const 定义数组问题

    const keysArr array aaa gt 11111 bbb gt 22222 ccc gt 33333 ddd gt 44444 上面是标准的错误格式 PHP编译不会报详细错误信息 500 查找半天的代码问题 一直未发现错误
  • 解决Jenkins构建前端时node-sass的.node文件下载报错问题的一种方案

    问题背景 公司的npm仓库未跟外网联通 为什么不联通 我也好鸡儿纳闷 使用Jenkins构建前端时 会在下载node sass的 node时报错 默认情况下会从github上去下 node文件 当然也可以通过配置 npmrc文件指定下载路径
  • 浅析Jetty与tomcat区别

    一 Jetty介绍 1 Jetty概述 Jetty是一个开源项目 最初由Mort Bay Consulting公司创建 它的目标是提供一个快速 灵活 可嵌入的Web服务器和Servlet容器 使Java开发人员能够轻松构建高性能的Web应用
  • 如何在 Mac 上录制屏幕?mac录屏教程分享

    您可以为整个屏幕或屏幕上的选定部分录制视频 1 使用 截屏 工具栏 要查看 截屏 工具栏 请同时按下以下三个按键 Shift Command 和 5 您将看到用于录制整个屏幕 录制屏幕的选定部分或拍摄屏幕静态图像的屏幕控制项 录制整个屏幕
  • 泛型的概念

    一 什么是泛型 参数化类型 为什么要引入泛型 1 将不同类型的数据添加到Arraylist中 取出数据要使用时 要进行强制转换 还原 向下转型 2 同时在编写程序时 不会报错 无类型安全监测机制 而结果出错ClassCastExceptio
  • 摄像头在H5的实时播放功能实现历程

    一 问题解决的路程 1 需求来源 因项目发展需求 需要在3D地图上进行实时摄像头监控展示 3D地图是基于浏览器H5页面展示的 在H5页面实时播放摄像头监控就需要可以直接拿到取流地址进行直接播放 以下各大摄像头产商取流方式 海康威视 默认IP
  • 网络层

    网络层 从它的名字可以看出 它解决的是网络与网络之间 即网际的通信问题 而不是同一网段内部的事 用于网络互联的设备都处于网络层 如 路由器 网络交互机等 一个底层网络内部只存在两层 即数据链接层 与 物理层 没有其它层
  • 考研C++/C数据结构之单链表两种查找方法

    继上篇文章我们探讨了单链表的两种创建方法 头插法和尾插法 今天我们来学习一下单链表的两种查找方法 按序查找和按值查找 按序查找的代码实现如下 按位查找 LinkList GetElem LinkList L int i int j 1 Li
  • python是真刑啊!爬虫这样用,离好日子越铐越近了~

    一个程序员写了个爬虫程序 整个公司200多人被端了 不可能吧 刚从朋友听到这个消息的时候 我有点不太相信 做为一名程序员来讲 谁还没有写过几段爬虫呢 只因写爬虫程序就被端有点夸张了吧 朋友说 消息很确认并且已经进入审判阶段了 01 对消息进
  • 求解视觉里程计(基于特征点法)

    目录 1 视觉里程计 VO 2 基于特征点法的视觉里程计算法 2 1 特征点 2 2 ORB特征点的提取与匹配 2 2 1 关键点与描述子 灰度质心法 特征描述子计算 2 2 2 特征点匹配 2 3 特征点法估计相机位姿 2 3 1 对极几
  • MySQL事务简介

    一 事务的起源 原子性 Atomicity 要么全做 要么全不做 一致性 Consistency 数据库中的数据全部符合现实中的约束 隔离型 Isolation 操作以原子性执行 且不同事务操作互不干扰 多种隔离级别 持久性 Durabil
  • Ubuntu18.04配置Seetaface6

    目录 一 下载安装Qt软件 1 安装包下载 2 安装Qt 3 配置 二 下载源码 三 编译工具 四 编译 1 编译OpenRoleZoo 2 编译SeetaAuthorize 3 编译TenniS 五 运行 1 修改lib路径 2 buil
  • 360n6pro刷鸿蒙系统,360N6和N6Pro通用刷机包MIUI9开发版V8.6.9紫火定制版

    本帖最后由 360fans 80867761 于 2018 8 7 19 44 编辑 360N6和N6Pro通用MIUI9开发版V8 6 9紫火定制版刷机包更新指纹解 除了有个小BUG 相机有时候加载有点慢 其他都很正常 无任何推广软件 刷
  • Vue实例选项之【methods】

    methods div h1 site site h1 h1 url url h1 h1 alexa alexa h1 p 通过调用方法返回数据 p div
  • 0欧电阻和磁珠的区别

    来源 B站https www bilibili com video BV1Yi4y1x7JL 0欧姆电阻实际上并不能达到真正的0欧姆 它是一个阻性的阻值极小的电阻 磁珠浅显的可以看成是一个电感 故很多原理图中磁珠的符号是电感的符号 磁珠的直
  • less命令详解-最好用的文档查看命令

    less命令详解 最好用的文档查看命令 其他文件查看命令 less使用场景 less的日常使用 less快捷键 less参数 其他文件查看命令 小文本查看命令 cat 将文件所有内容打印打控制台 tac 将文件所有内容反向打印打控制台 vi
  • C#线程中使用委托实现textbox显示

    delegate void SetTextCallback string text 后加的 好好想一想 参数是SetText带的参数 From www uzhanbao com private void SetText string tex
  • 《Spring源码深度分析》第3章 默认标签的解析

    目录标题 前言 一 Spring默认的四个标签 二 bean标签的解析及注册 1 BeanDefinition下的三个实现类 2 解析BeanDefinition 1 processBeanDefinition 2 parseBeanDef