@PropertySource 注解深度解析:Spring 外部属性源加载的实战与源码原理
示例工程文档【免费下载链接】spring-reading涵盖了 Spring 框架的核心概念和关键功能包括控制反转IOC容器的使用面向切面编程AOP的原理与实践事务管理的方式与实现Spring MVC 的流程与控制器工作机制以及 Spring 中数据访问、安全、Boot 自动配置等方面的深入研究。此外它还包含了 Spring 事件机制的应用、高级主题如缓存抽象和响应式编程以及对 Spring 源码的编程风格与设计模式的深入探讨。项目地址https://gitcode.com/GitHub_Trending/sp/spring-reading点击查看免费下载PropertySource是 Spring 3.1 引入的核心注解用于将外部属性文件如.properties、.xml甚至借助自定义工厂支持.yml中的键值对加载进 Spring 的Environment让应用代码能够统一、便捷地访问外部配置。本文以 spring-reading 仓库中 spring-annotation-propertySource 模块为依托先通过一个可运行的 YAML 属性加载示例掌握实战用法再沿着AnnotationConfigApplicationContext→refresh()→ConfigurationClassParser的调用链逐层拆解PropertySource从“注解声明”到“属性源注册”的底层处理逻辑最后给出 9 条高价值的注意事项帮助你写出健壮、可维护的外部配置加载方案。一、注解描述PropertySource是什么PropertySource用于指定外部的属性文件从而将该文件中的键值对加载到 Spring 的Environment中。这样应用程序就可以轻松地访问和使用这些属性值。它通常与Configuration注解类一起使用为整个 Spring 环境提供配置属性。例如Configuration PropertySource(classpath:/com/myco/app.properties) public class AppConfig { // ... }通过上面的配置app.properties文件中的所有属性都可以在 SpringEnvironment中使用。二、注解源码与属性详解PropertySource是 Spring 框架自 3.1 版本开始引入的核心注解since 3.1允许指定外部属性文件如.properties或.xml文件并将文件中的键值对加载到 Spring 的Environment中。其完整源码如下Target(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) Documented Repeatable(PropertySources.class) public interface PropertySource { /** * 指定此属性源的名称。如果省略工厂将根据底层资源生成一个名称。 */ String name() default ; /** * 指定要加载的属性文件的资源位置。 * 支持传统的属性文件和XML格式例如classpath:/com/myco/app.properties 或 file:/path/to/file.xml。 * 不支持资源位置的通配符。 * ${...} 占位符将根据已在 Environment 中注册的所有属性源进行解析。 * 每个位置将按声明的顺序添加到封闭的 Environment 作为它自己的属性源。 */ String[] value(); /** * 指示是否应忽略找不到的属性资源。 * 如果属性文件是完全可选的则 true 是合适的。 * 默认值为 false。 */ boolean ignoreResourceNotFound() default false; /** * 为给定的资源指定一个特定的字符编码例如 UTF-8。 */ String encoding() default ; /** * 指定一个自定义的 PropertySourceFactory。 * 默认情况下将使用标准资源文件的默认工厂。 */ Class? extends PropertySourceFactory factory() default PropertySourceFactory.class; }对注解的五个成员逐一说明其语义与默认值成员默认值作用说明name空串实际按未指定处理指定该属性源的名称。省略时由属性源工厂根据底层资源自动生成名称例如基于资源描述。显式命名可以让你在Environment中对属性源进行精确控制与覆盖value无必填指定要加载的属性文件的资源位置类型为String[]可一次声明多个位置如classpath:/com/myco/app.properties或file:/path/to/file.xml。注意不支持资源位置通配符${...}占位符会依据Environment中已注册的所有属性源解析每个位置会按声明顺序各自作为一个属性源加入EnvironmentignoreResourceNotFoundfalse属性文件找不到时是否忽略。false表示找不到即抛出异常true适用于属性文件完全可选的场景Spring 会记录日志并继续运行encoding空串按未指定处理为给定资源指定字符编码如UTF-8。Spring 4.3 起支持对包含非 ASCII 字符的配置文件尤为重要factoryPropertySourceFactory.class指定自定义的PropertySourceFactory用于解析和加载属性源。默认使用标准资源文件的默认工厂DefaultPropertySourceFactory值得注意的源码细节Repeatable(PropertySources.class)使得同一配置类上可以重复标注多个PropertySource也可使用容器注解PropertySources一次性声明多个属性源Target(ElementType.TYPE)表明它只能标注在类型类、接口、枚举上不能用于方法或字段。三、主要功能加载属性文件指定一个或多个外部属性文件如.properties或.xml文件并将其加载到 Spring 的上下文中。声明资源位置通过value属性指定外部属性文件的位置如classpath:/com/myco/app.properties或file:/path/to/file.xml。属性覆盖使用多个PropertySource或PropertySources时后声明的属性源会覆盖先声明的属性源中的同名属性。处理资源找不到的情况通过ignoreResourceNotFound属性指定资源缺失时是否抛出异常适合可选配置文件场景。指定文件编码通过encoding属性为资源文件指定字符编码如UTF-8。自定义属性源工厂通过factory属性指定自定义PropertySourceFactory定制属性解析与加载逻辑。支持占位符解析属性值中可使用${...}占位符这些占位符会按Environment中已加载的其他属性进行解析。与Environment交互属性文件加载进Environment后可通过注入Environment查询和使用这些属性。四、最佳实践从 YAML 文件加载属性本仓库 spring-annotation-propertySource 模块给出了一个完整的、可直接运行的最小示例展示了如何从 YAML 文件加载属性。与常规的.properties不同该示例借助 Spring Boot 的默认属性源工厂链路成功读取了classpath下的 YAML 文件。1. 启动类入口上下文环境使用AnnotationConfigApplicationContext使用 Java 注解配置 Spring 容器的方式构造参数给定MyConfiguration组件类然后从Environment中获取两个属性apiVersion和kind。public class PropertySourceApplication { public static void main(String[] args) { AnnotationConfigApplicationContext context new AnnotationConfigApplicationContext(MyConfiguration.class); System.out.println(apiVersion context.getEnvironment().getProperty(apiVersion)); System.out.println(kind context.getEnvironment().getProperty(kind)); } }对应仓库源码PropertySourceApplication.java2. 配置类MyConfiguration类标注Configuration表示它是一个基于 Java 的 Spring 配置类同时使用PropertySource注解加载名为my-application.yml的外部属性文件。Configuration PropertySource(classpath:my-application.yml) public class MyConfiguration { }对应仓库源码MyConfiguration.java3. 属性文件内容my-application.yml文件位于src/main/resources下即类路径根目录内容如下apiVersion: v1 kind: ConfigMap对应仓库资源my-application.yml4. 运行结果运行启动类后控制台输出如下说明属性已成功从 YAML 文件加载并可通过EnvironmentAPI 使用apiVersion v1 kind ConfigMap5. 构建与运行方式本模块是 Maven 多模块项目的一部分父工程 pom.xml 中定义了spring.version为5.2.15.RELEASE、spring.boot.version为2.3.12.RELEASE、Java 版本为 11。进入模块目录后即可编译运行mvn compile mvn exec:java -Dexec.mainClasscom.xcs.spring.PropertySourceApplication或直接在 IDE 中运行PropertySourceApplication#main方法。模块自身仅继承了父工程依赖见 spring-annotation-propertySource/pom.xml 与 spring-annotation/pom.xml。五、底层时序PropertySource的完整处理流程从注解声明到属性真正进入EnvironmentPropertySource经历了如下调用链时序图来自原文档结合仓库源码验证六、源码分析从refresh()到属性源注册下面沿调用链逐层分析代码均来自 Spring 框架源码与本仓库模块的演示代码对应。1. 上下文初始化AnnotationConfigApplicationContext在AnnotationConfigApplicationContext#AnnotationConfigApplicationContext构造函数中依次执行三个步骤重点在refresh()public AnnotationConfigApplicationContext(Class?... componentClasses) { this(); register(componentClasses); refresh(); }2. 刷新上下文AbstractApplicationContext#refreshrefresh()是整个容器启动的核心模板方法其中invokeBeanFactoryPostProcessors(beanFactory)负责调用在上下文中注册为 bean 的工厂处理器PropertySource正是在这一阶段被解析的Override public void refresh() throws BeansException, IllegalStateException { // ... [代码部分省略以简化] // 调用在上下文中注册为bean的工厂处理器 invokeBeanFactoryPostProcessors(beanFactory); // ... [代码部分省略以简化] }3. 委托调用invokeBeanFactoryPostProcessorsAbstractApplicationContext#invokeBeanFactoryPostProcessors将调用委托给PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors()protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) { PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors()); // ... [代码部分省略以简化] }4. 注册后置处理器invokeBeanDefinitionRegistryPostProcessors在PostProcessorRegistrationDelegate#invokeBeanFactoryPostProcessors中首先调用BeanDefinitionRegistryPostProcessorBeanFactoryPostProcessor的子接口专门用于在所有其他 bean 定义加载之前修改默认 bean 定义public static void invokeBeanFactoryPostProcessors( ConfigurableListableBeanFactory beanFactory, ListBeanFactoryPostProcessor beanFactoryPostProcessors) { // ... [代码部分省略以简化] invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup()); // ... [代码部分省略以简化] }invokeBeanDefinitionRegistryPostProcessors循环调用实现该接口的postProcessBeanDefinitionRegistry(registry)方法private static void invokeBeanDefinitionRegistryPostProcessors( Collection? extends BeanDefinitionRegistryPostProcessor postProcessors, BeanDefinitionRegistry registry, ApplicationStartup applicationStartup) { for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) { StartupStep postProcessBeanDefRegistry applicationStartup.start(spring.context.beandef-registry.post-process) .tag(postProcessor, postProcessor::toString); postProcessor.postProcessBeanDefinitionRegistry(registry); postProcessBeanDefRegistry.end(); } }5. 配置类后处理ConfigurationClassPostProcessorConfigurationClassPostProcessor#postProcessBeanDefinitionRegistry调用processConfigBeanDefinitions处理并注册配置类中定义的 beansOverride public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { // ... [代码部分省略以简化] processConfigBeanDefinitions(registry); }processConfigBeanDefinitions负责配置类的解析与验证确保所有 beans 正确注册public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) { // ... [代码部分省略以简化] // 步骤1创建一个用于解析配置类的解析器 ConfigurationClassParser parser new ConfigurationClassParser( this.metadataReaderFactory, this.problemReporter, this.environment, this.resourceLoader, this.componentScanBeanNameGenerator, registry); // 步骤2初始化候选配置类集合以及已解析配置类集合 SetBeanDefinitionHolder candidates new LinkedHashSet(configCandidates); SetConfigurationClass alreadyParsed new HashSet(configCandidates.size()); // 步骤3循环处理所有候选配置类直至没有候选类为止 do { // 步骤3.1 解析配置类 parser.parse(candidates); // 步骤3.2 验证配置类 parser.validate(); // ... [代码部分省略以简化] } while (!candidates.isEmpty()); // ... [代码部分省略以简化] }6. 解析配置类ConfigurationClassParser#parseparse(SetBeanDefinitionHolder)遍历所有配置类候选者对每个带注解的 Bean 定义进行解析查找Bean方法、组件扫描指令等parse(metadata, beanName)则将注解元数据与 Bean 名称转化为ConfigurationClass并交给processConfigurationClasspublic void parse(SetBeanDefinitionHolder configCandidates) { // ... [代码部分省略以简化] parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName()); // ... [代码部分省略以简化] } protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException { processConfigurationClass(new ConfigurationClass(metadata, beanName), DEFAULT_EXCLUSION_FILTER); }processConfigurationClass先递归处理配置类及其父类层次结构再把处理结果放入已解析配置类映射protected void processConfigurationClass(ConfigurationClass configClass, PredicateString filter) throws IOException { // ... [代码部分省略以简化] // 步骤1递归地处理配置类及其超类层次结构 SourceClass sourceClass asSourceClass(configClass, filter); do { sourceClass doProcessConfigurationClass(configClass, sourceClass, filter); } while (sourceClass ! null); // 步骤2将处理后的配置类放入映射中 this.configurationClasses.put(configClass, configClass); }7. 处理PropertySource注解doProcessConfigurationClassdoProcessConfigurationClass遍历配置类上的PropertySource含可重复注解与PropertySources容器注解。关键约束是只有当前Environment是ConfigurableEnvironment实例时才处理该注解否则记录日志并忽略protected final SourceClass doProcessConfigurationClass( ConfigurationClass configClass, SourceClass sourceClass, PredicateString filter) throws IOException { // 处理任何 PropertySource 注解 for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), PropertySources.class, org.springframework.context.annotation.PropertySource.class)) { // 检查当前环境是否为ConfigurableEnvironment的实例 if (this.environment instanceof ConfigurableEnvironment) { // 如果是则处理该属性源 processPropertySource(propertySource); } else { // 否则记录一条信息说明正在忽略PropertySource注解 logger.info(Ignoring PropertySource annotation on [ sourceClass.getMetadata().getClassName() ]. Reason: Environment must implement ConfigurableEnvironment); } } // 没有超类 - 处理完成 return null; }8. 处理属性源属性processPropertySourceprocessPropertySource先解析注解的name、encoding、value、ignoreResourceNotFound、factory等属性再逐一加载每个位置的文件并创建属性源/** * 处理给定的PropertySource注解属性。 * * param propertySource PropertySource注解的属性 * throws IOException 如果处理时发生IO异常 */ private void processPropertySource(AnnotationAttributes propertySource) throws IOException { // 获取name属性如果没有指定设置为null String name propertySource.getString(name); if (!StringUtils.hasLength(name)) { name null; } // 获取encoding属性如果没有指定设置为null String encoding propertySource.getString(encoding); if (!StringUtils.hasLength(encoding)) { encoding null; } // 获取value属性它表示属性文件的位置 String[] locations propertySource.getStringArray(value); // 确保至少指定了一个位置 Assert.isTrue(locations.length 0, At least one PropertySource(value) location is required); // 获取ignoreResourceNotFound属性表示如果资源找不到是否应该忽略 boolean ignoreResourceNotFound propertySource.getBoolean(ignoreResourceNotFound); // 获取并实例化PropertySourceFactory用于创建属性源 Class? extends PropertySourceFactory factoryClass propertySource.getClass(factory); PropertySourceFactory factory (factoryClass PropertySourceFactory.class ? DEFAULT_PROPERTY_SOURCE_FACTORY : BeanUtils.instantiateClass(factoryClass)); // 对于每一个指定的位置 for (String location : locations) { try { // 解析位置中的占位符 String resolvedLocation this.environment.resolveRequiredPlaceholders(location); // 获取资源 Resource resource this.resourceLoader.getResource(resolvedLocation); // 添加属性源到环境中 addPropertySource(factory.createPropertySource(name, new EncodedResource(resource, encoding))); } catch (IllegalArgumentException | FileNotFoundException | UnknownHostException | SocketException ex) { // 当尝试打开资源时占位符不可解析或资源找不到 if (ignoreResourceNotFound) { // 如果忽略资源找不到则记录一条信息 if (logger.isInfoEnabled()) { logger.info(Properties location [ location ] not resolvable: ex.getMessage()); } } else { // 否则抛出异常 throw ex; } } } }这段源码印证了几个关键事实value是必填且至少需要一个位置encoding通过EncodedResource包装资源生效工厂默认取DEFAULT_PROPERTY_SOURCE_FACTORY即DefaultPropertySourceFactory仅当显式指定了其他factory类时才实例化自定义工厂对每个 location 都会先做${...}占位符解析再定位资源。9. 创建属性源DefaultPropertySourceFactory#createPropertySource提供了name时按名称创建ResourcePropertySource否则仅基于资源创建名称由资源描述等默认方式确定Override public PropertySource? createPropertySource(Nullable String name, EncodedResource resource) throws IOException { return (name ! null ? new ResourcePropertySource(name, resource) : new ResourcePropertySource(resource)); }10. 注册到环境addPropertySourceaddPropertySource处理“同名合并”与“顺序插入”两个关键逻辑已存在同名属性源时新建/合并为CompositePropertySource并整体替换未处理过任何属性源时使用addLast最低优先级加入否则插入到上一次处理属性源之前addBeforeprivate void addPropertySource(PropertySource? propertySource) { // 获取属性源的名称 String name propertySource.getName(); // 获取当前环境的所有属性源 MutablePropertySources propertySources ((ConfigurableEnvironment) this.environment).getPropertySources(); // 如果此名称的属性源已经被添加过 if (this.propertySourceNames.contains(name)) { // 我们已经添加了一个版本需要扩展它 PropertySource? existing propertySources.get(name); if (existing ! null) { PropertySource? newSource (propertySource instanceof ResourcePropertySource ? ((ResourcePropertySource) propertySource).withResourceName() : propertySource); // 如果现有的属性源是CompositePropertySource if (existing instanceof CompositePropertySource) { ((CompositePropertySource) existing).addFirstPropertySource(newSource); } else { // 创建一个新的CompositePropertySource并将新的和现有的属性源添加进去 if (existing instanceof ResourcePropertySource) { existing ((ResourcePropertySource) existing).withResourceName(); } CompositePropertySource composite new CompositePropertySource(name); composite.addPropertySource(newSource); composite.addPropertySource(existing); propertySources.replace(name, composite); } return; } } // 如果还没有添加任何属性源 if (this.propertySourceNames.isEmpty()) { propertySources.addLast(propertySource); } else { // 添加属性源到上次处理的属性源之前 String firstProcessed this.propertySourceNames.get(this.propertySourceNames.size() - 1); propertySources.addBefore(firstProcessed, propertySource); } // 将属性源名称添加到跟踪的名称列表中 this.propertySourceNames.add(name); }至此PropertySource声明的外部属性文件已作为PropertySource进入Environment的属性源链MutablePropertySources后续Environment#getProperty(apiVersion)即可按属性源优先级顺序查找到对应值。七、注意事项文件位置确保文件路径正确。classpath:前缀表示文件在类路径中file:前缀表示文件在文件系统的特定位置url:前缀可用于远程资源。占位符在PropertySource的value属性中可以使用${...}占位符它们会按已注册的任何属性源解析。处理重复的属性源名称多个PropertySource或PropertySources若名称相同会合并为CompositePropertySource后声明的PropertySource将覆盖先前声明的同名属性源中的同名属性。属性源的顺序顺序很重要——多个属性源中存在同名属性时Environment取先找到的值。可通过name属性明确指定属性源名称以控制其在环境中的顺序默认后加载的PropertySource会插入到前一个已处理属性源之前从而获得更高优先级。忽略找不到的资源ignoreResourceNotFound默认false资源缺失会抛出异常设为true可在属性文件可选时让 Spring 记录日志并继续运行。字符编码Spring 4.3 起PropertySource提供encoding属性可为资源指定字符编码如UTF-8避免中文等非 ASCII 内容乱码。自定义属性源工厂需要特殊逻辑创建属性源时用factory属性指定自定义PropertySourceFactory实现类如解析 YAML 格式的工厂。激活属性占位符解析仅使用PropertySource并不会激活属性占位符解析。要替换 bean 定义中的${...}占位符还需添加PropertySourcesPlaceholderConfigurer的Bean定义Spring Boot 会自动注册或将Environment注入后通过environment.getProperty()取值。与 Profiles 结合可结合 Spring Profile 功能根据不同环境加载不同属性文件例如PropertySource(classpath:application-${spring.profiles.active}.properties)配合占位符解析实现环境差异化配置。八、总结最佳实践总结初始化上下文使用AnnotationConfigApplicationContext作为 Spring 容器的初始化方式允许从 Java 注解配置容器。定义配置类创建MyConfiguration类并使用Configuration标记为基于 Java 的 Spring 配置类。指定属性源在配置类上用PropertySource指定外部属性文件位置本示例为my-application.yml。定义属性内容在my-application.yml中定义apiVersion与kind两个属性。加载并访问属性运行应用后通过Environment#getProperty()访问这些属性并输出到控制台。运行结果控制台输出apiVersion v1与kind ConfigMap证明 YAML 属性加载成功。源码分析总结上下文初始化AnnotationConfigApplicationContext初始化 Spring 容器传入的MyConfiguration定义了要加载的属性源。处理PropertySource注解配置类处理期间Spring 检查每个配置类上的PropertySource提取位置及其他属性。资源位置解析对每个属性文件位置Spring 先解析占位符再尝试加载资源。创建属性源DefaultPropertySourceFactory从给定资源.properties、.xml等配合工厂也可支持.yml创建ResourcePropertySource。向环境中添加属性源属性源创建后加入Environment同名属性源会合并为CompositePropertySource或按顺序规则处理。处理完成全部配置类与属性源处理完毕后Spring 上下文继续正常启动应用运行时即可通过Environment获取这些属性。综合来看PropertySource的整个生命周期可以概括为一句口诀注解声明位置解析器读取属性工厂创建属性源addPropertySource按序挂入Environment属性源链。理解这条链路后无论是多环境配置、同名属性覆盖、还是自定义 YAML/远程配置源你都能从容应对。赞分享示例工程文档【免费下载链接】spring-reading涵盖了 Spring 框架的核心概念和关键功能包括控制反转IOC容器的使用面向切面编程AOP的原理与实践事务管理的方式与实现Spring MVC 的流程与控制器工作机制以及 Spring 中数据访问、安全、Boot 自动配置等方面的深入研究。此外它还包含了 Spring 事件机制的应用、高级主题如缓存抽象和响应式编程以及对 Spring 源码的编程风格与设计模式的深入探讨。项目地址https://gitcode.com/GitHub_Trending/sp/spring-reading点击查看免费下载相关推荐Spring PropertySource 之 StubPropertySource永远返回 null 的占位属性源原理与实战解析Spring PropertySource 之 StubPropertySource永远返回 null 的占位属性源原理与实战解析 导读 本文聚焦 Sprin文档教程知识库深入理解Spring中的PropertySource注解深入理解Spring中的PropertySource注解 一、PropertySource注解概述 PropertySource 是Spring框架中一个示例工程文档Small-Spring属性注入实战Value注解与配置管理的深度解析Small Spring属性注入实战Value注解与配置管理的深度解析 Spring框架作为Java企业级开发的事实标准其核心功能之一就是 属性注入 。在示例工程上一篇Agent Governance Toolkit 依赖审计实战以 agent-os caas 模块 pypdf 6.10.2→6.12.0 升级为例下一篇tsParticles Ink in Water 调色板完全指南墨入水中粒子效果的安装、配色与底层原理创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →