通常情况,一个项目从开发到上线需要经历多个环境。例如程序员在自己的开发环境完成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虚拟机参数的配置如下图所示:

clipboard.png

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来源。

clipboard.png

除了通过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文件:

  1. 在应用部署路径下的/config子路径
  2. 在应用部署根路径
  3. 在classpath下的/config子package下
  4. 在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中的一个哲学——规约大于配置


poype
425 声望79 粉丝