使用Profile进行配置

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

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

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

logging:
  level:
    tacos: DEBUG

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

定义特定profile的属性

定义特定profile相关的属性的一种方式就是创建另外一个YAML或属性文件,其中只包含用于生产环境的属性。文件的名称要遵守如下的约定:application-{profile名}.yml或 application-{profile名}.properties。

然后,我们就可以在这里声明适用于该profile的配置属性了。例如,我们可以创建一个新的名为application-prod.yml的文件,其中包含如下属性:

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如下所示:

logging:
  level:
    tacos: DEBUG

---
spring:
  profiles: prod

  datasource:
    url: jdbc:mysql://localhost/tacocloud
    username: tacouser
    password: tacopassword

logging:
  level:
    tacos: WARN

我们可以看到,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特定的相关属性。虽然这两种方法各有利弊,但是在实践中,一般来说,当属性数量较少时,将所有profile配置放在一个YAML文件中效果更好,而当有大量的属性时,为每个profile建立不同的文件效果会更好。

激活profile

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

spring:
  profiles:
    active:
    - prod

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

% export SPRING_PROFILES_ACTIVE = prod

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

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

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

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

% export SPRING_PROFILES_ACTIVE = prod,audit,ha

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

spring:
  profiles:
    active:
    - prod
    - audit
    - ha

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

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

使用profile条件化地创建bean

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

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

@Bean
@Profile("dev")
public CommandLineRunner dataLoader(IngredientRepository repo,
      UserRepository userRepo, PasswordEncoder encoder) {

  ...

}

或者,若在dev或qa profile激活的时候都需要创建CommandLineRunner,则可以将需要创建bean的所有profile都列出来:

@Bean
@Profile({"dev", "qa"})
public CommandLineRunner dataLoader(IngredientRepository repo,
      UserRepository userRepo, PasswordEncoder encoder) {

  ...

}

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

@Bean
@Profile("!prod")
public CommandLineRunner dataLoader(IngredientRepository repo,
      UserRepository userRepo, PasswordEncoder encoder) {

  ...

}

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

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

@Profile({"!prod", "!qa"})
@Configuration
public class DevelopmentConfig {

  @Bean
  public CommandLineRunner dataLoader(IngredientRepository repo,
        UserRepository userRepo, PasswordEncoder encoder) {

    ...

  }

}

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

小结

  • Spring bean可以添加@ConfigurationProperties注解,以从多个属性源中选取一个来注入它的值。

  • 配置属性可以通过命令行参数、环境变量、JVM系统属性、属性文件或YAML文件等方式进行设置。

  • 配置属性可以用来覆盖自动配置相关的设置,包括指定数据源URL和日志级别。

  • Spring profile可以与属性源协同使用,从而能够基于激活的profile条件化地设置配置属性。