5.3 使用profile进行配置

5.3 使用profile进行配置

当应用部署到不同的运行时环境中的时候,有些配置细节通常会有些差别。例如,数据库连接的细节在开发环境和质量保证(quality assurance)环境中可能就不相同,而它们与生产环境可能又不一样。配置不同环境之间有差异的属性时,有种办法就是使用环境变量,通过这种方式来指定配置属性,而不是在application.properties和application.yml中进行定义。

例如,在开发阶段,我们可以依赖自动配置的嵌入式H2数据库。但是在生产环境中,我们可以按照如下的方式将数据库配置属性设置为环境变量:

1
2
3
% export SPRING_DATASOURCE_URL=jdbc:mysql://localhost/tacocloud
% export SPRING_DATASOURCE_USERNAME=tacouser
% export SPRING_DATASOURCE_PASSWORD=tacopassword

尽管这种方式可以运行,但是如果配置属性比较多,那么将它们声明为环境变量会非常麻烦。除此之外,我们没有好的方式来跟踪环境变量的变化,也无法在出现错误的时候进行回滚。

相对于这种方式,我更加倾向于采用Spring profile。profile是一种条件化的配置,在运行时,根据哪些profile处于激活状态,可以使用或忽略不同的bean、配置类和配置属性。

例如,为了开发和调试方便,我们希望使用嵌入式的H2数据库,并且Taco Cloud代码的日志级别为DEBUG。但是在生产环境中,我们希望使用外部的MySQL数据库,并将日志级别设置为WARN。在开发场景下,我们可以很容易地设置数据源属性并使用自动配置的H2数据库。对于调试级别的日志需求,我们可以在application.yml文件中通过logging.level.tacos属性将tacos基础包的日志级别设置为DEBUG:

1
2
3
logging:
level:
tacos: DEBUG

这就是我们要针对开发环境做的事情。但是,如果我们不对application.yml做任何修改就将应用部署到生产环境,tacos包依然会写入调试日志并且依然会使用H2数据库。我们需要做的就是定义一个profile,其中包含适用于生产环境的属性。

5.3.1 定义特定profile的属性

定义特定profile相关的属性的一种方式就是创建另外一个YAML或属性文件,其中只包含用于生产环境的属性。文件的名称要遵守如下的约定:application-{profile名}.yml或application-{profile名}.properties。然后,我们就可以在这里声明适用于该profile的配置属性了。例如,我们可以创建一个新的名为application-prod.yml的文件,其中包含如下属性:

1
2
3
4
5
6
7
8
spring:
datasource:
url: jdbc:mysql://localhost/tacocloud
username: tacouser
password: tacopassword
logging:
level:
tacos: WARN

定义特定profile相关的属性的另外一种方式仅适用于YAML配置。它会将特定profile的属性和非profile的属性都放到application.yml中,它们之间使用3个中划线进行分割,并且使用spring.profiles属性来命名profile。如果按照这种方式定义生产环境的属性,等价的application.yml如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
logging:
level:
tacos: DEBUG
---
spring:
profiles: prod
datasource:
url: jdbc:mysql://localhost/tacocloud
username: tacouser
password: tacopassword
logging:
level:
tacos: WARN

定义特定profile相关的属性的另外一种方式仅适用于YAML配置。它会将特定profile的属性和非profile的属性都放到application.yml中,它们之间使用3个中划线进行分割,并且使用spring.profiles属性来命名profile。如果按照这种方式定义生产环境的属性,等价的application.yml如下所示:

1
2
3
4
spring:
profiles:
active:
- prod

我们可以看到,application.yml文件通过一组中划线(—)分成了两部分。第二部分指定了spring.profiles值,代表后面的属性适用于prod profile。而第一部分的属性没有指定spring.profiles,所以它们是所有profile通用的,或者如果当前激活的profile没有设置这些属性,它们就会作为默认值。

不管应用程序运行的时候哪个profile处于激活状态,根据默认profile,tacos包的日志级别都将会设置为DEBUG。但是,如果名为prod的profile激活,那么logging.level.tacos属性将会被重写为WARN。与之类似,如果prod profile处于激活状态,那么数据源相关的属性将会被设置为使用外部的MySQL数据库。

通过创建模式为application-{profile名}.yml或application-{profile名}.properties的YAML或属性文件,我们可以按需定义任意数量的profile。或者,我们也可以在application.yml中再输入3个中划线,结合spring.profiles属性来指定其他名称的profile,然后添加该profile特定的相关属性。

5.3.2 激活profile

