- 示例工程
- 文档
【免费下载链接】spring-reading
涵盖了 Spring 框架的核心概念和关键功能,包括控制反转(IOC)容器的使用,面向切面编程(AOP)的原理与实践,事务管理的方式与实现,Spring MVC 的流程与控制器工作机制,以及 Spring 中数据访问、安全、Boot 自动配置等方面的深入研究。此外,它还包含了 Spring 事件机制的应用、高级主题如缓存抽象和响应式编程,以及对 Spring 源码的编程风格与设计模式的深入探讨。
导读
PropertiesBeanDefinitionReader是 Spring 框架中一条"轻量级"的 Bean 定义读取通道:它允许开发者把 Bean 的类名、作用域、懒加载开关以及属性值等配置信息以key=value的纯文本形式写在.properties文件中,再由该读取器解析为BeanDefinition并注册进 IOC 容器,从而实现"配置与代码分离"的动态 Bean 管理。本篇基于 spring-reading 仓库中的 PropertiesBeanDefinitionReader 专题,完整讲解其工作原理、属性文件格式、可运行的实践示例、关键源码调用链,并结合仓库源码佐证每个环节的真实行为。读完本文,你将掌握如何脱离 XML 与注解,仅凭一份属性文件就把 Bean 定义"喂"给DefaultListableBeanFactory,并理解它与BeanDefinition、BeanDefinitionRegistry等核心组件之间的协作关系。
一、知识储备
在深入PropertiesBeanDefinitionReader之前,先补齐三个前置概念,它们分别回答了"读取什么资源""产出什么元数据""注册到哪里去"。
1.Resource:资源的统一抽象
Resource代表一个可访问的资源,可以是文件系统文件、类路径上的文件、URL 等,它向调用方屏蔽了底层来源差异,统一提供getInputStream()等访问方法。PropertiesBeanDefinitionReader正是通过Resource来定位属性文件的位置,例如ClassPathResource("bean-definitions.properties")表示从类路径根目录读取该文件。
关于
Resource接口的详细设计,可参考仓库中的 Resource 专题。
2.BeanDefinition:Bean 的配置元数据
BeanDefinition是 Spring 描述和管理 Bean 配置的核心模型,记录了类名、作用域、依赖关系、构造参数、属性值、初始化/销毁方法等全部元信息。PropertiesBeanDefinitionReader的核心任务之一,就是把属性文件中的键值对翻译成一个个BeanDefinition,再注册进容器——它是"配置文本"与"容器模型"之间的桥梁。
关于该接口的完整定义与各实现类(
GenericBeanDefinition、RootBeanDefinition、ChildBeanDefinition等),可参考仓库中的 BeanDefinition 专题。
3.BeanDefinitionRegistry:Bean 定义的注册表
BeanDefinitionRegistry是负责注册、移除、查询 Bean 定义的接口,DefaultListableBeanFactory与GenericApplicationContext都是它的实现者。Spring 官方注释明确指出:"Spring 的 Bean 定义读取器(Bean definition readers)希望工作在这个接口的实现之上",PropertiesBeanDefinitionReader正是这句话的典型例证——它的所有注册动作最终都落到getRegistry().registerBeanDefinition(...)上。
接口源码与注册语义细节可参考仓库中的 BeanDefinitionRegistry 专题。
二、基本描述
PropertiesBeanDefinitionReader是 Spring 框架中的一个组件,主要功能是从属性文件中加载 Bean 的配置信息,并将其解析、转化为 Spring 容器中的 Bean 定义。属性文件的格式通常是键值对:键表示 Bean 的名称(以及配置项),值表示 Bean 的类名或其他配置属性。
这一机制的价值在于:将 Bean 配置与应用程序代码、XML 配置文件彻底分离。开发者只需维护一份纯文本的属性文件,即可动态加载和管理 Bean 定义,从而增强应用程序的可扩展性与配置灵活性。相比 XML 的冗长标签、注解的编译期绑定,属性文件方式更加轻量、直观,也更容易被外部系统或运维脚本动态生成。
三、主要功能
PropertiesBeanDefinitionReader的完整工作流可以拆解为四个步骤:
加载属性文件通过
loadBeanDefinitions(Resource resource)方法加载属性文件。Resource可以是文件路径、类路径或其他资源标识符,用于指定包含 Bean 定义信息的属性文件位置。解析属性文件属性文件被加载后,读取器解析其中的内容,识别 Bean 的名称、类名以及其他配置属性(如作用域、懒加载标志、普通属性值等)。
创建 Bean 定义根据属性文件中的配置信息,创建相应的
BeanDefinition对象,其中包含 Bean 的名称、类、属性、依赖关系等定义信息。注册 Bean 定义将解析得到的
BeanDefinition注册到 Spring IOC 容器(BeanDefinitionRegistry)中,使这些 Bean 可以在应用程序中被实例化和使用。
这四个步骤构成了一个完整的"文本配置 → 容器元数据"流水线,全程不需要任何 XML 或注解参与。
四、属性文件格式详解
PropertiesBeanDefinitionReader使用标准的 Java.properties键值对格式,其约定的键结构为:
<beanName>.<属性项> = <值>1. 特殊键(控制 Bean 定义本身)
以下带( )包裹的键用于描述 Bean 定义级元信息,而非注入给 Bean 的属性:
| 键 | 作用 | 示例 |
|---|---|---|
(class) | 指定 Bean 的类名(全限定名) | myBean.(class)=com.xcs.spring.bean.MyBean |
(scope) | 指定 Bean 作用域 | myBean.(scope)=prototype |
(lazy-init) | 是否懒加载(true/false) | myBean.(lazy-init)=true |
(parent) | 指定父 Bean 定义名称(支持继承式配置) | myBean.(parent)=baseBean |
(abstract) | 标记为抽象定义,不参与实例化 | myBean.(abstract)=true |
(depends-on) | 指定依赖的 Bean 名称列表 | myBean.(depends-on)=dataSource |
(init-method)/(destroy-method) | 指定初始化 / 销毁方法名 | myBean.(init-method)=init |
(factory-bean)/(factory-method) | 指定工厂 Bean 与工厂方法 | myBean.(factory-method)=createInstance |
(autowire) | 指定自动装配模式 | myBean.(autowire)=byName |
以上特殊键的解析逻辑可以在registerBeanDefinition的源码中看到对应落点:该方法最终调用BeanDefinitionReaderUtils.createBeanDefinition(parent, className, getBeanClassLoader())创建定义,并依次设置scope、isAbstract、lazyInit、构造参数值(cas)与属性值(pvs),再注册进容器。
2. 普通键(注入 Bean 属性)
除特殊键外,其余形如myBean.message=hello world的键会被解析为 Bean 的属性注入项:点号后的部分(message)对应 JavaBean 的属性名,值(hello world)在 Bean 实例化后通过setMessage(...)写入。
3. 一个完整的示例文件
仓库中的 bean-definitions.properties 给出了可直接运行的最小示例:
myBean.(class)=com.xcs.spring.bean.MyBean myBean.message=hello world myBean.(lazy-init)=true myBean.(scope)=prototype该文件声明了一个名为myBean的 Bean:类为com.xcs.spring.bean.MyBean,注入属性message=hello world,懒加载开启,作用域为原型(prototype)。
五、最佳实践:从属性文件动态加载 Bean
仓库示例 PropertiesBeanDefinitionReaderDemo.java 展示了完整用法:创建一个 Spring Bean 工厂(DefaultListableBeanFactory)和一个属性文件读取器(PropertiesBeanDefinitionReader),然后从属性文件加载 Bean 定义,再通过 Bean 工厂获取 Bean 对象,从而实现动态加载和管理 Bean 配置。
public class PropertiesBeanDefinitionReaderDemo { public static void main(String[] args) { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); PropertiesBeanDefinitionReader reader = new PropertiesBeanDefinitionReader(beanFactory); // 从properties文件加载bean定义 reader.loadBeanDefinitions(new ClassPathResource("bean-definitions.properties")); // 获取bean System.out.println("myBean = " + beanFactory.getBean("myBean")); System.out.println("myBean = " + beanFactory.getBean("myBean")); } }配套的MyBean是一个带message属性及 getter/setter 的简单 JavaBean,并重写了toString()以便观察实例差异:
public class MyBean { private String message; public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } @Override public String toString() { return "MyBean{" + "message='" + message + '\'' + ", hashCode='0x" + Integer.toHexString(System.identityHashCode(this)).toUpperCase() + '\'' + '}'; } }完整源码位于 MyBean.java。
运行上述程序,输出如下:
myBean = MyBean{message='hello world', hashCode='0x6646153'} myBean = MyBean{message='hello world', hashCode='0x45DD4EDA'}运行结果揭示了两点关键信息:
- 属性注入生效:两次输出的
message均为hello world,说明属性文件中的myBean.message已被正确写入 Bean; - 原型作用域生效:两次输出中
hashCode(基于System.identityHashCode)不同,说明两次getBean("myBean")返回的是两个不同实例——这正是myBean.(scope)=prototype的预期行为。若将作用域改回默认的singleton,两次输出将是同一个实例。
至此,我们仅凭一份.properties文件就完成了"Bean 定义加载 + 属性注入 + 懒加载 + 作用域控制"的全套配置,全程无需 XML 与注解。
六、工作流程时序
下面这张时序图(出自仓库 README)清晰地呈现了从loadBeanDefinitions(resource)到容器注册的完整调用链:
七、源码分析
下面沿着时序图逐层剖析org.springframework.beans.factory.support.PropertiesBeanDefinitionReader的三个核心方法。
1.loadBeanDefinitions(Resource):入口方法
@Override public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException { return loadBeanDefinitions(new EncodedResource(resource), null); }入口方法将传入的Resource包装为EncodedResource(用于支持显式字符编码),并以null前缀委托给重载方法执行真正的加载逻辑。返回值为本次加载并注册的 Bean 定义数量。
2.loadBeanDefinitions(EncodedResource, prefix):加载与解析
public int loadBeanDefinitions(EncodedResource encodedResource, @Nullable String prefix) throws BeanDefinitionStoreException { // 如果启用了跟踪级别的日志,则输出加载属性文件的日志信息 if (logger.isTraceEnabled()) { logger.trace("Loading properties bean definitions from " + encodedResource); } // 创建 Properties 对象,用于存储属性文件中的键值对配置 Properties props = new Properties(); try { try (InputStream is = encodedResource.getResource().getInputStream()) { // 如果指定了编码,使用指定编码读取属性文件 if (encodedResource.getEncoding() != null) { getPropertiesPersister().load(props, new InputStreamReader(is, encodedResource.getEncoding())); } // 否则,使用默认编码读取属性文件 else { getPropertiesPersister().load(props, is); } } // 注册 Bean 定义,将属性文件中的配置转化为 Bean 定义并注册到容器中 int count = registerBeanDefinitions(props, prefix, encodedResource.getResource().getDescription()); // 如果启用了调试级别的日志,则输出已加载的 Bean 定义数量的日志信息 if (logger.isDebugEnabled()) { logger.debug("Loaded " + count + " bean definitions from " + encodedResource); } return count; } catch (IOException ex) { // 处理异常,如果在读取或注册过程中发生异常,抛出 BeanDefinitionStoreException 异常 throw new BeanDefinitionStoreException("Could not parse properties from " + encodedResource.getResource(), ex); } }要点拆解:
- 编码支持:
EncodedResource若携带显式编码(如 UTF-8),则通过InputStreamReader按指定编码读取,否则使用默认编码,这解决了属性文件包含非 ASCII 字符时的乱码问题; - 解析器可替换:实际读取动作由
getPropertiesPersister()返回的PropertiesPersister完成,该组件允许自定义属性文件解析策略; - 异常收敛:任何 IO 或解析失败都会统一包装为
BeanDefinitionStoreException抛出,保持了读取器对外异常语义的一致性; - 返回值语义:方法返回
registerBeanDefinitions的计数结果,供上层追踪"一次加载注册了多少 Bean 定义"。
3.registerBeanDefinitions(Map, prefix, resourceDescription):逐键识别与去重
public int registerBeanDefinitions(Map<?, ?> map, @Nullable String prefix, String resourceDescription) throws BeansException { if (prefix == null) { prefix = ""; } int beanCount = 0; // 遍历属性文件中的键值对配置 for (Object key : map.keySet()) { if (!(key instanceof String)) { throw new IllegalArgumentException("Illegal key [" + key + "]: only Strings allowed"); } String keyString = (String) key; if (keyString.startsWith(prefix)) { // 键的格式为:prefix<名称>.属性 String nameAndProperty = keyString.substring(prefix.length()); // 查找属性名之前的点号,忽略属性键中的点号。 int sepIdx ; int propKeyIdx = nameAndProperty.indexOf(PropertyAccessor.PROPERTY_KEY_PREFIX); if (propKeyIdx != -1) { sepIdx = nameAndProperty.lastIndexOf(SEPARATOR, propKeyIdx); } else { sepIdx = nameAndProperty.lastIndexOf(SEPARATOR); } if (sepIdx != -1) { String beanName = nameAndProperty.substring(0, sepIdx); if (logger.isTraceEnabled()) { logger.trace("Found bean name '" + beanName + "'"); } if (!getRegistry().containsBeanDefinition(beanName)) { // 如果还未注册该 Bean... // 注册 Bean 定义 registerBeanDefinition(beanName, map, prefix + beanName, resourceDescription); ++beanCount; } } else { // 忽略该键:它不是有效的 Bean 名称和属性, // 尽管它以所需的前缀开始。 if (logger.isDebugEnabled()) { logger.debug("Invalid bean name and property [" + nameAndProperty + "]"); } } } } // 返回已注册的 Bean 定义数量 return beanCount; }要点拆解:
- 前缀过滤:支持传入
prefix以只处理键名以该前缀开头的配置,实现"一段属性文件按前缀切分出多组 Bean"的批量管理; - Bean 名提取:以点号
.作为分隔符,取最左侧(lastIndexOf定位的最后一个分隔符之前的片段)作为 Bean 名,点号右侧视为属性项;同时借助PropertyAccessor.PROPERTY_KEY_PREFIX(即[)识别嵌套属性键,避免点号误切; - 幂等去重:通过
getRegistry().containsBeanDefinition(beanName)判断是否已注册,已注册的 Bean 会被跳过,避免重复定义; - 容错忽略:不含分隔符的键(如顶层无关配置)会被记录日志并忽略,而不是中断整个加载过程;
- 返回注册计数:每成功注册一个 Bean 定义
beanCount加一,作为方法返回值。
4.registerBeanDefinition(beanName, map, prefix, resourceDescription):构建并注册定义
protected void registerBeanDefinition(String beanName, Map<?, ?> map, String prefix, String resourceDescription) throws BeansException { // ... [代码部分省略以简化] try { AbstractBeanDefinition bd = BeanDefinitionReaderUtils.createBeanDefinition( parent, className, getBeanClassLoader()); bd.setScope(scope); bd.setAbstract(isAbstract); bd.setLazyInit(lazyInit); bd.setConstructorArgumentValues(cas); bd.setPropertyValues(pvs); getRegistry().registerBeanDefinition(beanName, bd); } catch (ClassNotFoundException ex) { throw new CannotLoadBeanClassException(resourceDescription, beanName, className, ex); } catch (LinkageError err) { throw new CannotLoadBeanClassException(resourceDescription, beanName, className, err); } }要点拆解:
- 定义创建:借助
BeanDefinitionReaderUtils.createBeanDefinition(parent, className, getBeanClassLoader())创建AbstractBeanDefinition,其中parent与className分别来自(parent)、(class)特殊键; - 元信息装配:依次写入
scope、isAbstract、lazyInit、构造参数值(cas)与属性值(pvs),对应属性文件中的(scope)、(abstract)、(lazy-init)、构造参数项与普通属性项; - 类型加载失败处理:
ClassNotFoundException与LinkageError分别被包装为CannotLoadBeanClassException抛出,并携带resourceDescription(资源描述)与 Bean 名,便于在容器启动阶段快速定位配置错误; - 最终注册:一切就绪后调用
getRegistry().registerBeanDefinition(beanName, bd),把定义真正写入BeanDefinitionRegistry(示例中即DefaultListableBeanFactory)。
八、与其他组件的关系
PropertiesBeanDefinitionReader并非孤立组件,它与 Spring Bean 体系中的多个角色协同工作:
独立应用程序在一些独立运行的 Spring 应用程序中,可以使用
PropertiesBeanDefinitionReader动态加载 Bean 定义,使配置更加灵活,应用可根据需要动态配置和管理 Bean。基于属性文件的配置一些应用整体采用属性文件配置方式,将各种 Bean 的配置信息集中存放于属性文件。
PropertiesBeanDefinitionReader负责加载这些配置并转化为 Spring 的 Bean 定义。BeanDefinitionRegistry/DefaultListableBeanFactory读取器面向BeanDefinitionRegistry编程,DefaultListableBeanFactory是其在仓库示例中的落地实现,承担定义存储与后续实例化职责。BeanDefinition体系读取器的产出物是BeanDefinition(具体为AbstractBeanDefinition子类),其语义与BeanDefinition专题(spring-bean-beanDefinition)中描述的定义模型完全一致。同族读取器Spring 还提供了面向其他配置形态的同族读取器:如 XML 形态的 XmlBeanDefinitionReader、Groovy 形态的
GroovyBeanDefinitionReader、注解形态的AnnotatedBeanDefinitionReader。PropertiesBeanDefinitionReader与它们共同实现了"外部配置 → BeanDefinition"的标准化读取范式。
九、常见问题
1. 如何使用PropertiesBeanDefinitionReader?
创建一个实例并与 Spring 的 Bean 工厂(如DefaultListableBeanFactory)结合使用:构造时传入工厂(它同时充当BeanDefinitionRegistry),随后调用loadBeanDefinitions方法加载属性文件中的配置,Bean 定义即被注册到容器中。
2. 属性文件的格式是什么?
采用标准的键值对(key=value)形式。键由"Bean 名 + 点号 + 配置项"构成,值即配置信息。文件可包含 Bean 的类((class))、作用域((scope))、懒加载((lazy-init))、普通属性(如myBean.message=...)等多项配置。
3. 如何配置懒加载和作用域?
使用特殊键(lazy-init)和(scope)。例如myBean.(lazy-init)=true将 Bean 配置为懒加载(首次请求时才实例化),myBean.(scope)=prototype将作用域配置为原型(每次获取均返回新实例)。
4. 什么是属性文件中的 Bean 名称?
Bean 名称即键中"第一个点号之前"的片段。例如myBean.(class)=com.example.MyBean中的 Bean 名称是myBean,同一 Bean 的所有配置项共享该前缀。
5. 为什么使用PropertiesBeanDefinitionReader?
它使应用程序更加灵活:配置信息从代码中分离,支持运行期动态加载与批量管理 Bean 定义;同时纯文本格式便于外部系统生成与运维脚本维护,非常适合需要高度可配置性的场景。作为对比,同一仓库的 XmlBeanDefinitionReader 面向 XML 形态、AnnotatedBeanDefinitionReader面向注解形态,三者可按配置风格各取所需。
6. 加载时 Bean 类不存在会怎样?
registerBeanDefinition在通过getBeanClassLoader()解析类时若抛出ClassNotFoundException或LinkageError,会统一转换为CannotLoadBeanClassException抛出,并附带资源描述与 Bean 名,便于定位配置问题;属性文件读取本身的 IO 异常则会被包装为BeanDefinitionStoreException。
- 示例工程
- 文档
【免费下载链接】spring-reading
涵盖了 Spring 框架的核心概念和关键功能,包括控制反转(IOC)容器的使用,面向切面编程(AOP)的原理与实践,事务管理的方式与实现,Spring MVC 的流程与控制器工作机制,以及 Spring 中数据访问、安全、Boot 自动配置等方面的深入研究。此外,它还包含了 Spring 事件机制的应用、高级主题如缓存抽象和响应式编程,以及对 Spring 源码的编程风格与设计模式的深入探讨。
相关推荐
spring-reading 源码解析:XmlBeanDefinitionReader 如何把 XML 配置解析并注册成 Bean 定义
spring reading 源码解析:XmlBeanDefinitionReader 如何把 XML 配置解析并注册成 Bean 定义 XmlBeanDefi
示例工程文档Java Collection Hierarchy 深度解析:掌握 JCF 接口、实现类与数据结构选型
Java Collection Hierarchy 深度解析:掌握 JCF 接口、实现类与数据结构选型 导读 本文以 System Design 101 仓库中
后端文档教程Spring Boot @ConditionalOnBean 源码解析:条件注解如何决定 Bean 的加载
Spring Boot @ConditionalOnBean 源码解析:条件注解如何决定 Bean 的加载 本文以 Spring Boot 自动配置中的 @Co
文档教程知识库
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考