发布在即!企业 AIGC 应用程度测评,3 步定制专属评估报告。抢首批测评权益>>> 了解详情
写点什么

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

评论 2 条评论

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

Android程序签名打包

攻城狮Wayne

Android Studio 打包签名 签名的含义

支持故障自动秒级检测,华为云VPN更省心!

IT科技苏辞

华为云ECS弹性服务器,加快企业数字化转型的进程!

IT科技苏辞

BAT大厂java程序员面试必问:JVM+Spring+分布式+tomcat+MyBatis

钟奕礼

程序员 Java 面试 Java、 java 编程

史上最全JVM大全详解!java程序员细节到极致的一次,魔鬼

钟奕礼

程序员 Java 面试 Java、 java 编程

华为云弹性负载均衡ELB,如何保障服务器不瘫痪?

爱科技的水月

共创、共享、共赢云生态,华为云ECS助力企业轻松上云

IT科技苏辞

2022年中国数字文化娱乐产业综合分析

易观分析

产业 文娱

出海有“云”!华为云全球加速助力跨国企业提升网络体验

与时俱进的时代

5分钟搞懂BFF

俞凡

架构 BFF

回顾与展望Zebec举办的“Web3.0 TechHive Summit 2022 大会”

BlockChain先知

8年java技术岗面试官总结:2022超强面试大全,抓住2022的小尾巴

钟奕礼

程序员 Java 面试 Java、 java 编程

我凭借这份pdf,最终拿到了阿里,腾讯,京东等八家大厂offer

钟奕礼

程序员 Java 面试 Java、 java 编程

如何准备Java技术岗春招面试:史上最全Java核心知识点笔记奉上

钟奕礼

程序员 Java 面试 Java、 java 编程

运维训练营第十课作业

好吃不贵

笑对过往、活在当下、期盼未来

阿Q说代码

程序员 flag 年度总结

【web 开发基础】PHP8中数组的序列化和反序列化(54)

迷彩

数组 对象 序列化 反序列化 PHP基础

华为云智能云接入ICA,助力企业轻松上云

与时俱进的时代

助力企业降本增效:华为云ECS,助力企业数字化转型需求

IT科技苏辞

2022年度总结:虽迷茫,仍前行

年度总结

java程序员:拜托别再问我Spring原理了!你问的这篇文章都有

钟奕礼

程序员 Java 面试 Java、 java 编程

无需企业搭建基础设施,华为云弹性公网IP经济实惠又便捷!

与时俱进的时代

华为云全球加速GA,为企业跨国办公保驾护航

与时俱进的时代

阿里高工珍藏版“亿级高并发系统设计手册(全彩版)”面面俱到,太全了!

架构师之道

Java 编程 高并发

实力是最好的武器!华为云ECS助力企业更好发展

IT科技苏辞

【web 开发基础】如何调换数组中的键和值(53)

迷彩

数组合并 数组操作 PHP基础 数组整理

开启全新身份!华为阅读畅读会员震撼来袭,限时首月9元快来领取

最新动态

华为云智能云接入ICA,企业数据上云的信赖之选

与时俱进的时代

数字经济时代,为什么华为云ECS能获得更多用户的青睐?

IT科技苏辞

【web 开发基础】PHP中的类和对象(55)

迷彩

面向对象 封装、继承、多态 类与对象 PHP基础

PostgreSQL 技术内幕(四)执行引擎之Portal

酷克数据HashData

postgresql

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