书名:Spring Boot 3:高级与架构设计
ISBN:978-7-115-68628-2
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
著 LinkedBear
责任编辑 单瑞婷
人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
网址 http://www.ptpress.com.cn
读者服务热线:(010)81055410
反盗版热线:(010)81055315
随着JDK的升级与迭代,Spring Framework与Spring Boot也分别升级到了全新的6.x与3.x版本。全新的版本带来了更加强大的功能和特性。本书重点讲解Spring Framework与Spring Boot的高级机制、架构设计和前沿特性,通过源码解读和剖析深入讲解框架底层原理,并对新特性进行详细解读。
本书分为3个部分,共8章。第一部分介绍Spring Framework的IOC容器高级机制与原理;第二部分讲解AOP高级特性与原生设计;第三部分主要讲解Spring整合AI的前沿技术,以及Spring Framework 6与Spring Boot 3的新版本特性。
本书适合已经有Spring使用基础的开发者、想了解Spring前沿特性的进阶者,以及熟练使用Spring Boot且希望进一步提升技能与认知的探究者阅读和使用。
Spring Framework和Spring Boot已然是当下构建Java Web应用的首选。其强大的容器和应用特性,再加上优异的第三方技术整合能力,使得Spring Framework和Spring Boot稳坐Java Web应用开发的头把交椅。在出版《Spring Boot 源码解读与原理分析》之后,不少读者提出,其阅读难度较大,对部分还不完全熟悉Spring Framework和Spring Boot的读者而言更是难上加难。为此,作者经过深思熟虑,并考虑到在撰写期间Spring Framework与Spring Boot的版本更新,决定以Spring Framework 6.x与 Spring Boot 3.x为基础,重新编写一套从零开始学习的系列书——《Spring Boot 3:入门与应用实战》(后文简称为入门篇)、《Spring Boot 3:高级与架构设计》(后文简称为高级篇),旨在帮助读者了解、掌握Spring Framework与Spring Boot,并对新版本中引入的核心特性进行讲解。作者期望那些刚刚完成Java Web学习的初学者能够借助本书学习Spring Framework与Spring Boot,同时希望那些实战经验尚浅的开发者能够利用本书巩固自身基础并弥补知识空缺。此外,作者也希望那些致力于深入研究的进阶学习者能够借助本书进一步深入了解Spring Framework与Spring Boot的全貌。
本书分为3个部分,分别介绍Spring Framework的IOC容器高级机制与原理(第1~3章)、AOP高级特性与原生设计(第4~5章),以及前沿技术与新版本特性(第6~8章)。具体安排如下。
• 第1章讲解Spring Framework中的核心设计思想“元编程”“元信息”等,并引出IOC容器中的具体实现BeanDefinition与BeanDefinitionRegistry。
• 第2章重点讲解Spring Framework核心容器中的高级机制,包括Environment、后置处理器、SPI机制,以及编程式驱动IOC容器等高级使用方式。
• 第3章从一个Bean的源头开始挖掘和探索Bean的全生命周期,使读者可以更好地理解IOC容器中重要组件的来龙去脉,同时对其中的设计理念有更深刻的认识。
• 第4章介绍了一些AOP相关的高级特性,包括TargetSource的设计、切入点表达式的更多语法、引介机制等。
• 第5章对Spring Framework原生设计和实现的AOP模型进行详尽的讲解。从核心模型和组件出发,阐述原生AOP机制的API之间如何协作,以及原生AOP机制如何与AspectJ融合与适配。
• 第6章从LLM的起源出发,重点讲解Spring拥抱AI的全新框架Spring AI的使用。结合多种常见的生成式AI使用场景,讲解和演示Spring AI如何完成大语言模型的对接,以及如何完成AI与应用的集成和配合。
• 第7章介绍Spring Framework 6.x的新特性,主要包含AOT编译、GraalVM的支持、可观察性的使用等。
• 第8章介绍Spring Boot 3.x的新特性,包括版本升级要求、配置属性变动、原生镜像制作等。
本书不是一本关于Spring Boot的入门图书,所以要求读者至少了解Spring Boot和Spring Framework,并有基本的使用经验。此外,还希望读者掌握一定的JavaSE、JavaEE的相关基础。
因此,本书更适合以下人群阅读:
• 会使用Spring Boot、Spring Framework,想要掌握框架底层核心原理的读者;
• 能熟练使用Spring Boot,但没有深入挖掘其深层次特性和高级使用功能的读者;
• 技术广度足够,但深度有限的进阶者;
• 职业规划目标为技术总监、架构师等高级技术岗位的进阶者;
• 被Spring Boot、Spring Framework问题困扰的求职者;
• 有意向对Spring生态进行深入探究的研究者。
本书中出现的部分名词受作者主观表述方式的影响,可能会出现多种不同的叫法,以下是部分专有名词的映射关系。
• Spring Framework:指Spring框架,简称Spring。
• WebMvc:指Spring MVC、Spring WebMvc。
• Bean:Spring Framework中管理的组件对象(概念)。
• bean:容器中真实存在的组件对象实例(具体)。
• IOC容器:泛指ApplicationContext。当上下文在讲解BeanFactory时,IOC容器则指代BeanFactory。
• Web容器:Servlet容器与NIO容器的统称,不仅限于Tomcat、Jetty等Servlet容器。
• LLM:大语言模型的英文缩写。大模型等简称同样指的是LLM。
在展示测试代码或框架源码的片段中,考虑到部分代码的篇幅和长度问题会受纸质图书的呈现方式影响,会对部分代码进行折叠或换行处理,对涉及源码的部分视情况进行省略或删减。读者在阅读时可适当结合IDE阅读,以获得最好的阅读体验。
在编写本书期间,Spring Framework的最新正式版本为6.1.x,Spring Boot的最新正式版本是3.2.x。而时至定稿日,Spring Framework和Spring Boot的版本已分别更新到6.2.x与3.4.x。考虑到Spring Framework 6.2版本与Spring Boot 3.4中更新的特性没有特别值得讲解的部分,最终本书定稿时所采用的Spring Framework基准版本为6.1.15,Spring Boot基准版本为3.2.12。在没有特别说明版本时,本书中引用的源码均基于Spring Framework 6.1.15 与 Spring Boot 3.2.12。
由于Spring Boot和Spring Framework在当下的应用范围甚广,升级到新版本后更是出现大量改动,版本迭代速度较快,加之作者本人的技术水平有限,因此书中难免出现错误。作者虽然在编写本书时已经对每个知识点反复研究、测试和推敲,力求讲解得尽可能正确、精准、易懂,但还是不敢保证所有内容都没有错误。如果读者发现本书中有任何错误,或者想给本书及作者提供任何建议,欢迎通过以下方式与作者取得联系,以便及时修订本书。
• 邮箱:LinkedBear@163.com。
• 微信公众号:Spring引路熊导师。
• GitHub博客:http://site.linkedbear.top。
本书的勘误情况将发布在微信公众号与GitHub博客中,请各位读者及时关注,以便获取最新的更新资讯。
本书附带的所有测试代码已托管至GitHub平台,欢迎各位读者下载参考。GitHub仓库地址:https://github.com/LinkedBear/spring6-boot3-projects-epubit。
创作本书是承接入门篇的又一次挑战,面对升级和高级原理的解析,我既胸有成竹又审慎稳健。高级篇的高专业性和前瞻性,使我更加严谨地对待本书的每一个知识点和特性的讲解和剖析,同时还要做到尽可能易懂。本书的难度相对较高,创作本书的过程更是漫长且艰巨,想先对自己说一句辛苦了!撰写本书的过程中,我的家人一直在背后默默地支持着我,使我能倾尽全心写作并使本书顺利出版,在这里祝他们健康平安!
本书得以顺利出版,离不开人民邮电出版社编辑老师的鼎力相助,尤其是与我对接的单瑞婷老师。单老师从资深专业图书编辑的角度,在本书撰写过程中提供了非常宝贵的经验、指点和支持。感谢编辑老师们的辛勤付出,谢谢你们!
还要感谢在本书创作中帮助过我的读者朋友。本书脱胎于掘金小册,在整合重写和升级的过程以及宣传推广和阅读反馈中,得到了来自读者朋友非常多的帮助和反馈,他们也是构建本书的有力支持者。正因为有他们宝贵的反馈和建议,本书才能做到内容深入浅出,讲解清晰透彻,知识主次分明。
最后要感谢的是正在阅读本书的您,感谢您选择本书作为陪伴您学习Spring Framework和Spring Boot的“好伙伴”。希望本书能帮助您在学习Spring Framework和Spring Boot时汲取尽可能多的方法、思想、设计等。衷心地希望本书能给正在阅读的您带来帮助,祝您学习顺利,阅读愉快,前程似锦!
LinkedBear(苏振志)
2025年6月
本书提供如下资源:
• 本书配套PPT;
• 本书配套视频;
• 本书源代码;
• 本书思维导图;
• 异步社区7天会员。
要获得以上资源,您可以扫描下方二维码,根据指引领取。

我们的联系邮箱是shanruiting@ptpress.com.cn。
如果您对本书有任何疑问、建议,或者发现本书中有任何错误,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。
如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们。
如果您所在的学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。
如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。
“异步社区”(www.epubit.com)是由人民邮电出版社创办的IT专业图书社区,于2015年8月上线运营,致力于优质内容的出版和分享,为读者提供高品质的学习内容,为作译者提供专业的出版服务,实现作者与读者在线交流互动,以及传统出版与数字出版的融合发展。
“异步图书”是异步社区策划出版的精品IT图书的品牌,依托于人民邮电出版社在计算机图书领域多年的发展与积淀。异步图书面向IT行业以及各行业使用IT技术的用户。
► 第1章 元编程与元信息
► 第2章 IOC容器的高级机制
► 第3章 Bean的全生命周期原理
❏ 元编程的概念与设计理念;
❏ 配置元信息;
❏ BeanDefinition信息定义模型;
❏ BeanDefinitionRegistry注册中心。
从高级篇开始,我们不再局限于了解和认识Spring Framework和Spring Boot的应用,而是要更深入地探究Spring Framework与Spring Boot内部的工作机制与原理。从IOC容器出发,Spring Framework的IOC容器以及注册到IOC容器中所有的Bean,它们都是如何定义的,又是如何描述和存储的?针对这些问题,我们先走出深层次探究的第一步:了解元编程与元信息。
本章题目中的两个关键词都包含“元”字,这个概念对于部分读者而言可能较难理解,因此我们先来解释“元”的概念。
“元”即英语中的“meta”。在英语中,“meta”被解释为“a ××× about ×××”,或者“××× of ×××”,例如1.1.2节讲到的元编程即用编程的手段去操纵编程过程。如果将“元”的概念稍做扩展和引申,则可以有另一种理解方式:在自然语言中,我们经常会使用一些常见词汇来描述一些复杂的场景或事物,例如使用性能的高低来评价计算机的运行速度,使用能耗比来衡量笔记本计算机中的CPU在同等性能下的功耗水平。这两个概念场景中都包含一个“性能”,而性能又被解释为“机械、器材、物品等所具有的性质和功能”。如果再向下分解概念,那么“性质”这个词就相对难以使用更通俗的语言进行描述。在这个例子中,“性能”是描述计算机或者CPU的定义,而“性质”就是描述“性能”的元定义。
简单总结,“元”这个概念可以用来解释或者定义一个其他的概念,一些相对简单的概念背后,往往存在更能揭示其本质和深层含义的概念,这些概念就蕴含着“元”的意思。
理解了“元”的概念之后,下面通过一个简单示例进行巩固。一般情况下,我们编写程序代码来实现需求的过程称为编程,而元编程则可以理解为使用编程的手段来操纵完成编程这一过程。可能读者听到这个解释后会联想到代码生成器,在笔者的观点中,代码生成器可以算作元编程的一种体现,例如基于CRUD业务场景的代码生成器可以读取数据库的二维表结构,并以此为依据生成相应的模型类、VO类、Mapper接口、Service和Controller等基础代码。如果没有代码生成器,我们需要人工阅读表结构并编写上述基础代码,而借助代码生成器实现只需要编写与数据库表结构相关的解析逻辑和代码生成逻辑,就可以驱动其生成基础代码。
结合Spring Framework和Spring Boot的使用,我们可以再举一个元编程的例子。通过入门篇的学习,相信读者已经了解到Spring Framework中的IOC容器包含大量的Bean,这些Bean可以通过XML配置文件中使用的<bean>标签、注解配置类中使用的@Bean 注解,以及@ComponentScan配合模式注解的方式完成注册,这些方式都需要我们遵循Spring Framework提供的Bean注册规则。如果我们使用某种编程的方式取代上述3种方式,仍然可以将一些所需的Bean注册到IOC容器,那也就意味着使用编程的方式来取代以往手动编码的方式完成编程工作,这种使用编程式驱动编码的方式,也可以理解为“元编程”。
这些概念的理解可能需要一定过程,初期存在困惑是正常的,结合后续的内容,读者会逐渐感受到元编程的概念。
说到配置源,可能部分读者的第一反应是联想到数据源,实际上这两个概念之间没有必然联系。配置源针对的是某个框架,不局限于连接数据库,这个概念以及元信息的概念可能理解起来相对困难,本节将尽可能地用比较容易理解的例子来解释清楚。
简单理解,配置源即配置的来源。在入门篇讲解的各种基于原生Spring Framework的代码示例中,我们均使用注解配置类或者XML配置文件来驱动IOC容器,那么对于IOC容器而言,注解配置类或者XML配置文件就可以称为“配置源”。对于Spring Boot工程来说,由于我们通常都使用注解配置类和自动配置类完成组件注册和配置编写,因此对于Spring Boot工程而言,我们自行编写的配置类和Spring Boot负责加载的自动配置类就是配置源。
解释这个概念本身难度不大,不过请读者思考一个问题:Spring Framework如何基于我们提供的自定义配置源来引导和启动整个应用上下文?
这个问题不难回答,Spring Framework得到配置源后通常先加载,再解析,最后将定义好的Bean注册到IOC容器。这个过程中比较重要的环节是配置源的解析。纵观入门篇中讲解的内容,我们接触的配置源大致有两种类型:注解配置类和XML配置文件。
入门篇中出现的比较简单的注解配置类如代码清单1-1所示。
┃ 代码清单1-1 简单的注解配置类
@Configuration
@ComponentScan("com.linkedbear.spring.bean.b_scope.bean")
public class BeanScopeConfiguration {
@Bean
public Child child1(Toy toy) {
Child child = new Child();
child.setToy(toy);
return child;
}
@Bean
public Child child2(Toy toy) {
Child child = new Child();
child.setToy(toy);
return child;
}
}仔细观察这个注解配置类,可以发现其包含以下3个部分。
(1)代表当前类是注解配置类的@Configuration注解。
(2)驱动组件扫描的@ComponentScan注解。
(3)使用@Bean注解注册的两个Bean,名称分别为child1和child2。
如果给上述的几个部分进行信息定义,则可以使用一种比较抽象的语言来指代注解配置类的内容,如代码清单1-2所示。可以发现,这种抽象语言已经包含原注解配置类中的关键信息,且仅包含注解配置类中的配置结构,不会记录配置的具体信息。
┃ 代码清单1-2 抽象语言描述的注解配置类
BeanScopeConfiguration.java: {
annotations: [ComponentScan]
beans: [child1, child2]
}与注解配置类有所不同,XML配置文件中的内容稍多,代码清单1-3是入门篇中讲解Bean依赖注入的一个XML配置文件。
┃ 代码清单1-3 简单的XML配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans ...>
<context:component-scan
base-package="com.linkedbear.spring.basic_di.c_value_spel.bean"/>
<context:property-placeholder location="classpath:basic_di/value/red.properties"/>
<bean class="com.linkedbear.spring.basic_di.c_value_spel.bean.Green">
<property name="name" value="#{'copy of ' + blue.name}"/>
<property name="order" value="#{blue.order + 1}"/>
</bean>
<bean class="com.linkedbear.spring.basic_di.c_value_spel.bean.White">
<property name="name" value="#{blue.name.substring(0, 3)}"/>
<property name="order" value="#{T(java.lang.Integer).MAX_VALUE}"/>
</bean>
</beans>如果仿照注解配置类的方式来解剖代码清单1-3所示的内容,则可以提取出如下4个部分:
(1)<beans>头信息,它需要声明当前默认使用的命名空间等;
(2)<context:component-scan>标签声明的组件扫描;
(3)<context:property-placeholder>引入外部properties文件;
(4)<bean>注册Bean并进行属性赋值。
进一步抽象,则可以将其转换为代码清单1-4所示的抽象语言。可以发现,抽象语言同样不会描述具体的组件扫描路径等,只会记录XML配置文件中声明了哪些标签。
┃ 代码清单1-4 抽象语言的描述XML配置文件
beans.xml {
context: [component-scan, property-placeholder]
beans: [person]
}从1.2.1节剖析配置源的过程中,读者是否可以感受到解析和抽象化的思路?这种解析思路像是把整个文件中配置的所有定义都抽取出来,形成一个类似于配置定义信息的元素。如果读者可以体会到这种感觉,请一直保持住;即便没有体会到也没关系。下面先来了解其中的一个重要概念:元信息。
|
|
元信息又可以理解为元定义,按照1.1.1节的概念解释,元信息就是定义的定义(a definition about definition)。如果读者认为这样理解过于抽象,可通过下面的简单示例来理解。
(1)定义一个“人”,他的姓名是张三,年龄是18岁,男性。
• 他的元信息就是他的属性:Person {name, age, sex} 。
(2)定义一只“猫”,它的名字是咪咪,品种是美国短毛猫,黑白毛,主人是张三。
• 它的元信息可以抽取为:Cat {name, type, color, master} 。
读到这里读者是否可以意识到一点:这种结构剖析非常类似于对象和类。所以我们可以这样定义:在面向对象的编程中,类中包含对象的元信息。
请读者再思考一个小问题,类中是否包含元信息?答案是肯定的,如Class这个类里面就包含一个类的所有定义(属性、方法、继承实现、注解等),因此我们又可以得出一个结论:Class中包含类的元信息。
再举一个读者相对熟悉的例子:数据库表与表结构信息,这也是非常典型的信息与元信息,其中数据库表结构描述了数据库表的整体属性,以及表字段的属性。
理解了元信息的概念,接下来就可以了解Spring Framework中的配置元信息。但Spring Framework涉及的元信息更多,我们先从最熟悉的开始了解。
与1.2.2节中讲解的Class描述类相似,Spring Framework中定义的Bean也会封装为一个个Bean的元信息,也就是BeanDefinition。它包含了一个Bean所需要的几乎所有维度的定义:
(1)Bean的全限定名(className);
(2)Bean的作用域(scope);
(3)Bean是否延迟加载(lazy);
(4)Bean的工厂Bean名称(factoryBean);
(5)Bean的构造方法参数列表(constructorArgumentValues);
(6)Bean的属性值(propertyValues)。
(7)……
可以发现,这些定义信息能够描述一个Bean的大多数特征和属性。有关BeanDefinition的内容,下面1.3节将详细讲解。
IOC容器本身也有元信息,只不过这些元信息在入门篇基本没有涉及。IOC容器的配置元信息分为beans和context两部分。
(1)beans的配置元信息
以XML配置文件为例,如果读者仔细观察配置文件的顶层标签,会发现<beans>标签包含一些属性,如图1-1所示。但这些属性我们几乎没有接触过,一般都使用其默认值。

