写点什么

Spring Boot 多环境配置最佳实践

  • 2019-05-14
  • 本文字数:6658 字

    阅读完需:约 22 分钟

Spring Boot多环境配置最佳实践

1、Spring Environment 概念简介

任何一个软件项目至少都需经过开发、测试、发布阶段,不同阶段有不同的运行环境,其对应的数据库、运行主机、存储、网络、外部服务也会有所区别,故大多数项目都有多套配置对应多个环境,一般来说有开发环境(dev)、测试环境(sit/test)、预生产环境(pre)和生产环境(prd),有些项目可能还有验证新功能的灰度环境等。


Spring 框架从 3.1 版本以后提供了 Environment 接口,包含两个关键概念 profiles 和 properties。Profile 是 Spring 容器中所定义的 Bean 的逻辑组名称,当指定 Profile 激活时,才会将 Profile 中所对应的 Bean 注册到 Spring 容器中,并把相关能力开放给了开发者;而 properties 代表着一组键值对配置信息,其实现中借助了 ConversionService 实现,具备 String 到 Object 的转换能力。其类图如下:


2、Spring Boot Environment 深入分析

2.1 Environment 内部结构

Environment 有三个实现类:


  • 标准实现 StandardEnvironment,继承于 AbstractEnvironment,重写 customizePropertySources 方法增加系统环境属性


protected void customizePropertySources(MutablePropertySources propertySources) {    propertySources.addLast(new MapPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));    propertySources.addLast(new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment())); }
复制代码


  • Servlet 容器实现 StandardServletEnvironment,继承于 StandardEnvironment,重写 customizePropertySources 方法增加 Servlet 和 JNDI 属性来源


protected void customizePropertySources(MutablePropertySources propertySources) {   propertySources.addLast(new StubPropertySource(SERVLET_CONFIG_PROPERTY_SOURCE_NAME));   propertySources.addLast(new StubPropertySource(SERVLET_CONTEXT_PROPERTY_SOURCE_NAME));   if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {      propertySources.addLast(new JndiPropertySource(JNDI_PROPERTY_SOURCE_NAME));   }   super.customizePropertySources(propertySources);}
复制代码


  • StandardReactiveWebEnvironment 响应式 web 容器实现,继承于 StandardEnvironment



对于 web 应用来说 Environment 实现是 StandardServletEnvironment,其主要功能由抽象类 AbstractEnvironment 来实现,最核心的 2 个字段如下:


public abstract class AbstractEnvironment {   private final MutablePropertySources propertySources = new MutablePropertySources();
private final ConfigurablePropertyResolver propertyResolver = new PropertySourcesPropertyResolver(this.propertySources);
public <T> T getProperty(String key, Class<T> targetType) { return this.propertyResolver.getProperty(key, targetType); }}
复制代码


其中 propertySources(MutablePropertySources 对象)存放所有 Environment 所有的 PropertySource(代表一个配置来源),内部是 CopyOnWriteArrayList,提供有序添加 PropertySource 能力。


public class MutablePropertySources {   private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();   public void addFirst(PropertySource<?> propertySource){   }   public void addLast(PropertySource<?> propertySource){   }   public void addBefore(String relativePropertySourceName, PropertySource<?> propertySource){   }   public void addAfter(String relativePropertySourceName, PropertySource<?> propertySource){   }}
复制代码


调用 Environment 的 API 获取 property 时内流程是 propertyResolver 遍历所有 PropertySource 查询 property,并进行占位符替换处理后返回结果。


public class PropertySourcesPropertyResolver {   private final PropertySources propertySources;
protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) { if (this.propertySources != null) { for (PropertySource<?> propertySource : this.propertySources) { Object value = propertySource.getProperty(key); if (value != null) { if (resolveNestedPlaceholders && value instanceof String) { value = resolveNestedPlaceholders((String) value); } logKeyFound(key, propertySource, value); return convertValueIfNecessary(value, targetValueType); } } } return null; }}
复制代码

2.2 Environment 初始化

Spring Boot 通过 SPI 和 Listener 机制完成各个组件的初始化,Environment 的初始化是其中一个环节,其主要流程如下:



  • 运行 SpringApplication

  • 通过 SPI 机制加载所有 ApplicationListener 类,其中包含配置文件应用监听器


META-INF/spring.factories:


org.springframework.context.ApplicationListener= org.springframework.boot.context.config.ConfigFileApplicationListener
复制代码


  • 准备 Environment 相关配置信息

  • 通知所有 ApplicationListener 处理 ApplicationEnvironmentPreparedEvent 事件

  • ConfigFileApplicationListener 处理此事件,通过 SPI 机制加载 EnvironmentPostProcessor 实现类列表,调用各个类的 postProcessEnvironment 方法把对应的环境配置信息加载到 Environment 对象中


