ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

《Spring Boot 3:高级与架构设计》第 2 章 IOC 容器的高级机制 Environment 阅读笔记 4

《Spring Boot 3:高级与架构设计》第 2 章 IOC 容器的高级机制 Environment 阅读笔记 4 《Spring Boot 3高级与架构设计》第 2 章 IOC 容器的高级机制 Environment 阅读笔记 4在入门篇中接触了大量关于 ApplicationContext 的操作在第 1 章中深入 ApplicationContext 组合的 BeanFactory 中了解了 BeanDefinitionRegistry 等设计。本章会接触更多 ApplicationContext 中组合的 API 与运行机制这些机制在深层次开发与定制中往往发挥非常有效的作用。想要深挖 Spring Framework 内部工作原理的读者一定要仔细学习本章的内容。2.1 Environment在入门篇中讲解过使用 properties 文件、YML 文件加载到 ApplicationContext 的内部当时使用的注解是 PropertySource。读者在当时的阅读中是否想到过一个问题这些配置项的键值对都保存到 IOC 容器的什么区域使用 Value 完成属性赋值时又是如何从 IOC 容器中获取的带着这些疑问开始认识本章第一个重要 APIEnvironment。2.1.1 Environment 概述Environment 是从 Spring Framework 3.1 开始引入的抽象模型称为 “运行时环境” 。Environment 本身的设计比较特殊具体可以有以下 3 点理解。1Environment 中包含两个重要元素 profiles 和 properties这些配置信息会影响 IOC 容器中 Bean 的注册与创建。2Environment 在 ApplicationContext 创建后才创建所以 Environment 应该是伴随着 ApplicationContext 的存在而存在的。3ApplicationContext 中同时包含 Environment 和组件 Bean而且从 BeanFactory 的视角来看Environment 也是一个Bean只是其地位非常特殊。基于以上 3 点理解可以得到 ApplicationContext 与 Environment 等成员的结构关系如图所示。虽然 Environment 也属于 BeanFactory 内部的一个Bean但是将 Environment 划分在 IOC 容器的组件以外是考虑到 Environment 对象本身组合在 ApplicationContext 中它的地位非常特殊Environment 对组件 Bean 有提供配置属性值的功能所以图中有一个从 Environment 指向组件 Bean 的箭头。Environment 的 javadoc 比较长本节仅讲解关键信息。读者可以借助 IDE 自行阅读完整的 javadoc。简单总结Environment 是 Spring Framework 3.1 引入的抽象的概念它包含 profiles 和 properties 的信息可以实现统一的配置存储和注入、配置属性的解析等。其中 profiles 实现了一种基于模式的环境配置properties 则多应用于外部化配置。2.1.2 Environment 的结构了解 Environment 的概念设计后下面结合 Environment 的结构与继承关系来探索 Environment 在 Spring Framework 中的设计地位。借助IDEA 可以形成 Environment 的类继承结构关系如图所示。观察图可以得到两个简单的结论1Environment 不是顶层根接口2Environment 的落地实现有多种方式分别适用于不同的场景。1 PropertyResolver从类名上看PropertyResolver 是一个属性解析器。它可以处理 XML 配置文件、注解配置类、普通 Bean 中使用到的属性配置占位符。Environment 继承了 PropertyResolver并且 Environment 中存放了 profiles 和 properties 属性配置这意味着 Environment 可以解析属性配置占位符并提取出 Environment 中的 properties 配置以完成赋值操作。属性配置占位符即 ${}2 ConfigurableEnvironment学习过入门篇中的 BeanFactory 与 ApplicationContext 后读者对 Configurable 这个前缀应该不陌生Configurable 前缀意味着这个类/接口具备可配置的能力接口中大概率会有 set/add 之类的方法。借助 IDE 浏览其核心方法如代码所示。前 3 个方法不难理解即可以编程式地设置 activeProfiles 和 defaultProfiles最后一个方法 getPropertySources 比较特殊它可以用来获取所有的 PropertySource 对象但返回值的类型是 MutablePropertySources。这个类型内部组合了一个 List并使用更合理方便的 API 予以封装。由此又可以得出一个小结论Mutable 开头的类名通常是一个类型的 List 组合封装。PropertySource 可以简单理解为一个 PropertySource 对象对应了一个配置源这个配置源可能来自 properties 文件可能来自系统环境变量、项目信息也可能是自定义的加载方式如数据库​。但无论其来自哪里最终都会被封装为 key-value 的形式存储在一个 PropertySource 对象中统一归 Environment 管理。3 AbstractEnvironment所有 Environment 落地实现的抽象父类是 AbstractEnvironment其部分核心成员如代码所示。之所以看抽象类不看实现类是因为实现类中的代码量相当少而且没有很重要的逻辑大多数实现类都在 AbstractEnvironment 中体现。此处了解 AbstractEnvironment 的核心设计即可。代码所列的是 AbstractEnvironment 的部分核心成员其存储默认的 profile 和声明激活的所有 profile以及所有的 PropertySource。此外请读者注意另外一个很重要的成员即 PropertySourcesPropertyResolver它也是一个 PropertyResolver。AbstractEnvironment 在此处组合了一个 PropertyResolver 的实现类意图也很明显只要是 PropertyResolver 接口下的方法要做的工作全部交予 PropertySourcesPropertyResolver 代替完成。这种设计称为“委派”它与代理、装饰者不同委派仅仅是将方法的工作执行转移给另一个对象而代理可能会在此做额外的处理装饰者也会在方法执行前后做增强处理。在笔者的另一本图书《Spring Boot源码解读与原理分析》的第 12 章中讲解 DispatcherServlet 的工作流程时也反复提到过委派机制感兴趣的读者可以移步阅读。2.1.3 Environment 的基本使用虽然不建议直接在应用程序中使用 Environment但是在少部分场景下还是需要直接接触并操纵它。此处先了解 Environment 的简单使用方式然后在 2.1.4 节进行扩展。1 获得 Environment 的 API既然 Environment 存在于 ApplicationContext 中那么获取 Environment 的方式之一就是使用 Autowired。下面通过一个简单示例来获取Environment。先编写一个 Environment 的持有器 EnvironmentHolder使用 Autowired 声明注入 Environment随后使用注解驱动的 IOC 容器进行组件扫描并获取 EnvironmentHolder如代码所示。packagecom.yangjunbo.spring.environment.examplea.bean;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.core.env.Environment;importorg.springframework.stereotype.Component;ComponentpublicclassEnvironmentHolder{AutowiredprivateEnvironmentenvironment;publicvoidprintEnvironment(){System.out.println(environment);}}执行 main 方法后控制台打印了 Environment 的基本信息从输出的内容可以直观地了解到Environment 中存放的基本信息包括 profiles 的信息、PropertySource 的组合信息。packagecom.yangjunbo.spring.environment.examplea;importcom.yangjunbo.spring.environment.examplea.bean.EnvironmentHolder;importorg.springframework.context.annotation.AnnotationConfigApplicationContext;/** * ClassName: environmentAApplication * Package: com.yangjunbo.spring.environment.examplea * Description: * * Author 杨钧博 * Create 2026/9/8 9:20 * Version 1.0 */publicclassenvironmentAApplication{publicstaticvoidmain(String[]args)throwsException{AnnotationConfigApplicationContextctxnewAnnotationConfigApplicationContext(com.yangjunbo.spring.environment.examplea.bean);EnvironmentHolderenvironmentHolderctx.getBean(EnvironmentHolder.class);environmentHolder.printEnvironment();}}此外由于 BeanFactory、ApplicationContext 的注入方式还有回调注入作为 Spring Framework 的内置 APIEnvironment 自然也有一个配套的回调注入接口 EnvironmentAware。2 使用 Environment 获取配置属性的值上一小节中已经获取了 Environment接下来就可以调用 Environment 的方法。为了方便获取 properties 的配置信息首先编写一个配置类将入门篇 4.7 节中演示的 jdbc.properties 文件加载进来随后从 EnvironmentHolder 中获取 Environment并打印 jdbc.url 的值如代码所示。jdbc.urljdbc:mysql://localhost:3306/test jdbc.driver-class-namecom.mysql.jdbc.Driver jdbc.usernameroot jdbc.password123456packagecom.yangjunbo.spring.environment.exampleb.config;importorg.springframework.context.annotation.ComponentScan;importorg.springframework.context.annotation.Configuration;importorg.springframework.context.annotation.PropertySource;ConfigurationComponentScan(com.yangjunbo.spring.environment.exampleb.bean)PropertySource(environment/exampleb/jdbc.properties)publicclassEnvironmentPropertyConfiguration{}packagecom.yangjunbo.spring.environment.exampleb.bean;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.core.env.Environment;importorg.springframework.stereotype.Component;ComponentpublicclassEnvironmentHolder{AutowiredprivateEnvironmentenvironment;publicvoidprintEnvironment(){System.out.println(environment.getProperty(jdbc.url));}}重新驱动 IOC 容器并取出 EnvironmentHolder执行 printEnvironment 方法控制台可以正确打印 properties 文件中 jdbc.url 对应的值这就是 Environment 的基本使用方式。有关 Environment 中其他的 API 方法读者可以自己动手测试和体验本节不过多展开。packagecom.yangjunbo.spring.environment.exampleb;importcom.yangjunbo.spring.environment.exampleb.bean.EnvironmentHolder;importorg.springframework.context.annotation.AnnotationConfigApplicationContext;/** * ClassName: EnvironmentBApplication * Package: com.yangjunbo.spring.environment.exampleb * Description: * * Author 杨钧博 * Create 2026/9/8 9:34 * Version 1.0 */publicclassEnvironmentBApplication{publicstaticvoidmain(String[]args)throwsException{AnnotationConfigApplicationContextctxnewAnnotationConfigApplicationContext(com.yangjunbo.spring.environment.exampleb);EnvironmentHolderenvironmentHolderctx.getBean(EnvironmentHolder.class);environmentHolder.printEnvironment();}}2.1.4 Environment 深入探讨请读者观察 2.1.3 节第一个示例中控制台打印的默认 profiles可见其中有一个默认值是 default这个值没有显式声明它由何而来另外为什么在没有定义其他内容的前提下Environment 中还会保存两套 PropertySource针对这些问题接下来将逐一进行深入探究。1 Environment 的默认 profiles若想知道 Environment 中 profiles 的默认配置就要进入 Environment 的抽象实现 AbstractEnvironment 中一探究竟。从代码所示的 AbstractEnvironment 源码片段中可以得知获取默认 profiles 使用的 getDefaultProfiles 方法底层调用了 doGetDefaultProfiles 方法。这种设计在 Spring Framework 中大量出现和应用。在 Spring Framework 的框架源码中如果一个方法以 do 开头并且去掉 do 后能找到一个与剩余名称一样的方法则其代表如下含义 “不以 do 开头的方法通常负责前置校验处理、返回结果封装以 do 开头的方法才是真正执行逻辑的方法”​。下面继续深入探究 doGetDefaultProfiles 方法。结合源码与注释可以发现该方法实现的整体逻辑并不复杂关键是它取框架默认的 profiles可知获取的就是 AbstractEnvironment 中内置的常量RESERVED_DEFAULT_PROFILE_NAME即 “default”​。需要补充的是如果在应用启动时主动声明 spring.profiles.default 参数的配置属性值则可以覆盖 Spring Framework 中原有的默认 profiles。一个比较常用的方法是在 JVM 的启动项中添加该参数即使用图所示的方式覆盖在 IDEA 的启动配置中声明 VMoptions。同理指定激活的profiles 也可以使用相同的方式只需配置属性名为 spring.profiles.active。2 Environment 解析 properties 的底层2.1.2 节在介绍 Environment 的结构时已经指出Environment 继承了父接口 PropertyResolver拥有解析配置元信息的能力。AbstractEnvironment 中组合了一个 PropertySourcesPropertyResolver它与 Environment 实现了相同的 PropertyResolver 接口以实现 “委派” 的效果。结合源码片段也可以看出凡是 Environment 要处理配置属性获取的动作就会转交给其内部的 PropertySourcesPropertyResolver 来完成。3 Environment 在开发中的应用场景简单分析源码后回到实际项目开发中考虑Environment 在实际开发中有什么应用场景由于 Environment 本身保存了配置属性因此它可被看作一个应用级或业务级配置库用于存储一些与应用运行相关的配置参数业务无关参数如线程池大小、全局响应最大时间等​以及一些在应用运行期间通常不会改变的业务参数如当前应用的版权信息、网站信息等​。由于大多数 Web 应用开发的业务场景下都能直接或间接地获取 IOC 容器自然也就可以获取 Environment因此将这些配置属性存放到Environment 后应用运行的大多数位置都可以获取这些配置属性值从而支撑业务或功能运行。
返回列表