图1-1 <beans>标签的属性
<beans>标签中可以声明的属性如表1-1所示。请读者注意,由于<beans>标签支持嵌套使用,因此表1-1中“默认值”一列提到的default在没有声明时,会继承父配置的默认值;如果父配置也没有声明,则配置的默认值是default后括号内的值。
表1-1 <beans>标签的属性
|
配置元信息 |
含义/作用 |
默认值 |
|---|---|---|
|
profile |
基于环境的配置 |
"" |
|
default-autowire |
默认的自动注入模式(不需要声明 |
default(no) |
|
default-autowire-candidates |
满足指定属性名规则的属性才会被自动注入 |
"" |
|
default-init-method |
全局bean的初始化方法 |
"" |
|
default-destroy-method |
全局bean的销毁方法 |
"" |
|
default-lazy-init |
全局bean是否延迟加载 |
default(false) |
|
default-merge |
继承父bean时直接合并父bean的属性值 |
default(false) |
(2)context的配置元信息
此外,还有一部分IOC容器的配置源信息来自spring-context包的context前缀标签(例如之前写过的<component-scan/>标签),如图1-2所示。

图1-2 <context>标签的属性
同样地,我们将这些配置元信息都罗列出来,如表1-2所示。读者仅需简单了解其中几个(尤其是前3个)相对常见的配置即可。
表1-2 <context>标签的属性
|
配置元信息 |
含义 / 作用 |
|---|---|
|
|
开启注解驱动 |
|
|
开启组件扫描 |
|
|
引入外部的资源文件(properties、XML、YML等) |
|
|
指定配置源会覆盖全局配置(可用于配置覆盖) |
|
|
可以对没有注册到IOC容器的Bean实现依赖注入 |
|
|
与AOP相关(4.4节将介绍) |
|
|
暴露应用运行状态监控(与JMX管理监控有关) |
|
|
注册MBean到JMX实现运行状态监控(与JMX管理监控有关) |
至此,针对IOC容器的基本配置即列举完毕。其中需要了解的配置属性不多,而且大多都在入门篇提及,读者注意巩固前面的知识点即可。
入门篇4.7节关于PropertySource的讲解中,properties和YML文件曾被提及。它们的作用都是将具体的配置抽取为一个可任意修改的配置文件,以防止在程序代码中出现硬编码配置,避免修改配置还需要重新编译的麻烦。这种将配置内容抽取为配置文件的动作被称为“配置外部化”,被抽取出来的配置文件又被称为“外部化配置文件”。加载这些外部化配置文件的方式,要么是通过上述<context:property-placeholder/>标签,要么是通过@Property Source注解。它们最终都会被封装为一个个的PropertySource对象(properties文件被封装为PropertiesPropertySource),PropertySource对象内部持有这些外部化配置文件的所有内容。这些内容在入门篇已经介绍过,本节不再赘述。
对IOC容器中的Bean而言,描述它的配置元信息称作BeanDefinition。BeanDefinition的核心作用是描述Bean的定义信息,这个类相当重要,理解它有助于更深入地进入Spring Framework的核心原理内部进行探索。
与入门篇中了解API的方式相似,我们通过多个途径来试着了解BeanDefinition的概念和定义。
官方文档并没有用大篇幅去介绍BeanDefinition,仅使用一段话进行描述。
|
A bean definition can contain a lot of configuration information, including constructor arguments, property values, and container-specific information, such as the initialization method, a static factory method name, and so on. A child bean definition inherits configuration data from a parent definition. The child definition can override some values or add others as needed. Using parent and child bean definitions can save a lot of typing. Effectively, this is a form of templating. Bean的定义信息可以包含许多配置信息,包括构造方法参数,属性值和特定于容器的信息,例如初始化方法、静态工厂方法名称等。子Bean定义可以从父Bean定义继承配置数据。子Bean的定义信息可以覆盖某些值,或者可以根据需要添加其他值。使用父Bean和子Bean的定义可以节省很多输入。实际上这是一种模板的设计形式。 |
文档解释得比较清楚,Bean的定义包含这个Bean应该有的重要信息。此外文档又提到了一个概念:Bean的定义信息也有层次性(联想BeanFactory的层次性),Bean的定义信息可以继承自某个已有的定义信息,并覆盖父信息的一些配置值。而且文档最后也解释了这相当于模板的设计。
查阅BeanDefinition接口的javadoc,其内容虽不多,但已足够精确地说明BeanDefinition的基本作用。
|
A BeanDefinition describes a bean instance, which has property values, constructor argument values, and further information supplied by concrete implementations. This is just a minimal interface: The main intention is to allow a BeanFactoryPostProcessor such as PropertyPlaceholderConfigurer to introspect and modify property values and other bean metadata.
这只是一个最小的接口,它的主要目的是允许 |
与官方文档相比,javadoc额外提及了编码设计中BeanDefinition的使用:BeanFactoryPostProcessor可以任意修改BeanDefinition中的信息。其中又提到了BeanFactoryPostProcessor的概念,这是一种后置处理器,将在2.3节讲解。
借助IDE打开BeanDefinition的接口定义,从方法列表上看BeanDefinition整体包含以下5个部分,由此可见BeanDefinition把Bean的几乎所有信息都收集并封装起来,考虑的维度比较全面。
(1)Bean的类信息:全限定类名(beanClassName)。
(2)Bean的属性:作用域(scope)、是否默认Bean(primary)、描述信息(description)等。
(3)Bean的行为特征:是否延迟加载(lazy)、是否自动注入(autowireCandidate)、初始化/销毁方法(initMethod/destroyMethod)等。
(4)Bean与其他Bean的关系:父Bean的名称(parentName)、依赖的其他Bean(dependsOn)等。
(5)Bean的配置属性:构造方法参数(constructorArgumentValues)、属性变量值(propertyValues)等。
结合上述资料和分析,可以得出一个相对全面的参考描述:BeanDefinition描述了Spring Framework中Bean的元信息,它包含Bean的类信息、属性、行为、依赖关系、配置信息等。BeanDefinition具有层次性,并且可以在IOC容器初始化阶段被BeanDefinition RegistryPostProcessor构造和注册,被BeanFactoryPostProcessor拦截修改等。
理解BeanDefinition的含义后,下面我们深入源码了解BeanDefinition的结构与继承体系。借助IDEA可以形成图1-3所示的继承关系,从中可以发现,BeanDefinition并不是顶层接口,并且继承结构中涉及的接口、抽象类和扩展非常多。下面对其中比较重要的类和接口予以解析。

图1-3 BeanDefinition的继承关系
AttributeAccessor可直译为“属性访问器”,它具备访问对象内部属性的能力。文档注释中仅有一句话描述:Interface defining a generic contract for attaching and accessing metadata to/from arbitrary objects(定义用于将元数据附加到任意对象,或从任意对象访问元数据的通用协定的接口)。如果仅将这句话作为出发点理解,或许读者率先想到的是对象中的getter方法或setter方法,然而实际并非如此。
如前文元信息部分所述,一个类中有什么属性、什么方法,最终会封装在Class类对象中,通过反射可以获取类的属性、方法定义信息。例如代码清单1-5所示的Person类就可以使用代码清单1-6所示的抽象型定义性语言描述。
┃ 代码清单1-5 简单类Person
public class Person {
private String name;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}┃ 代码清单1-6 使用抽象型定义性语言描述Person
className: Person attributes: [name] methods: [getName, setName]
进入AttributeAccessor的源码中观察接口中定义的方法,会发现它不只是简单的getter和setter方法封装,还能移除属性信息(此处的属性就是Bean的成员属性)。结合代码清单1-7中的方法签名和注释,读者是否能联想到集合中的Map?从表面上看,Map与AttributeAccessor都具备设置、获取、移除、检查等行为能力。由此我们就可以总结出BeanDefinition的第一个特征:BeanDefinition继承了AttributeAccessor接口,具有配置Bean属性的功能(注意此处的措辞,“配置”包含访问、修改、移除等在内的操作)。
┃ 代码清单1-7 AttributeAccessor
public interface AttributeAccessor {
// 设置bean对象中属性的值
void setAttribute(String name, @Nullable Object value);
// 获取bean对象中指定属性的值
Object getAttribute(String name);
// 移除bean对象中的属性
Object removeAttribute(String name);
// 判断bean对象中是否存在指定的属性
boolean hasAttribute(String name);
// 获取bean对象的所有属性
String[] attributeNames();
}BeanMetadataElement的类名中包含“metadata”关键词,可能会使读者立即联想到元信息的概念。仅从类名上也可以看出BeanMetadataElement的功能:存放Bean的元信息。这个接口中只有一个方法,即获取Bean的资源来源,如代码清单1-8所示。所谓“资源来源”,简单理解就是Bean对应的文件或URL路径。对于我们编写的大多数Bean而言,它们的资源来源或者是XML配置文件,或者是注解驱动类,或者是组件扫描,我们可以在1.3.3节的演示环节加以留意。
┃ 代码清单1-8 BeanMetadataElement
public interface BeanMetadataElement {
default Object getSource() {
return null;
}
}AbstractBeanDefinition是BeanDefinition的第一个实现类。作为BeanDefinition的抽象实现,AbstractBeanDefinition中已经定义好了大部分的属性和功能,大体包含代码清单1-9所示的内容(只列出部分重要属性)。如果仅从内容上看,可能会有读者产生疑问:AbstractBeanDefinition中定义的属性看上去已经足够全面,为什么还要单独做一层抽象?
┃ 代码清单1-9 AbstractBeanDefinition中的重要属性
// bean对象的全限定类名 private volatile Object beanClass; // 默认的作用域为单实例 private String scope = SCOPE_DEFAULT; // 默认bean对象都不是抽象的 private boolean abstractFlag = false; // 是否延迟初始化 private Boolean lazyInit; // 自动注入模式(默认不自动注入) private int autowireMode = AUTOWIRE_NO; // 是否参与IOC容器的自动注入(设置为false则它不会注入其他bean对象,但其他bean对象可以注入它本身) private boolean autowireCandidate = true; // 同类型的首选bean对象 private boolean primary = false; // bean对象的构造器参数和参数值列表 private ConstructorArgumentValues constructorArgumentValues; // bean对象的属性和属性值集合 private MutablePropertyValues propertyValues; // bean对象的初始化方法 private String initMethodName; // bean对象的销毁方法 private String destroyMethodName; // bean对象的资源来源 private Resource resource;
想要解释这个问题,可以结合AbstractBeanDefinition的javadoc。从javadoc中可以发现,所描述的AbstractBeanDefinition不包含几个落地实现中的差异性属性,即不同的子类扩展会有特定的属性设计,所以AbstractBeanDefinition仅提取共性属性。
|
Base class for concrete, full-fledged BeanDefinition classes, factoring out common properties of GenericBeanDefinition, RootBeanDefinition, and ChildBeanDefinition. The autowire constants match the ones defined in the AutowireCapableBeanFactory interface.
|
|
在继续向下了解落地实现之前,这里我们补充一个入门篇没有讲解的知识点:自动注入模式。一般而言,IOC容器中的Bean之间大多都有相互依赖的关系,在进行组件依赖注入时,通常需要在XML配置文件或在属性/构造方法/setter方法上标注特定的注解( |
自动注入模式的使用方式很简单,比如从入门篇2.2.2节的依赖注入中,我们选择一个非常简单的配置文件,如代码清单1-10所示,此处cat注入的Person完全可以不写,只需要在<bean>标签上声明自动注入模式为byName(按名称注入)即可,运行效果是完全一样的。
┃ 代码清单1-10 XML配置文件中调整自动注入模式
<bean id="cat" class="com.linkedbear.spring.basic_di.a_quickstart_set.bean.Cat" autowire="byName">
<property name="name" value="test-cat"/>
<!-- <property name="master" ref="person"/> 可以不写 -->
</bean>自动注入模式有5种:AUTOWIRE_NO(不自动注入)、AUTOWIRE_BY_NAME(根据Bean的名称注入)、AUTOWIRE_BY_TYPE(根据Bean的类型注入)、AUTOWIRE_CONSTRUCTOR(根据Bean的构造方法注入)、AUTOWIRE_AUTODETECT(借助内省决定如何注入,在版本3.0及以后即弃用)。该功能默认不开启,需要开发者对需要注入的属性标注注解,或者在XML配置文件中配置。
GenericBeanDefinition是BeanDefinition的第一个落地实现,“Generic”开头意味着这个类代表通用、一般的,所以这种BeanDefinition也具有一般性。Generic BeanDefinition的源码实现非常简单,仅仅比AbstractBeanDefinition多了一个parentName属性,由这个设计可以得出以下3个结论。
(1)AbstractBeanDefinition已经完全可以构成BeanDefinition的实现。
(2)GenericBeanDefinition就是AbstractBeanDefinition的非抽象扩展。
(3)GenericBeanDefinition具有层次性(可从父BeanDefinition处继承一些属性信息)。
不过有一点需要注意,GenericBeanDefinition的层次性体现得不强烈,而接下来要介绍的两种BeanDefinition有非常强的层次性关系体现。
Root和Child很明显是父子关系的含义。对于ChildBeanDefinition,它的设计实现与GenericBeanDefinition如出一辙,都是集成一个parentName,将其作为父BeanDefinition的“指向引用”。不过有一点要注意,ChildBeanDefinition没有默认的无参构造方法,必须传入parentName才可以创建对象,但GenericBeanDefinition则有两种不同的构造方法。
RootBeanDefinition有“根”的概念,它只能作为单体独立的BeanDefinition或父BeanDefinition出现(不能继承其他BeanDefinition)。RootBeanDefinition中的设计也相对复杂,从源码的篇幅(接近500行,而GenericBeanDefinition只有100多行)上就能看得出来。不过我们无须逐一研究其属性,只了解重要的组成部分即可。
代码清单1-11中展示的是RootBeanDefinition中扩展的重要属性。RootBeanDefinition在AbstractBeanDefinition的基础上又扩展了诸如Bean的id、别名、注解信息、工厂信息等,而且其中不乏与反射相关的元素,可见BeanDefinition在底层会利用反射机制来实现效果。
┃ 代码清单1-11 RootBeanDefinition中扩展的重要属性
// BeanDefinition的引用持有,存放了Bean的别名 private BeanDefinitionHolder decoratedDefinition; // Bean上面的注解信息 private AnnotatedElement qualifiedElement; // Bean中的泛型 volatile ResolvableType targetType; // BeanDefinition对应的真实的Bean volatile Class<?> resolvedTargetType; // 是否是FactoryBean volatile Boolean isFactoryBean; // 工厂Bean方法返回的类型 volatile ResolvableType factoryMethodReturnType; // 工厂Bean对应的方法引用 volatile Method factoryMethodToIntrospect;
最后提一下AnnotatedBeanDefinition,它并不是BeanDefinition的实现类,而是一个子接口,如代码清单1-12所示。由接口定义的方法可知,AnnotatedBeanDefinition可以提供Bean上的注解信息。借助IDEA可发现,它的实现类中有Annotated GenericBeanDefinition和ScannedGenericBeanDefinition,它们都是基于注解驱动的Bean注册封装的BeanDefinition。当前我们没有必要对这些BeanDefinition进行深入研究,随着源码阅读的逐渐深入,这些问题会被逐个击破。
┃ 代码清单1-12 AnnotatedBeanDefinition
public interface AnnotatedBeanDefinition extends BeanDefinition {
AnnotationMetadatagetMetadata();
MethodMetadatagetFactoryMethodMetadata();
}了解了BeanDefinition的结构和继承关系后,接下来我们将通过一些简单示例来体会BeanDefinition中的设计以及封装的内容。
|
|
使用XML配置文件的方式中,每定义一个<bean>标签就相当于构建了一个BeanDefinition。下面演示基于XML的BeanDefinition。以最简单的Person为例,我们使用XML配置文件的方式完成Person对象的注册及属性注入,如代码清单1-13所示。
┃ 代码清单1-13 Person对象的注册及属性注入
public class Person {
private String name;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
<?xml version="1.0" encoding="UTF-8"?>
<beans ...>
<bean id="person" class="com.linkedbear.spring.definition.a_quickstart.bean.Person">
<property name="name" value="zhangsan"/>
</bean>
</beans>接下来使用XML配置文件驱动IOC容器,并尝试获取BeanDefinition,也就是利用ClassPathXmlApplicationContext加载XML配置文件。但是借助IDE尝试后,我们并没有从ClassPathXmlApplicationContext中找到类似于getBeanDefinition的方法,只找到了一个获取名称和获取数量的方法,如图1-4所示。这是否意味着我们只能先获取所有的BeanDefinition名称,再逐个获取并判断它是否是我们想要的BeanDefinition?

图1-4 ClassPathXmlApplicationContext中没有getBeanDefinition方法
当然不是。对于IOC容器而言,BeanDefinition是一个相对底层的API,所以使用ClassPathXmlApplicationContext根接口无法正常获取,需要先从其中获取BeanFactory,再借助BeanFactory才可以获取BeanDefinition(不要忘记ClassPathXmlApplication Context的内部组合了一个BeanFactory)。
找到BeanFactory的现役实现DefaultListableBeanFactory,发现该类中包含一个getBeanDefinition(String)方法,如图1-5所示。这就意味着我们可以用这个方法来得到BeanDefinition。另外请读者注意一个细节,如果向上追踪getBeanDefinition方法的定义,发现该方法定义在ConfigurableListableBeanFactory中,结合名称不难知晓它的特性:这是一个同时具备“可配置”和“可列举”功能的BeanFactory。

图1-5 DefaultListableBeanFactory中包含getBeanDefinition方法
回到正题,既然ClassPathXmlApplicationContext没有getBeanDefinition方法,那么就由它获取BeanFactory,再使用BeanFactory获取BeanDefinition,如代码清单1-14所示。编码完毕后运行启动类,观察控制台中打印person的BeanDefinition信息,可以发现id为"person"的BeanDefinition中存储了这个Bean的所有基本信息。另外值得注意的是,它是一个Generic Bean(打印personBeanDefinition的类型可得到GenericBeanDefinition),这个信息比较重要,请读者记住。
┃ 代码清单1-14 使用BeanFactory获取BeanDefinition
public class BeanDefinitionQuickstartXmlApplication {
public static void main(String[] args) throws Exception {
var ctx = new ClassPathXmlApplicationContext("definition/definition-beans.xml");
BeanDefinition personBeanDefinition = ctx.getBeanFactory().getBeanDefinition("person");
System.out.println(personBeanDefinition);
}
}
Generic bean: class [com.linkedbear.spring.definition.a_quickstart.bean.Person]; scope=; abstract=false; lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodNames=null; destroy MethodNames=null; defined in class path resource [definition/definition-beans.xml]接下来介绍使用组件扫描的方式注册Bean。我们给Person标注一个@Component注解,并使用AnnotationConfigApplicationContext的组件扫描驱动IOC容器,如代码清单1-15所示。与ClassPathXmlApplicationContext不同的是,在AnnotationConfig ApplicationContext中可以直接调用getBeanDefinition方法,这是与XML配置文件驱动的IOC容器不同的一点。
┃ 代码清单1-15 注解驱动扫描Person获取BeanDefinition
public class BeanDefinitionQuickstartComponentApplication {
public static void main(String[] args) throws Exception {
var ctx = new AnnotationConfigApplicationContext(
"com.linkedbear.spring.definition.a_quickstart.bean");
BeanDefinition personBeanDefinition = ctx.getBeanDefinition("person");
System.out.println(personBeanDefinition);
System.out.println(personBeanDefinition.getClass().getName());
}
}运行启动类的main方法,控制台打印出来的依然是一个Generic Bean,但是BeanDefinition的类型与前述XML BeanDefinition不同。可以发现,BeanDefinition的打印信息里最大的不同是加载来源:基于XML解析出来的Bean定义来源是XML配置文件;基于@Component注解解析出来的Bean定义来源是类的 .class文件。
Generic bean: class [com.linkedbear.spring.definition.a_quickstart.bean.Person]; scope=singleton; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodNames=null; destroyMethodNames=null; defined in file [E:\IDEA\spring6-boot3-projects-epudit\spring-01-ioc\ target\classes\com\linkedbear\spring\definition\a_quickstart\bean\Person.class]org.springframework.context.annotation.ScannedGenericBeanDefinition
使用注解配置类的方式也不麻烦,我们可以编写一个BeanDefinitionQuickstart Configuration配置类,使用@Bean注解注册一个Person,随后使用该配置类驱动IOC容器,并直接获取BeanDefinition,如代码清单1-16所示。
┃ 代码清单1-16 注解配置类驱动注册Person获取BeanDefinition
@Configuration
public class BeanDefinitionQuickstartConfiguration {
@Bean
public Person person() {
return new Person();
}
}
public class BeanDefinitionQuickstartBeanApplication {
public static void main(String[] args) throws Exception {
var ctx = new AnnotationConfigApplicationContext(
BeanDefinitionQuickstartConfiguration.class);
BeanDefinition personBeanDefinition = ctx.getBeanDefinition("person");
System.out.println(personBeanDefinition);
System.out.println(personBeanDefinition.getClass().getName());
}
}运行启动类的main方法,发现控制台打印的内容与前面的内容相比有很大的区别,具体包含以下4点。
(1)Bean 的类型是 Root Bean(ConfigurationClassBeanDefinition继承自RootBeanDefinition)。
(2)Bean的className消失不见。
(3)自动注入模式为AUTOWIRE_CONSTRUCTOR(根据Bean的构造方法注入)。
(4)factoryBean属性不为空:person由BeanDefinitionQuickstartConfiguration的person方法创建。
Root bean: class [null]; scope=; abstract=false; lazyInit=null; autowireMode=3; dependency Check=0; autowireCandidate=true; primary=false; factoryBeanName=beanDefinitionQuickstartConfiguration; factoryMethodName=person; initMethodNames=null; destroyMethodNames=[(inferred)]; defined in com.linkedbear.spring.definition.a_quickstart.config.BeanDefinitionQuickstartConfigurationorg.springframework.context.annotation.ConfigurationClassBeanDefinitionReader$ConfigurationClassBeanDefinition
为什么通过上述不同方式注册的Bean在生成BeanDefinition时会有较大差异?下面我们来归纳和解释这一现象。
考虑到大部分读者对BeanDefinition的理解可能停留在使用和功能层面,所以本小节只会进行解释,不会展开源码剖析。读者在该环节仅保留认识和印象即可。
(1)对于通过XML加载的BeanDefinition,其读取工具是XmlBeanDefinition Reader,它会解析XML配置文件,最终来到DefaultBeanDefinitionDocumentReader的doRegisterBeanDefinitions方法,根据XML配置文件中的Bean定义构造BeanDefinition。最底层创建BeanDefinition的位置在org.springframework. beans.factory.support.BeanDefinitionReaderUtils#createBeanDefinition。
(2)对于通过模式注解+组件扫描的方式构造的BeanDefinition,其扫描工具是ClassPathBeanDefinitionScanner,它会扫描指定包路径下包含特定模式注解的类。其核心工作方法是doScan方法,它会调用到父类ClassPathScanningCandidateComponent Provider的findCandidateComponents方法,创建ScannedGenericBeanDefinition并返回。
(3)通过配置类+@Bean注解的方式构造的BeanDefinition则相对复杂,涉及配置类的解析。而配置类的解析要追踪到ConfigurationClassPostProcessor的process ConfigBeanDefinitions方法,它会处理配置类并将其交给ConfigurationClassParser来解析,取出所有标注了@Bean的方法。随后这些方法又被ConfigurationClassBean DefinitionReader解析,最终在底层创建ConfigurationClassBeanDefinition并返回。
在讲解BeanDefinition的继承关系中我们提到过Root和Child的概念,这一对概念可以引出BeanDefinition的一个机制:合并。我们将结合1.3.3节的内容,深入介绍合并这个概念。
由1.3.3节可知,通过3种不同的方式注册Bean所形成的BeanDefinition通常都是相互独立的,例如代码清单1-17中编写的两个Bean最终形成的BeanDefinition几乎没有任何关联。
┃ 代码清单1-17 两个独立注册的Bean
<bean class="com.linkedbear.spring.basic_dl.b_bytype.bean.Person"></bean> <bean class="com.linkedbear.spring.basic_dl.b_bytype.dao.impl.DemoDaoImpl"/>
然而从BeanDefinition的结构中也能获悉,IOC容器中的Bean也存在父子关系。与Class的抽象、继承一样,<bean>标签中有一个abstract属性和parent属性,由这两个属性的相互配合就可以形成具有父子关系的BeanDefinition。下面通过一个示例来演示BeanDefinition的合并效果。
|
|
我们虚拟一个演示场景:所有的动物都归人饲养,而动物分很多种。以这个场景为基准进行编码,则需要构建几个实体类,这些实体类都不难。代码清单1-18提供了一个Person类、一个抽象类Animal和一个实现类Cat,其中Cat继承自Animal,并且为了方便打印出person的信息,toString方法并未直接使用IDE自动生成的格式,而是在此基础上稍做改造。
┃ 代码清单1-18 Person类、抽象类Animal和实现类Cat
public class Person {}
public abstract class Animal {
private Person person;
// getter setter ......
}
public class Cat extends Animal {
private String name;
// getter setter toString ......
}要想体现出BeanDefinition的合并效果,我们需要使用XML配置文件来注册Bean。注册Person的方式与之前完全一致,但是注册Animal和Cat时需要注意,由于Animal是一个抽象类,而抽象类在XML配置文件中无法直接声明,因此若我们强行编写,IDEA则会给出报错提示(如图1-6所示),程序也无法正确运行。

图1-6 抽象类无法直接定义为IOC容器中的Bean
正确的方式是在声明Animal的<bean>标签上标注abstract="true",即代表这个类不参与注册IOC容器中具体类型的Bean注册,而是作为一个抽象定义表示,如代码清单1-19所示。有了抽象的Animal后,再注册Cat时就可以直接引用,<bean>标签上还有一个parent属性,可以指定当前Bean继承哪个Bean中的信息,所以这里注册Cat时就需要标注parent="abstract-animal"。当声明parent后,cat中的person属性就不再需要显式声明,IOC容器会对这两个Bean的定义信息进行合并。
┃ 代码清单1-19 注册Animal与Cat
<bean id="abstract-animal" class="com.linkedbear.spring.definition.b_merge.bean.Animal"
abstract="true">
<property name="person" ref="person"/>
</bean>
<bean id="cat" class="com.linkedbear.spring.definition.b_merge.bean.Cat" parent="abstract-animal">
<property name="name" value="咪咪"/>
</bean>最后使用XML配置文件驱动IOC容器,随后取出cat的BeanDefinition并打印,如代码清单1-20所示。从打印的结果中可以发现,虽然cat中的person被成功注入,但是打印的BeanDefinition是一个Generic Bean,而且还可以看到它的parent信息("abstract-animal")。这个结果其实并不是BeanDefinition合并后的结果,也就是说,我们并没有获取到正确且完整的BeanDefinition。
┃ 代码清单1-20 获取Cat的BeanDefinition
public static void main(String[] args) throws Exception {
var ctx = new ClassPathXmlApplicationContext("definition/definition-merge.xml");
Cat cat = (Cat) ctx.getBean("cat");
System.out.println(cat);
BeanDefinition catDefinition = ctx.getBeanFactory().getBeanDefinition("cat");
System.out.println(catDefinition);
}
Cat{name=咪咪, person='com.linkedbear.spring.definition.b_merge.bean.Person@6b26e945'}
Generic bean with parent 'abstract-animal': class [com.linkedbear.spring.definition. b_merge.bean.Cat]; scope=; ......以Debug的形式重新运行main方法,在获取到的BeanDefinition中我们可以看到,内部的属性赋值信息中只包含name,没有person属性,如图1-7所示,这也证明了刚才我们所说的获取合并信息错误。要想获得合并后的完整BeanDefinition,需要用到BeanFactory的另一个方法:getMergedBeanDefinition。这个方法的名称已经带有“合并”的意思,它来自ConfigurableBeanFactory,用于合并本身的BeanDefinition信息与继承的父BeanDefinition信息然后返回该信息。

图1-7 直接获取的BeanDefinition中属性赋值信息不全
换用getMergedBeanDefinition方法后重新运行main方法,我们发现此时控制台打印的BeanDefinition类型变为RootBeanDefinition,且没有出现与parent相关的信息;如果此时再以Debug的形式运行main方法,就可以发现此时的propertyValues中出现了两个属性键值对,如图1-8所示,证明合并成功。
Root bean: class [com.linkedbear.spring.definition.b_merge.bean.Cat]; scope=singleton; abstract=false; lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodNames=null; destroyMethodNames=null; defined in class path resource [definition/definition-merge.xml]
图1-8 merge后的BeanDefinition包含全部的属性配置信息
BeanDefinition的基本使用已经演示完毕,读者可能会产生疑问(也有可能是醒悟):Spring Framework为什么会设计BeanDefinition这样一个复杂且难理解的结构?直接注册Bean有什么缺陷?有关这个问题,在不同阶段的学习中,每个人的理解和回答不尽相同。在本节内容结束之前,笔者希望能借助BeanDefinition传达一个设计理念:定义信息 → 实例。该设计理念在1.2节已经反复解释过,之所以要强化加深,是因为理解BeanDefinition对于后续更深入地理解IOC容器和工作原理有至关重要的作用。
如同平时先编写类,再使用new关键字创建对象,Spring Framework在面对应用程序时也需要对其中的Bean进行定义抽取。只有抽取成可以统一类型/格式的模型,才能在后续Bean的管理中进行统一管理,抑或是对特定的Bean进行特殊化处理。这一统一类型的最终落点就是BeanDefinition这个抽象化的模型。若读者仅一知半解,仍然对BeanDefinition的设计有疑惑,那么在学完本章和第2章后,自会对这个问题有更深刻的理解。
BeanDefinition只是一个描述Bean的信息载体。之所以能从DefaultListable BeanFactory中获取BeanDefinition,是因为DefaultListableBeanFactory除了保存和管理Bean,还要对BeanDefinition进行统一管理,而统一管理的核心来自另一个接口:BeanDefinitionRegistry。
首先,让我们简单了解BeanDefinitionRegistry的设计和构造。由于官方文档中并没有提及BeanDefinitionRegistry的设计,因此我们只尝试从javadoc中获取一些信息。结合javadoc和源码,我们可以总结出BeanDefinitionRegistry的以下4个特点。
|
Interface for registries that hold bean definitions, for example RootBeanDefinition and ChildBeanDefinition instances. Typically implemented by BeanFactories that internally work with the AbstractBeanDefinition hierarchy. This is the only interface in Spring's bean factory packages that encapsulates registration of bean definitions. The standard BeanFactory interfaces only cover access to a fully configured factory instance. Spring's bean definition readers expect to work on an implementation of this interface. Known implementors within the Spring core are DefaultListableBeanFactory and GenericApplicationContext.
|
Registry有注册表的含义,可类比Windows的注册表——存放了Windows系统中的应用和设置信息。沿此思路,BeanDefinitionRegistry中存放的就应该是BeanDefinition的设置信息。其实Spring Framework底层对于BeanDefinition的注册表的设计,本质上就是一个Map,如代码清单1-21所示。
┃ 代码清单1-21 存放BeanDefinition的容器
// 源自DefaultListableBeanFactory private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);
Registry还有注册器的含义。既然Map有基本的增删改查操作,那作为BeanDefinition的注册器,自然也就有BeanDefinition的注册功能。BeanDefinitionRegistry中有3个方法,刚好对应了BeanDefinition的增、删、查,如代码清单1-22所示。
┃ 代码清单1-22 BeanDefinitionRegistry方法节选
void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
throws BeanDefinitionStoreException;
void removeBeanDefinition(String beanName) throws NoSuchBeanDefinitionException;
BeanDefinition getBeanDefinition(String beanName) throws NoSuchBeanDefinitionException;笔者尝试用一个例子来引导读者理解BeanDefinitionRegistry的支撑作用。入门篇的4.4节讲解ImportBeanDefinitionRegistrar时,我们操纵接口方法registerBean Definitions中第2个参数BeanDefinitionRegistry的API完成了“服务员”类的注册,而BeanDefinition创建完毕后需要注册到IOC容器中,由IOC容器负责将BeanDefinition转换为对应的bean对象。所以在这个过程中,BeanDefinitionRegistry会支撑ImportBeanDefinitionRegistrar完成它的BeanDefinition注册工作。
注意标题的措辞是“主要实现”而不是“唯一实现”,这是因为BeanDefinitionRegistry除了有常用的实现类DefaultListableBeanFactory,还有一个不常用的实现类SimpleBeanDefinitionRegistry。因其连内部的IOC容器都没有,仅仅是一个BeanDefinition Registry的表面实现,因此它几乎不会被使用,没有进入大众的视野。
可能部分读者借助IDE发现很多ApplicationContext也实现了BeanDefinition Registry,所以认为ApplicationContext也能管理BeanDefinition。需要区分的是,ApplicationContext本身不负责管理Bean,而是通过内部组合一个DefaultListable BeanFactory来实现,所以ApplicationContext对BeanDefinition的操作也转交给内部的DefaultListableBeanFactory来实现。
最后简单总结BeanDefinitionRegistry:BeanDefinitionRegistry是维护BeanDefinition的注册中心,内部存放了IOC容器中Bean的定义信息,同时也是支撑其他组件和动态注册Bean的重要组件。在Spring Framework中,BeanDefinitionRegistry的实现类是DefaultListableBeanFactory。
BeanDefinitionRegistry内部的设计没有太多可以剖析的细节,我们的主要目标是掌握如何利用BeanDefinitionRegistry去维护BeanDefinition。
|
|
我们目前接触的注册BeanDefinition的方式就是借助ImportBeanDefinition Registrar,读者可以回看入门篇的4.4.6节,或者参考本小节对应的代码(位于com. linkedbear.spring.definition.c_registry包下),此处不再赘述。
BeanDefinitionRegistry除了能给IOC容器中添加BeanDefinition,还可以移除一些特定的BeanDefinition。移除操作会在Bean的实例化之前进行,目的是阻止IOC容器创建对应的Bean。移除BeanDefinition的方式比较特殊,涉及一个第2章才会讲解的API(BeanFactoryPostProcessor),此处仅将关注点放到移除BeanDefinition的动作上即可。
(1)声明特殊的Person
演示本节内容使用的Person之前,需要添加一个age属性,它会在后面的演示过程中作为判断依据。此外,均使用常规的实体类声明写法,如代码清单1-23所示。
┃ 代码清单1-23 Person中附带age属性
public class Person {
private String name;
private Integer age;
// getter setter toString
}(2)编写配置文件
接下来我们要注册两个Person对象,分别是一个成年人和一个儿童。由1.3.3节最后总结的BeanDefinition注册方式与实现类型可知,如果此处使用注解配置类的方式注册Bean(借助@Bean注解),生成的BeanDefinition将无法获取beanClassName(也无法获取PropertyValues),故此处选用XML配置文件的方式注册Bean,如代码清单1-24所示。
┃ 代码清单1-24 配置文件声明注册两个Person
<?xml version="1.0" encoding="UTF-8"?>
<beans ...>
<bean id="dazhuang" class="com.linkedbear.spring.definition.d_removedefinition.bean.Person">
<property name="name" value="大壮"/>
<property name="age" value="24"/>
</bean>
<bean id="xiaoming" class="com.linkedbear.spring.definition.d_removedefinition.bean.Person">
<property name="name" value="小明"/>
<property name="age" value="8"/>
</bean>
<!-- 注意此处要开启组件扫描 -->
<context:component-scan
base-package="com.linkedbear.spring.definition.d_removedefinition.config"/>
</beans>(3)编写移除BeanDefinition的组件
移除BeanDefinition时需要用到后置处理器组件BeanFactoryPostProcessor,这个组件可以在XML配置文件和注解配置类都加载到IOC容器后修改IOC容器中某些BeanDefinition的属性,抑或是直接移除BeanDefinition。为此,我们可以编写一个RemoveBeanDefinitionPostProcessor,实现BeanFactoryPostProcessor接口,并重写必要的postProcessBeanFactory方法。注意该方法的入参是一个Configurable ListableBeanFactory,毫无疑问,它的唯一实现是DefaultListableBeanFactory,而DefaultListableBeanFactory本身又实现了BeanDefinitionRegistry接口,所以相当于可以直接将beanFactory强转为BeanDefinitionRegistry类型。得到BeanDefinitionRegistry后就可以假定一个移除规则:小于18岁的Person会被移除。编写方式如代码清单1-25所示。另外,注意RemoveBeanDefinitionPostProcessor上要标注@Component注解,否则将不会生效。
┃ 代码清单1-25 移除BeanDefinition的后置处理器
@Component
public class RemoveBeanDefinitionPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
// 获取IOC容器中的所有BeanDefinition
for (String beanDefinitionName : beanFactory.getBeanDefinitionNames()) {
// 判断BeanDefinition对应的Bean是否为Person类型
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanDefinitionName);
if (Person.class.getName().equals(beanDefinition.getBeanClassName())) {
// 判断Person的年龄是否大于等于18,小于18的会被移除
TypedStringValue age = (TypedStringValue) beanDefinition
.getPropertyValues(). get("age");
if (Integer.parseInt(age.getValue()) < 18) {
// 移除BeanDefinition
registry.removeBeanDefinition(beanDefinitionName);
}
}
}
}
}(4)测试获取“小明”
使用XML配置文件驱动IOC容器,随后从IOC容器中尝试获取dazhuang和xiaoming,如代码清单1-26所示。运行main方法之后可以发现可以正常获取dazhuang,但是获取xiaoming时会打印NoSuchBeanDefinitionException,这说明xiaoming对应的BeanDefinition已经被移除,无法创建对应的Person实例。
┃ 代码清单1-26 测试获取“小明”
public class RemoveBeanDefinitionApplication {
public static void main(String[] args) throws Exception {
var ctx = new ClassPathXmlApplicationContext("definition/remove-definitions.xml");
Person dazhuang = (Person) ctx.getBean("dazhuang");
System.out.println(dazhuang);
Person xiaoming = (Person) ctx.getBean("xiaoming");
System.out.println(xiaoming);
}
}至此,我们对BeanDefinitionRegistry已有比较清晰的认识,其设计本身也并不复杂。随着后续对IOC容器与原理的深入学习,我们还会频繁与它打交道。
本章从“元”这个概念开始,逐一讲解元编程、元信息、配置元信息等多个复杂概念。这些概念对后续理解和深入学习IOC容器的原理有重大帮助。
对于IOC容器的Bean而言,描述它们的元信息是BeanDefinition,其中记录了包括Bean的类型、id、作用域、属性赋值和依赖注入等多方面信息。IOC容器利用BeanDefinition即可解析并生成相应的Bean。
IOC容器中的Bean集中保存在BeanDefinitionRegistry,也即DefaultListable BeanFactory中。BeanDefinitionRegistry的设计与容器类似,都有保存、获取、移除动作。借助BeanDefinitionRegistry可以在程序中动态添加新的BeanDefinition,也可以在IOC容器没有集中创建Bean之前,修改BeanDefinition的信息甚至移除BeanDefinition。