50万奖金+官方证书,深圳国际金融科技大赛正式启动,点击报名 了解详情
写点什么

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

评论 2 条评论

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

iuap助力三花控股集团打造主数据管理平台

用友BIP

用友 用友iuap

技术平台&应用开发专题月 | 如何保证业务服务稳定运行—用友云原生技术平台高可用能力介绍

用友BIP

用友 用友iuap

坐标PCB公司,想做实时数仓、推生产线看板,和Tapdata Cloud的偶遇来得就是这么凑巧

tapdata

实时数据

WMS是什么?

源字节1号

开源 后端开发

译文《Java并发编程之CAS》

潘大壮

乐观锁 并发编程 CAS 并发’ Java Concurrency

龙蜥开发者说来了,来看看社区一周动态还有什么? | 3.07-3.11

OpenAnolis小助手

开源 开发者 龙蜥社区 一周动态

TypeScript 2.0开启空值的严格检查

华为云开发者联盟

typescript js 空指针 ts

在线JSON格式化美化

入门小站

工具

Docker Shim 被移除,K8s v1.24 升级该怎么办

Daocloud 道客

Docker Kubernetes CRI-Dockerd

一种小程序弱网离线优化的思路

阿里巴巴终端技术

小程序 弱网 体验优化

Figma禁封中国企业,下一个会是Postman吗?国产软件势在必行

Liam

后端 Postman Apifox API swagger

推荐算法!基于隐语义模型的协同过滤推荐之商品相似度矩阵

编程江湖

大数据 算法

阿里IM技术分享(七):闲鱼IM的在线、离线聊天数据同步机制优化实践

JackJiang

即时通讯 IM im开发

天翼云供应链API安全治理实践获“优秀治理实践奖”

天翼云开发者社区

东数西算加快云网与数据融合天翼云架起云间高速

天翼云开发者社区

java编程技术FastDFS 安装和配置

编程江湖

深入跨国互联网业务场景,看华为云数智融合元数据如何打破“数据墙”

华为云开发者联盟

大数据 数据仓库 华为云 元数据 数智融合

做开发这么久了,还不会搭建服务器Maven私有仓库?这也太Low了吧

冰河

系统架构 程序开发 程序员进阶 编程基础 Maven仓库

新一代对抗作战框架MITRE Engage V1版本正式发布

青藤云安全

网络安全 青藤 青藤云安全

云原生中间件 -- Redis Operator 篇

Daocloud 道客

redis 云原生 中间件 云原生中间件

Java有了synchronized,为什么还要提供Lock

华为云开发者联盟

Java synchronized 死锁 lock 同步代码块

Web安全渗透测试基本流程

学神来啦

网络安全 Web 渗透测试 WEB安全 kali

详细解读PolarDB HTAP的功能特性和关键技术

阿里云数据库开源

数据库 阿里云 开源 postgre polarDB

技术平台&应用开发专题月 | 企业上云利器-YMS(Yon Middleware Service)

用友BIP

用友 用友iuap

基于 EventBridge 构建 SaaS 应用集成方案

阿里巴巴云原生

云原生 SaaS

flask POST请求,数据入库,文件上传,一文看懂,3天掌握Flask开发项目系列博客之三

梦想橡皮擦

3月月更

拥抱云原生 2.0 时代,Tapdata 入选阿里云首期云原生加速器!

tapdata

数据库 实时数据服务平台

这是我见过最详细的Nginx 内存池分析

Linux服务器开发

nginx 线程池 Linux服务器开发 Linux后台开发 内存池

由Figma封停大疆,看国产IDE如何应对与突围?

Baihai IDP

人工智能 ide AI 基础软件 国产化

技术平台&应用开发专题月 | 业务上云后的调试利器—云机一体

用友BIP

用友 用友iuap

3天掌握Flask开发项目系列博客之二,操作数据库

梦想橡皮擦

3月月更

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