META-INF/spring.factories:


org.springframework.boot.env.EnvironmentPostProcessor =\org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor,\org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor
复制代码


4 个处理器的作用说明:



ConfigFileApplicationListener 作为 EnvironmentPostProcessor 一个实现,通过 SPI 机制加载 PropertySourceLoader 接口的实现类来完成配置文件加载,分别是 properties 格式和 yaml 格式的配置加载器


org.springframework.boot.env.PropertySourceLoader=\org.springframework.boot.env.PropertiesPropertySourceLoader,\org.springframework.boot.env.YamlPropertySourceLoader
复制代码


Spring 默认加载配置文件的位置是 classpath:/,classpath:/config/,file:./,file:./config/,文件名为 application。其主要流程是:


  • 从环境中获取指定 profile 集合,包括 active 和 include

  • 加载 application.yaml(或 application.properties)文件

  • 读取配置文件转为 PropertySource 对象,并获取 profiles.active 和 profiles.include 配置项,加入到 profile 集合

  • 遍历 profile 集合,加载 application-${profile}.yaml,重复上一步骤,直至配置完所有 profile


ConfigFileApplicationListener.Loader#load()


public void load() {   this.profiles = new LinkedList<>();   this.processedProfiles = new LinkedList<>();   this.activatedProfiles = false;   this.loaded = new LinkedHashMap<>();   initializeProfiles();   while (!this.profiles.isEmpty()) {      Profile profile = this.profiles.poll();      if (profile != null && !profile.isDefaultProfile()) {         addProfileToEnvironment(profile.getName());      }      load(profile, this::getPositiveProfileFilter,            addToLoaded(MutablePropertySources::addLast, false));      this.processedProfiles.add(profile);   }   resetEnvironmentProfiles(this.processedProfiles);   load(null, this::getNegativeProfileFilter,         addToLoaded(MutablePropertySources::addFirst, true));   addLoadedPropertySources();}
private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter, DocumentConsumer consumer) { Resource resource = this.resourceLoader.getResource(location); String name = "applicationConfig: [" + location + "]"; List<Document> documents = loadDocuments(loader, name, resource); List<Document> loaded = new ArrayList<>(); for (Document document : documents) { if (filter.match(document)) { addActiveProfiles(document.getActiveProfiles()); addIncludedProfiles(document.getIncludeProfiles()); loaded.add(document); } }}
复制代码


最后形成有优先级顺序的 PropertySource 集合,其顺序如下:



故开发者可以从 Environment 中获取系统相关所有配置信息,包括系统和 Java 环境信息、容器和应用程序配置,例如


  • environment.getProperty(“user.home”) 获取系统环境用户目录

  • environment.getProperty(“java.vendor”) 获取 Java 环境 JDK 厂商

  • environment.getProperty(“random.int(3,10)”) 通过随机化配置获取[3,10)之间的随机数

  • environment.getProperty(“server.port”) 获取配置文件中容器监听端口

3、配置实践

3.1 Active Profile 配置

从上面的分析可以看出指定 profile 的配置优先于默认配置,故可以把与环境无关的配置放在 application.yaml 中,环境相关的配置写在 application-${profile}.yaml 中。其中激活 profile 指定有三种方式:


  • 命令行启动参数设置 --spring.profiles.active={profile}

  • Java 环境或系统环境变量 spring.profiles.active={profile}

  • application.yaml 中 spring.profiles.active 配置项


其实这三种都还是通过 Environment 自身的 PropertySource 获取的,分别是 CommandLinePropertySource、SystemEnvironmentPropertySource 和 MapPropertySource#application.yaml。优先级从前到后,命令行指定的 profile 优先级最高,其次是环境变量,最后才是配置文件。前两种一般是本地开发测试时使用,大部分项目要求在打包时就需要生成特定环境的构件,所以需要在 application.yaml 中指定 profile,实现方法一般有两种:


(1)maven profile 配合 resource filter 机制


通过配置 maven profile 指定对应不同环境的变量配置文件,application 相关配置相同名称的占位符,通过 maven resource filter 在 resource process 阶段替换。举例说明:


vars 目录包含 dev 和 sit 环境的变量配置文件


vars.dev.properties:


env.profile=dev
复制代码


var.sit.properties


env.profile=sit
复制代码


spring boot 配置占位符


spring:  profiles:    active: ${env.profile}
复制代码


maven profiles 配置


<profiles>    <profile>        <id>dev</id>        <activation>            <activeByDefault>true</activeByDefault>        </activation>        <properties>            <profile.active>dev</profile.active >        </properties>    </profile>    <profile>        <id>sit</id>        <properties>            <profile.active >sit</profile.active >        </properties>    </profile></profiles>
复制代码


占位符替换


