通常情况,一个项目从开发到上线需要经历多个环境。例如程序员在自己的开发环境完成Coding,QA在测试环境完成对软件的测试。而最终软件运行在生产环境对外提供服务,产生真正的业务价值。一个正规的系统研发流程会按顺序经历如下多个环境:DEV->TEST->SIT->PRE->PROD。
那么问题就来了,各个环境间肯定会存在差异(例如各个环境的数据源配置一定是不同的),如何让我们开发好的应用能正常的运行在不同的环境呢?这就需要外部化配置。简单的说就是提前在代码中定义好配置项,代码中引用的是配置项的key,配置项真正的value放置在应用外部(包括配置文件、命令行参数、系统环境变量等等),这样不同的环境就可以根据自身情况定义不同的value。
本文会先介绍Spring Framework Environment的概念,有了基础之后再来看Spring Boot的Externalized Configuration。
Environment
Spring的Environment接口用来抽象application正在运行的环境,它有两个核心概念:profile和properties。
profile用于限制一组bean的定义,这组bean只有在该profile处于激活状态时才会被注入到Spring容器中。
properties就是一组配置项,它可以有多个来源:properties配置文件,JVM参数,系统环境变量等等。
下面分别对profile和profiles进行详细介绍
Profile
为了描述Profile的概念,我们先定义一个简单的Bean类:
public class SampleBean {
private String description;
public void setDescription(String description) {
this.description = description;
}
@Override
public String toString() {
return "bean description " + description;
}
}
我们的目的是在不同的环境,这个bean的description属性有不同的值。例如在Prod环境,description属性的值是prod。在Dev环境,description属性的值是dev。
bean的定义代码如下:
@Configuration
public class SmapleConfig {
@Profile("Prod")
@Bean
public SampleBean prodSampleBean() {
SampleBean sampleBean = new SampleBean();
sampleBean.setDescription("simulate Prod env");
return sampleBean;
}
@Profile("Dev")
@Bean
public SampleBean devSampleBean() {
SampleBean sampleBean = new SampleBean();
sampleBean.setDescription("simulate Dev env");
return sampleBean;
}
}
@Profile("Prod")注解限制了只有当名为Prod的profile被激活时才会创建prodSampleBean这个bean。对应的,只有当名为Dev的profile被激活时,才会创建名为prodSampleBean这个bean。
在main方法中创建applicationContext会调用的代码如下:
public static void main(String[] args) {
ApplicationContext ctx =
new AnnotationConfigApplicationContext(SmapleConfig.class);
SampleBean sampleBean = ctx.getBean(SampleBean.class);
System.out.println(sampleBean);
}
此时执行main函数会抛出NoSuchBeanDefinitionException这个异常,因为没有任何profile被激活,所有没有SampleBean类型的bean被创建。
现在我们用Java代码的方式激活Prod这个profile:
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(SmapleConfig.class);
ctx.getEnvironment().setActiveProfiles("Prod");
ctx.refresh();
SampleBean sampleBean = ctx.getBean(SampleBean.class);
System.out.println(sampleBean);
}
从applicationContext中获取代表运行环境的Environment,并通过Environment的setActiveProfiles方法激活了名为Prod的profile,所以id为prodSampleBean的bean会被创建,程序输出结果bean description simulate Prod env。也可以用同样的方式激活名为Dev的profile。
但是,通过Java代码的方式指定被激活的profile仍然不能自适应不同的环境,总不能每发布一个新环境就更改代码重新编译吧。通过Java虚拟机启动参数指定要激活的profile更加适合。
我们将main函数代码回滚到之前的版本,但在JVM虚拟机启动参数中加入-Dspring.profiles.active="Prod"
,运行程序仍然可以获取正常的运行结果。JVM虚拟机参数的配置如下图所示:
Dspring.profiles.active
可以同时激活多个profile,例如想要Prod和Dev这两个profile同时被激活,可以在JVM参数中设置Dspring.profiles.active="Prod,Dev"
。
综上,利用Profile的特性,我们可以提前为不同的环境提供好对应Bean的定义,并用Profile加以限制(只有profile被激活时,Bean才会被构建)。然后在不同的环境下,利用不同的JVM参数启动应用程序,达到在不同环境激活不同profile的目的。
Properties
properties内容可以有多种来源,我们可以通过@PropertySource注解指定properties内容来源的配置文件。
例如在classpath的根路径下有一个名为test.properties的文件,在文件中有一行配置description=test for properties
,key是“description”,value是“test for properties”。可以通过下面代码使用这条配置构建Bean:
@Configuration
@PropertySource("classpath:/test.properties")
public class SmapleConfig {
@Autowired
Environment env;
@Bean
public SampleBean sampleBean() {
SampleBean sampleBean = new SampleBean();
sampleBean.setDescription(env.getProperty("description"));
return sampleBean;
}
}
可以看到,在Spring容器启动后,会将各种来源的properties数据全部加载到Environment中,可以通过Environment获取到这些配置数据。
除了我们自定义的properties源外,StandardEnvironment还会自动加载JVM系统参数(System.getProperties())和系统环境变量(System.getenv())。
下图是Debug下Environment变量的状态,可以看到除了test.properties外还有systemProperties和systemEnvironment两个properties来源。
除了通过Environment对象获取到properties数据外,还可以通过@Value注解直接将properties配置项的value直接赋值给一个Bean的属性。
@Component
public class TestBean {
@Value("${description}")
private String name;
@Override
public String toString() {
return "TestBean{" +
"name='" + name + '\'' + '}';
}
public void setName(String name) {
this.name = name;
}
}
这段代码直接将description的value赋值给name属性,调用TestBean对象的toString方法输入结果为:TestBean{name='test for properties'}。
SpringBoot外部化配置
首先声明,SpringBoot的外部化配置也是在Spring Framework Environment的基础上实现的。但SpringBoot规范了properties的配置方式,包括配置文件的存放位置、多个配置文件的加载顺序,文件的命名方式等等。我们只需要按照自己的需求,将property配置项放到恰当的文件中,或给文件取符合规范的名字即可。这也体现了SpringBoot的一个重要特性——规约大于配置。
SpringBoot中的properties可以有多种配置方式,但下面我们只介绍最常使用的配置方式,要了解全部配置方式可参考这里。
application.properties
application.properties是Spring Boot中默认的properties配置文件。我们不需要再用@PropertySource注解指定一个自定义的PropertySource,Spring Boot已经帮我们准备好了,我们只需要将properties的配置放入文件即可。
一个Spring Boot应用可以有多个application.properties配置文件,每个application.properties根据存放文件的位置不同有不同的优先级。如果在两个application.properties文件中有相同的配置项,则高优先级中的配置会覆盖掉低优先级的配置。按照优先级从高到底,有下列location可以放置application.properties文件:
- 在应用部署路径下的/config子路径
- 在应用部署根路径
- 在classpath下的/config子package下
- 在classpath下的根路径
当然,这些路径是默认的,可以通过启动命令行参数进行修改。
理论上可以将通用的配置放置在classpath下的application.properties,把各个环境不同的配置放置在服务器的文件系统上,以此来达到适配不通环境的目的。但这样好像也不是很方便。
@ConfigurationProperties
有时,我们可能希望一个Bean中的属性自动与application.properties文件中的配置绑定,这时就可以使用@ConfigurationProperties注解。@ConfigurationProperties接受一个prefix参数表示匹配的properties配置的前缀。例如有下面一个Bean代码:
@Component
@ConfigurationProperties(prefix="test")
public class TestModel {
private int a;
private int b;
@Override
public String toString() {
return "TestModel{" +
"a=" + a +
", b=" + b +
'}';
}
public void setA(int a) {
this.a = a;
}
public void setB(int b) {
this.b = b;
}
}
在application.properties文件中有如下配置:
test.a = 10
test.b = 99
"test"与@ConfigurationProperties注解中prefix参数的值相匹配,a和b与Bean中的field相匹配,所以test.a和test.b这两个配置值可以直接赋值给Bean中a和b属性。调用该Bean的toString方法,得到结果"TestModel{a=10, b=99}"。
请注意TestModel类的a、b两个属性必须有对应的setter方法才能直接将properties的值注入。
profile-specific application.properties
profile-specific application.properties的文件命名规则为:application-{profile}.properties
。可以同时提供多个profile-specific application.properties配置文件,然后通过激活profile的方式选择让哪个配置文件生效。如果没有任何profile被激活,则默认application-default.properties配置文件生效。
profile-specific application.properties的存放位置与标准的application.properties存放位置相同。但无论文件存放在哪个路径,profile-specific application.properties文件中的配置项总会override标准的application.properties文件中的内容。
例如,在上面例子的基础上,再添加一个application-default.properties配置文件,其内容为:
test.a = 1234
test.b = 5678
现在在application-default.properties和application.properties两个配置文件中都有test.a、test.b的配置,则在application-default.properties文件中的配置会覆盖application.properties文件中的配置。toString方法打印结果:TestModel{a=1234, b=5678}
。
我们再来增加一个新的配置文件application-prod.properties。默认情况下会使用application-default.properties文件中的配置,但当我们激活名为prod的profile时,就会切换到使用application-prod.properties文件中的配置。将应用打成一个jar,在启动应用的时候执行下面命令:
java -jar -Dspring.profiles.active="prod" XXX.jar
注意这个命令已经激活了prod这个profile,所以此时应用会选择使用application-prod.properties文件中的配置。
这样,我们就可以将各个环境相同的配置放置到application.properties文件中,而将各个环境特有的配置放置到对应的application-{profile}.properties中,然后通过激活profile的方式选择环境,激活对应的profile-specific application.properties配置文件。
总结
我们会经常有“将同一份代码运行在多个不同环境上”这样的需求,Spring Framework提供了这样的支持,它提供的Environment接口就是对运行环境的一个抽象。Environment包含两个重要概念——profile和properties,利用这两个特性,就可以实现在不同环境执行不同的配置。
Spring Boot在Spring Framework的基础上实现了外部化配置。其实用的还是properties和profile这两个特性,只是spring boot对properties文件的名字(默认application.properties)、文件存放位置(默认有四个location)、不同环境的配置文件切换(profile-specific application.properties)等进行了规范,无需我们自己再指定properties source。这也充分体现了Spring Boot中的一个哲学——规约大于配置。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。