写点什么

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:4618248

评论 2 条评论

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

复古艺术效果照片编辑软件推荐: Chromatic Edges激活最新

mac大玩家j

Mac软件 图片编辑器 复古风图像处理工具

Space Capsule for mac(macOS 网格空间整理工具)v1.12激活版

iMac小白

AlDente Pro for Mac(电池最大充电限制工具)v1.24激活版

iMac小白

Jixipix Watercolor Studio Pro for Mac(mac水彩画制作软件)v1.4.17激活版

iMac小白

Sip for mac(强大的屏幕取色软件)v3.3激活版

iMac小白

带你认识多模数据库GeminiDB架构与应用实践

华为云开发者联盟

数据库 后端 华为云 华为云开发者联盟 华为云GeminiDB

数智化如何帮助水泥企业提高人效

用友BIP

CADintosh X 8 for Mac(CAD制图软件)v8.8.6 (714)直装版

iMac小白

苹果Mac超好用的动态视频壁纸软件:Dynamic Wallpaper v16.8下载

Rose

视频动态壁纸 Dynamic Wallpaper下载 Mac壁纸软件下载 Dynamic Wallpaper中文版

王文京:企业数智化1-2-3,加速企业数智化进程

用友BIP

Chroma for Mac(调色板创建工具)v1.0.4激活版

iMac小白

Dynamic Wallpaper for Mac(精美的动态壁纸)v16.8免激活版

iMac小白

GraphicConverter 12 for mac v12.0.8中文激活版下载

iMac小白

JixiPix Chromatic Edges Mac(复古艺术风格照片转换软件)v1.0.31激活版

iMac小白

第10期 | 用友BIP项目云,助力业主方“投融建管营”一体化项目管理

用友BIP

项目管理

Find Any File for Mac(本地文件搜索工具)v2.4.2b1免激活版

iMac小白

时下最火的文生图服务—「imAgine绘图解决方案」一个视频让你快速了解它的强大能力!

伊克罗德信息科技

重写equals后为什么要重写hashcode方法

javaNice

Java

苹果电脑macos:简单好用的视频修复软件分享

Rose

Mac视频修复 视频损坏怎么办 媒体修复软件 苹果破解软件下载

实用又美观,这8个组织架构图模板你一定会需要!

职场工具箱

模板 组织架构图

小米二面:Redis 如何保证数据不丢失?

王磊

Java 面试

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