<build>    <filters>        <filter>${maven.multiModuleProjectDirectory}/vars/vars.${profile.active}.properties</filter>    </filters>    <resources>        <resource>            <directory>${basedir}/src/main/resources</directory>            <includes>                <include>**/*.*</include>            </includes>            <filtering>true</filtering>        </resource>    </resources></build>
复制代码


(2)maven profile 指定 resource 目录包含多级 application.yaml


在 src/main/profiles 包含环境相关的目录如 dev、sit,每个目录都包含对应的 application.yaml,如 dev 目录 application.yaml 内容为


spring:  profiles:    active: dev
复制代码


在 maven 中指定 resources 目录包含 profile 对应的目录


<build>    <resources>        <resource>            <directory>src/main/resources</directory>        </resource>        <resource>            <directory>src/main/profiles/${profile.active}</directory>        </resource>    </resources>
复制代码


src/main/resources/config 目录放置环境无关的配置 application.yaml 和环境相关的配置 application-{profile}.yaml,构建时把包含 active profile 的 application.yaml 打包到 classpath:/根目录,最终构件目录是:


classpath:/  application.yaml  config/    pplication.yaml、application-dev.yaml、application-sit.yaml        
复制代码

3.2 Inculde Profile 配置

在实际项目中可能会出现不同的环境也有部分配置相同的情况,例如开发环境和测试环境数据库相同,但 redis 不同。若每个环境只有一个配置,相同配置项会出现在多个文件,修改时也要编辑多个地方,不符合 DRY(Don’t Repeat Yourself )原则。所以应该把不同环境相同部分抽离出来成为一个 profile,通过 include 方式引用,即便捷又清晰,也足够灵活。配置示例如下:


application-dev.yaml


spring:  profiles:    include: database-sit,redis-dev
复制代码


application-sit.yaml


spring:  profiles:    include: database-sit,redis-sit
复制代码


还有三个子 profile 配置文件 application-redis-dev.yaml、application-redis-sit.yaml、application-database-sit.yaml 分别配置各自参数。


当然也可以通过多个 profile 指定,但若应用要求运行环境具备唯一性,则需要其他配置变量配合实现,稍显繁琐,所以指定一个 active profile 并包含多个子 profile 应该是更优的解决方案。

4、苏宁易购的多环境配置设计

4.1 苏宁云环境管理

苏宁易购所有业务系统都运行在苏宁私有云上且分布在多个机房,每个业务有多个模块和部署单元,为此苏宁云在建设之初就考虑到环境相关的问题,从服务器申请、软件开发、测试到发布,环境的概念贯穿始终。



苏宁业务系统的环境是与机房(数据中心)、服务器、系统模块、统一配置相关的,需要系统设计时规划好环境信息,如 XX 机房集成测试环境、XX 机房压测环境、XX 机房生产环境。在申请资源指明环境对应的部署单元及服务器,在代码中配置相关 profile 信息,并在持续集成平台定义环境打包时对应的 profile 和部署单元。如下图是一个包含 2 个机房 4 个环境的系统:



4.2 苏宁统一配置管理(SCM)

项目源码放置配置文件对于小型项目来说比较便捷,但对于包含数千个系统的大型平台却有很多弊端如缺乏对配置数据的集中掌控,运维风险逐渐累积;配置管理工作繁杂、配置迁移人工介入过多,风险极大;无法很好的满足内控要求等等。为了解决这些问题,苏宁基于 Zookeeper 自研了统一配置管理系统 SCM,具备毫秒级同步、本地文件和内存两级缓存、多机房部署等高可用特性,且支持版本化、回滚、流程审核、权限控制等功能。


项目中使用 SCM 功能需要在 maven 中集成 snf-scm-client 依赖,并在资源目录中加入一个 scm.properties 文件,其中配置了 SCM 服务器和环境信息。SCM 客户端提供了配置读取和变化监听接口,业务方可以基于此能力灵活方便定制业务框架的功能。


5、总结

在项目中大家可以根据实际情况结合 maven 工具灵活配置 profile 和参数配置信息,除了设置 default、active、include profile 之外,YAML 格式配置文件还支持多 profile 并设置复杂逻辑关系如 production & eu-central、 production & (eu-central | eu-west)等。另外 Spring Boot 在各个环节都使用 SPI 机制,具备良好的扩展性,可以定制 EnvironmentPostProcessor 或 PropertySourceLoader 的实现满足业务框架个性化需求。

作者介绍

胡正林,苏宁科技集团消费者平台高级架构师,十余年软件开发经验,熟悉大型分布式高并发系统架构和开发,目前主要负责易购各系统架构优化与大促保障工作。


黄小虎,苏宁科技集团消费者平台购物流程架构负责人,全面负责苏宁易购商品详情页、购物车、大聚会等核心系统的优化及大促保障工作。对电商交易流程和业务有较深入的思考和研究,专注于高并发大型电商网站的架构设计、高可用的系统设计。曾主导和参与了 Commerce 系统拆分、商品详情页接入层优化、云信客服系统重构等重大技术攻关项目。现致力于打造苏宁易购新一代核心购物流程系统,希望将购物体验做到极致。


2019-05-14 06:4618275

评论 2 条评论

发布
用户头像
一开始我们也这么搞,但是新增一个点的配置改完yml还要改pom.xml,还是太麻烦了。对于我们的需求更复杂,我们有开发、测试、演示3套环境,然后再包括发布给客户的生产环境最少4套,最多可能会超过50套,这样pom.xml里会配置好几个屏幕的环境。现在,我们已经通过shell脚本自动打包省去了人为对pom.xml的改动,以及去占位符,这样配置的复杂度又回归了简单。
2020-04-21 14:36
回复
用户头像
能不能走点心啊,截图里全是红色波浪线,很赏心悦目么?
2019-05-14 10:57
回复
没有更多了
发现更多内容

数字转型得力伙伴:华为云Flexus X实例打造云计算应用新标杆

轶天下事

聚焦中小企业上云用云需求,华为云Flexus X实例带来双倍性能、跃级体验

YG科技

华为云Flexus X实例全方位为中小企业护航,加速迈进“专精特新”

YG科技

华为云Flexus X实例全方位创新,开拓企业数字化发展新边界

YG科技

商品搜索API的未来:1688阿里巴巴引领智能化电商新趋势

代码忍者

API 接口 API 文档 API 测试

Downie 4 for Mac v4.7.26中文最新版 苹果视频下载器

理理

Downie 4许可证 Mac视频下载器 Downie 4 下载 Downie 4 Mac版

重塑云服务,华为云Flexus X实例破解云服务传统难题

YG科技

华为云Flexus X实例性能出众,降本显著,促进中小企业高质量发展

YG科技

性能易用二合一!华为云Flexus X实例加快中小企数字转型

YG科技

无缝升级云服务,华为云Flexus X实例带来零门槛极致体验

YG科技

聚焦中小企业实际需求,华为云Flexus X实例推动中小企业数字化转型愿转敢转

轶天下事

Mac红巨星粒子插件:Red Giant Trapcode Suite 激活版

你的猪会飞吗

Mac软件 mac破解软件下载 mac插件下载

MySQL 5.7 DDL 与 GH-OST 对比分析

vivo互联网技术

MySQL ddl GH-OST

​MES系统如何精准采集与对接设备数据,全面优化设备管理

万界星空科技

数据采集 mes 设备管理 万界星空科技 设备能源管理

中小企业数字化转型难点凸显,华为云Flexus X实例革新上云体验

YG科技

直击中小企业痛点!Flexus X实例让中小企业也可以上好云、用好云

YG科技

最好用的markdown写作工具Ulysses for Mac中文直装版

理理

markdown Markdown文本编辑器 Ulysses中文版 Ulysses破解版

蓝易云 - Linux之autofs自动挂载服务

百度搜索:蓝易云

云计算 Linux 运维 云服务器 autofs

掀起性能巨浪:华为云Flexus X实例定义业务负载多面手

YG科技

手把手教你如何参与开源,详细实用~

XIAOJUSURVEY

GitHub 开源 PR 贡献者 Issue

蓝易云 - rke2在线部署kubernetes

百度搜索:蓝易云

Linux Kubernetes 运维 k8s RKE2

华为云Flexus X实例依托生态优势,破解中小企业数字化转型困局

YG科技

蓝易云 - 如何选择合适的服务器进行外贸建站?

百度搜索:蓝易云

服务器 云服务器 建站 搭建网站 高防服务器

蓝易云 - linux压缩webfile文件夹 webfile.tar.gz和webfile.tar的区别

百度搜索:蓝易云

Linux 运维 云服务器 压缩 解压

飞天发布时刻:大数据AI平台产品升级发布

阿里云大数据AI技术

人工智能 大数据 MaxCompute EMR PAI

Bartender 4 for Mac(应用图标管理工具)v4.2.25中/英直装版

理理

Mac软件 Bartender 4 菜单栏应用图标管理

激发云服务效能,华为云Flexus X实例助力破除中小企上云痛点

YG科技

低代码实践:题型物料化设计(四)

XIAOJUSURVEY

低代码 schema 源码解读 配置化 设置器

重磅!华为中国政企明确要把服务打造成为核心竞争力

新消费日报

数字化转型正当时!华为云Flexus X实例助力中小企业高效便捷上云

轶天下事

蓝易云 - MySQL之MHA高可用配置及故障切换

百度搜索:蓝易云

MySQL 云计算 运维 云服务器 MHA

Spring Boot多环境配置最佳实践_架构_胡正林_InfoQ精选文章