如果我们不激活这些profile,声明profile相关的属性其实没有任何用处。但是,我们该如何激活一个profile呢?要激活某个profile,需要做的就是将profile名称的列表赋值给spring.profiles.active属性。例如,在application.yml中,我们可以这样设置:

1
2
3
4
spring:
profiles:
active:
- prod

但是,这可能是激活profile最糟糕的一种方式。如果我们在application.yml中设置处于激活状态的profile,那么这个profile就会变成默认的profile,我们体验不到使用profile将生产环境相关属性和开发环境相关的属性分开的任何好处。因此,我推荐使用环境变量来设置处于激活状态的profile。在生产环境中,我们可以这样设置SPRING_PROFILES_ACTIVE:

1
% export SPRING_PROFILES_ACTIVE=prod

这样部署到该机器上的任何应用就都会激活prod profile,对应的属性会比默认profile具备更高的优先级。

如果以可执行JAR文件的形式运行应用,那么我们还可以以命令行参数的形式设置激活的profile:

1
% java -jar taco-cloud.jar --spring.profiles.active=prod

你可能已经注意到了,spring.profiles.active属性名是复数形式的profile。这意味着我们可以设置多个激活的profile。如果使用环境变量,通常这可以通过逗号分隔的列表来实现:

1
% export SPRING_PROFILES_ACTIVE=prod,audit,ha

但是,在YAML中,我们要按照如下的方式来声明列表:

1
2
3
4
5
6
spring:
profiles:
active:
- prod
- audit
- ha

另外,值得一提的是,如果我们将Spring应用部署到Cloud Foundry中,将会自动激活一个名为cloud的profile。如果你的生产环境是Cloud Foundry,那么你可以将生产环境相关的属性放到cloud profile下。

在Spring应用中,profile不仅能够用来条件化地设置配置属性,接下来我们看一下如何基于处于激活状态的profile来声明特定的bean。

5.3.3 使用profile条件化地创建bean

有时候,为不同的profile创建一组独特的bean是非常有用的。正常情况下,不管哪个profile处于激活状态,Java配置类中声明的所有bean都会被创建。但是,假设我们希望某些bean仅在特定profile激活的情况下才需要创建。在这种情况下,@Profile注解可以将某些bean设置为仅适用于给定的profile。

例如,在TacoCloudApplication中,我们有一个CommandLineRunner bean,它用来在应用启动的时候加载嵌入式数据库和配料数据。对于开发阶段来讲,这是很不错的;但是对于生产环境的应用来说,就没有必要(也是不符合需求的)了。为了防止在生产部署环境中每次都加载配料数据,我们可以为声明CommandLineRunner bean的方法添加@Profile注解,如下所示:

1
2
3
4
5
6
@Bean
@Profile("dev")
public CommandLineRunner dataLoader(IngredientRepository repo,
UserRepository userRepo, PasswordEncoder encoder) {
...
}

或者,假设我们在dev或qa profile激活的时候都需要创建CommandLineRunner。在这种情况下,我们可以为要创建的bean把所有profile都列出来:

1
2
3
4
5
6
@Bean
@Profile({"dev", "qa"})
public CommandLineRunner dataLoader(IngredientRepository repo,
UserRepository userRepo, PasswordEncoder encoder) {
...
}

现在,配料数据会在dev或qa profile激活的时候才加载。这意味着,在开发环境运行的时候我们需要将dev profile激活。如果除了prod激活时,CommandLineRunner bean都需要创建,那么我们可以采用一种更简便的方式。在这种情况下,我们可以按照如下的方式来使用@Profile

1
2
3
4
5
6
@Bean
@Profile("!prod")
public CommandLineRunner dataLoader(IngredientRepository repo,
UserRepository userRepo, PasswordEncoder encoder) {
...
}

在这里,感叹号(!)否定了profile的名称。实际上,它的含义是只要prod profile不激活就要创建CommandLineRunner bean。

我们还可以在带有@Configuration注解的类上使用@Profile。例如,假设我们要将CommandLineRunner抽取到一个名为DevelopmentConfig的配置类中,那么我们可以按照如下的方式为DevelopmentConfig添加@Profile

1
2
3
4
5
6
7
8
9
@Profile({"!prod", "!qa"})
@Configuration
public class DevelopmentConfig {
@Bean
public CommandLineRunner dataLoader(IngredientRepository repo,
UserRepository userRepo, PasswordEncoder encoder) {
...
}
}

在这里,CommandLineRunner bean(包括DevelopmentConfig中定义的其他bean)只有在prod和qa均没有激活的情况下才会创建。