likes
comments
collection
share

SpringBoot核心特性——万字拆解外部配置

作者站长头像
站长
· 阅读数 15

前言

在使用SpringBoot开发的过程中,有时候可能需要修改一些默认配置,或者需要SpringBoot读取一些开发者自定义配置,根据不同的开发环境使用不同的配置等,这样就需要用到SpringBoot外部配置的功能了,以下便是对此功能做一些详细讲解。

前置工作

新建一个MyConfig类,该类包含一个name属性,和一个printName()方法,代码如下:

package geek.springboot.application.configuration;  
  
import lombok.extern.slf4j.Slf4j;  
import org.springframework.beans.factory.annotation.Value;  
import org.springframework.stereotype.Component;  
  
/**  
* 自定义配置  
*  
* @author Bruse  
*/  
@Slf4j  
@Component  
public class MyConfig {  
  
    /**  
    * 姓名  
    * 添加上@Value注解,表示该属性值从配置中读取  
    */  
    @Value("${name}")  
    private String name;  
  
    /**  
    * 打印name的值  
    */  
    public void printName() {  
        log.info("My name is {}", this.name);  
    }  
  
}

添加一个HelloController,在外部发起Get请求时,打印MyConfig的name属性,查看配置是否成功,代码如下:

package geek.springboot.application.controller;  
  
import geek.springboot.application.configuration.MyConfig;  
import lombok.extern.slf4j.Slf4j;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.web.bind.annotation.GetMapping;  
import org.springframework.web.bind.annotation.RestController;  
  
/**  
* Controller  
*  
* @author Bruse  
*/  
@Slf4j  
@RestController  
public class HelloController {  
  
    @Autowired  
    private MyConfig myConfig;  
  
    @GetMapping("/hello")  
    public String hello() {  
        // 打印name属性  
        this.myConfig.printName();  
        return "hello";  
    }  
  
}

外部配置的N种方式

1. 添加命令行Properties

直接贴图,操作方式如下: SpringBoot核心特性——万字拆解外部配置

SpringBoot核心特性——万字拆解外部配置

SpringBoot核心特性——万字拆解外部配置

启动SpringApplication,请求 /hello,控制台输出如下:

SpringBoot核心特性——万字拆解外部配置

可以看到命令行属性读取是成功的,这里补充一点,SpringApplication也可以关闭读取命令行参数功能,代码如下:

package geek.springboot.application;  
  
import lombok.extern.slf4j.Slf4j;  
import org.springframework.boot.SpringApplication;  
import org.springframework.boot.autoconfigure.SpringBootApplication;  
  
@Slf4j  
@SpringBootApplication  
public class Application {  
  
    public static void main(String[] args) {  
        // 初始化SpringApplication  
        SpringApplication springApplication = new SpringApplication(Application.class);  
        // 关闭命令行参数  
        springApplication.setAddCommandLineProperties(false);  
        // 启动SpringApplication  
        springApplication.run(args);  
    }  
  
}

关闭命令行参数后,再次尝试启动SpringApplication,会发现程序无法正常启动了:

SpringBoot核心特性——万字拆解外部配置

SpringBoot核心特性——万字拆解外部配置

2.添加Java Properties

代码如下:

package geek.springboot.application;  
  
import lombok.extern.slf4j.Slf4j;  
import org.springframework.boot.SpringApplication;  
import org.springframework.boot.autoconfigure.SpringBootApplication;  
  
import java.util.Properties;  
  
@Slf4j  
@SpringBootApplication  
public class Application {  

    public static void main(String[] args) {  
        // 初始化SpringApplication  
        SpringApplication springApplication = new SpringApplication(Application.class);  
        // 声明属性  
        Properties properties = new Properties();  
        // 设置name属性  
        properties.put("name", "Bruse");  
        // 设置默认属性  
        springApplication.setDefaultProperties(properties);  
        // 启动SpringApplication  
        springApplication.run(args);  
    }  
  
}

这里不再贴出控制台输出,实际运行结果输出name is Bruse.

3.添加JSON Properties

直接添加命令行参数,不过是JSON格式的,设置如图:

SpringBoot核心特性——万字拆解外部配置

--spring.application.json={\"name\":\"test\"}

这里不再贴出控制台输出,实际运行结果输出name is test.

4. 添加外部配置文件 xxx.properties / xxx.yaml

SpringBoot在运行时,默认会自动从以下路径查找并加载application.properties和application.yaml文件

从 classpath 类路径查询,包括类路径,和该路径下的/config文件夹.
从当前目录查询,包括当前目录,和该目录下的/config文件夹.

这里简单做个示例,在项目resources文件夹下,创建一个application.yaml文件,配置上name属性,代码如下:

SpringBoot核心特性——万字拆解外部配置

SpringBoot核心特性——万字拆解外部配置

这里不再贴出控制台输出,实际运行结果输出name is MyApplication.

如果不想在application.properties或application.yaml中进行属性配置的话,也可以通过添加命令行参数来引导SpringBoot读取外部别的文件.

在resources目录下新建myconfig.yml文件

SpringBoot核心特性——万字拆解外部配置

命令行参数如下:

--spring.config.name=myconfig

或者使用另一种方式让SpringBoot加载myconfig.yml,命令行参数如下:

--spring.config.location=classpath:/myconfig.yml

启动SpringApplication,这里不再贴出控制台输出,两种方式实际运行结果输出name is MyConfig.

两种方式的区别在于--spring.config.name=xxx,只能指定SpingBoot加载额外的仅此一份配置文件,但是--spring.config.location=xxx.yml,却可以指定SpringBoot加载多份配置文件,比如--spring.config.location=xxx.yml,xxxx.yml

另外,--spring.config.location=xxx.yml,还可以加上optional:前缀,表示该文件不一定存在。 完整表示为: --spring.config.location=optional:classpath:/xxx.yml

--spring.config.location 和 --spring.config.name 还可以搭配使用,在resources下新建config文件夹,并把myconfig.yml移动到该文件夹:

SpringBoot核心特性——万字拆解外部配置

启动SpringApplication时,同时指定config location和config name

--spring.config.location=classpath:/config/ --spring.config.name=myconfig

实际运行结果输出name is MyConfig.

4.1 多环境配置隔离

在平常开发中,生产、测试、开发...不同环境的配置都各不相同,所以这里再简单介绍一下如何根据当前环境需要,启用相应的配置。

在resources目录下新建application-dev.yml(开发环境)、application-prod.yml(生产环境)、application-stage.yml(测试环境),不同环境的application-xxx.yml,分别配置不同的name: dev、name: prod、 name: stage

SpringBoot核心特性——万字拆解外部配置

application.yml添加配置spring.profiles.active: dev,表示当前需要启用application-dev.yml

SpringBoot核心特性——万字拆解外部配置

spring.profiles.active也可以指定多个配置文件,比如spring.profiles.active = dev,prod

实际运行结果输出name is dev.

4.2 包含多个逻辑分区的配置文件

SpringBoot支持读取包含多个逻辑文档的单个物理文件,排在最后的逻辑文档可以覆盖前面的文档所定义的属性。

在resources目录下创建application.yml,代码示例如下:

spring:  
application:  
name: MyApp  
  
name: myApp  
  
# 多个逻辑文档用 --- 分割  
---  
  
spring:  
application:  
name: otherApp  
name: otherApp

启动SpringApplication,控制台输出name is otherApp, 因为最底下的逻辑文档的name,覆盖了上边的name。

4.3 配置随机值

还是application.yml稍作改动,将name的值改成随机UUID,这里涉及到${}占位符和使用random生成随机值两个知识点,代码如下:

spring:  
application:  
name: MyApp  
  
name: myApp  
  
# 多个逻辑文档用 --- 分割  
---  
  
spring:  
application:  
name: otherApp  
name: ${random.uuid}  
# ${}表示占位符,可以在${}中引用一些别的属性值,比如${spring.application.name},表示name的值等于spring.application.name的值  
# ${random}则表示调用SpringBoot内建变量random的uuid方法,将该方法的值作为name的值

控制台输出name is 随机生成的uuid。

5. 使用@PropertySource注解

MyConfig调整后代码如下:

package geek.springboot.application.configuration;  
  
import lombok.extern.slf4j.Slf4j;  
import org.springframework.beans.factory.annotation.Value;  
import org.springframework.context.annotation.Configuration;  
import org.springframework.context.annotation.PropertySource;  
  
/**  
* 自定义配置  
*  
* @author Bruse  
*/  
@Slf4j  
@Configuration // 声明为一个配置类  
@PropertySource("classpath:application.yml") // 指定文件路径,可以设置多个路径  
public class MyConfig {  
  
    /**  
    * 姓名  
    * 添加上@Value注解,表示该值从配置中读取  
    */  
    @Value("${name}")  
    private String name;  
  
    /**  
    * 打印name的值  
    */  
    public void printName() {  
        log.info("My name is {}", this.name);  
    }  
  
}

resource目录下的application.yml :

spring:  
    application:  
        name: MyApp  
name: myApp

控制台输出name is myApp .

配置绑定

1. @ConfigurationProperties

除了用前面提到的@Value注解来对Java Bean的单个属性进行配置绑定外,也可以使用@ConfigurationProperties注解来对Java Bean的多个属性进行绑定,以下简单做个示例:

创建一个用户配置类

package geek.springboot.application.configuration;  
  
import lombok.extern.slf4j.Slf4j;  
import org.springframework.boot.context.properties.ConfigurationProperties;  
import org.springframework.stereotype.Component;  
  
/**  
* 用户配置Config  
*  
* @author Bruse  
*/  
@Slf4j  
@Component  
// 添加@ConfigurationProperties注解,这里写上user[也就是将要读取配置的前缀]表示,需要把user相关的配置与UserConfig的属性进行映射转换  
@ConfigurationProperties("user")  
public class UserConfig {  
  
    private String name;  

    private String password;  

    private Integer age;  
  
    public String getName() {  
        return name;  
    }  

    public void setName(String name) {  
        this.name = name;  
    }  

    public String getPassword() {  
        return password;  
    }  

    public void setPassword(String password) {  
        this.password = password;  
    }  

    public Integer getAge() {  
        return age;  
    }  

    public void setAge(Integer age) {  
        this.age = age;  
    }  

    @Override  
    public String toString() {  
        return "UserConfig{" +  
        "name='" + name + '\'' +  
        ", password='" + password + '\'' +  
        ", age=" + age +  
        '}';  
    }  
  
}

application.yml相关配置

user:  
    name: root  
    age: 17  
    password: 13ddff

输出打印UserConfig

UserConfig{name='Bruse', password='13ddff', age=17}

2. @ConstructorBinding

以上@ConfigurationProperties演示,本质是调用Java Bean的setxxx()方法来进行属性值注入的,这里也有另一种通过构造器进行属性值注入的方式。

在SpringApplication启动类加上@ConfigurationPropertiesScan注解,并配置要扫描的路径

package geek.springboot.application;  
  
import lombok.extern.slf4j.Slf4j;  
import org.springframework.boot.SpringApplication;  
import org.springframework.boot.autoconfigure.SpringBootApplication;  
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;  
  
@Slf4j  
@SpringBootApplication  
// 添加@ConfigurationPropertiesScan注解,并配置修饰了@ConfigurationProperties注解的类路径  
@ConfigurationPropertiesScan({"geek.springboot.application.configuration"})  
public class Application {  
  
    public static void main(String[] args) {  
        // 初始化SpringApplication  
        SpringApplication springApplication = new SpringApplication(Application.class);  
        // 启动SpringApplication  
        springApplication.run(args);  
    }  
  
}

调整UserConfig,删除所有setxxx()方法,移除掉@Component注解,创建全参构造函数,添加上@ConstructorBinding注解

package geek.springboot.application.configuration;  
  
import lombok.extern.slf4j.Slf4j;  
import org.springframework.boot.context.properties.ConfigurationProperties;  
import org.springframework.boot.context.properties.ConstructorBinding;  
  
/**  
* 用户配置Config  
*  
* @author Bruse  
*/  
@Slf4j  
// 添加@ConfigurationProperties注解,这里写上user[也就是将要读取配置的前缀]表示,需要把user相关的配置与UserConfig的属性进行映射转换  
@ConfigurationProperties("user")  
public class UserConfig {  
  
    private String name;  

    private String password;  

    private Integer age;  
  
    // 添加 @ConstructorBinding ,指定SpringBoot在初始化UserConfig时调用该构造方法进行属性注入
    // 如果Java Bean有多个构造函数, 用@ConstructorBinding修饰要指定的构造函数即可
    @ConstructorBinding  
    public UserConfig(String name, String password, Integer age) {  
        this.name = name;  
        this.password = password;  
        this.age = age;  
    }  
  
    @Override  
    public String toString() {  
        return "UserConfig{" +  
        "name='" + name + '\'' +  
        ", password='" + password + '\'' +  
        ", age=" + age +  
        '}';  
    }  
  
}

输出效果和之前如出一辙

3. @EnableConfigurationProperties

有时 @ConfigurationProperties 注解的Java Bean,可能不会被SpringApplication启动时所扫描,这个时候除了可以使用 @ConfigurationPropertiesScan 指定扫描路径外,也可以使用 @EnableConfigurationProperties

这里沿用上面的代码作为基础,只需要在SpringApplication启动类上做一点调整:

package geek.springboot.application;  
  
import geek.springboot.application.configuration.UserConfig;  
import lombok.extern.slf4j.Slf4j;  
import org.springframework.boot.SpringApplication;  
import org.springframework.boot.autoconfigure.SpringBootApplication;  
import org.springframework.boot.context.properties.EnableConfigurationProperties;  
  
@Slf4j  
@SpringBootApplication  
// 添加EnableConfigurationProperties注解  
@EnableConfigurationProperties(UserConfig.class)  
public class Application {  
  
    public static void main(String[] args) {  
        // 初始化SpringApplication  
        SpringApplication springApplication = new SpringApplication(Application.class);  
        // 启动SpringApplication  
        springApplication.run(args);  
    }  
  
}

UserConfig属性映射输出依然和之前如出一辙

4. List、Map绑定

这里再详解一下当Java Bean 属性包含了List、Map这种类型时,要怎么配置才能让SpringBoot映射注入属性成功

4.1 简单List绑定

在UserConfig新建一个List集合,关键代码如下:

    // List集合
    private List<String> teacherList;  
  
    @ConstructorBinding  
    public UserConfig(String name, String password, Integer age, List<String> teacherList) {  
        this.name = name;  
        this.password = password;  
        this.age = age;  
        this.teacherList = teacherList;  
    }

application.yml添加相关配置:

user:  
    name: root  
    age: 17  
    password: 13ddff  
    Teacher-List:  
        - "Cu"  
        - "Za"

Tips: @ConfigurationProperties 对于属性的命名规则是比较宽松的,不要求配置项的名称和Jave Bean属性名完全一致,比如UserConfig是teacherList,application.yml是Teacher-List,其实SpringBoot也可以正确将它们匹配上。

控制台输出如下: UserConfig{name='Bruse', password='13ddff', age=17, teacherList=Cu,Za,}

4.2 复杂List绑定

新建Teacher类,代码如下:

@AllArgsConstructor  
public static class Teacher {  

    private String name;  

    private String phone;  
  
    @Override  
    public String toString() {  
        return "Teacher{" +  
        "name='" + name + '\'' +  
        ", phone='" + phone + '\'' +  
        '}';  
    }  
}

UserConfig的teacherList稍作修改:

private List<Teacher> teacherList;  

application.yml稍作修改:

user:  
    name: root  
    age: 17  
    password: 13ddff  
    teacher-List:  
        - name: "Cu"  
          phone: "1131313"  
        - name: "Za"  
          phone: "131313"

输出 UserConfig{name='Bruse', password='13ddff', age=17, teacherList=Tearcher{name='Cu', phone='1131313'},Tearcher{name='Za', phone='131313'},}

4.3 简单Map绑定

UserConfig新建addressMap:

private Map<String, String> addressMap;  
  
@ConstructorBinding  
public UserConfig(String name, String password, Integer age, List<Teacher> teacherList, Map<String, String> addressMap) {  
    this.name = name;  
    this.password = password;  
    this.age = age;  
    this.teacherList = teacherList;  
    this.addressMap = addressMap;  
}

application.yml稍作调整:

user:  
name: root  
age: 17  
password: 13ddff  
teacher-list:  
    - name: "Cu"  
      phone: "1131313"  
    - name: "Za"  
      phone: "131313"  
address-map:  
    # 这里分别对应Map每个元素的Key和Value
    key1: "北京"  
    key2: "天津"  
    key3: "上海"

输出UserConfig{name='Bruse', password='13ddff', age=17, addressMap=北京, 天津, 上海, }

4.4 复杂Map绑定

新建Address类

@AllArgsConstructor  
public static class Address {  
  
    private String city;  

    private String address;  
  
    @Override  
    public String toString() {  
        return "Address{" +  
        "city='" + city + '\'' +  
        ", address='" + address + '\'' +  
        '}';  
    }  
}

UserConfig稍作修改

private Map<String, Address> addressMap;

application.yml稍作修改

user:  
name: root  
age: 17  
password: 13ddff  
teacher-list:  
    - name: "Cu"  
      phone: "1131313"  
    - name: "Za"  
      phone: "131313"  
address-map:  
    key1:  
     city: "北京"  
     address: "203号8楼802室"  
    key2:  
     city: "深圳"  
     address: "南山区1900号"  
    key3:  
     city: "上海"  
     address: "黄浦区8799号"

输出UserConfig{name='Bruse', password='13ddff', age=17, addressMap=Address{city='北京', address='203号8楼802室'}, Address{city='深圳', address='南山区1900号'}, Address{city='上海', address='黄浦区8799号'}, }

5. 属性校验

在对@ConfigurationProperties修饰的JavaBean进行配置绑定时,也可以对属性值进行校验.

在pom.xml引入依赖

<dependencies>  
    <dependency>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-validation</artifactId>  
    </dependency>  
</dependencies>

UserConfig添加@Validated注解

@Slf4j  
@Validated  
@ConfigurationProperties("user")  
public class UserConfig {
 // 代码...
}

这里以addressMap校验为例,对要校验的字段添加上@Valid注解

@Valid  
private Map<String, Address> addressMap;
@AllArgsConstructor  
public static class Address {  
  
    private String city;  

    // 这里约束address字段不能是空字符串或者空格
    @NotBlank(message = "address is blank")  
    private String address;  

    @Override  
    public String toString() {  
        return "Address{" +  
        "city='" + city + '\'' +  
        ", address='" + address + '\'' +  
        '}';  
    }  
}

application.yml调整

address-map:  
    key1:  
     city: "北京"  
     address: "203号8楼802室"  
    key2:  
     city: "深圳"  
     address: "南山区1900号"  
    key3:  
     city: "上海"  
     address: "" # 这里address为空字符串,校验不会通过

启动失败

SpringBoot核心特性——万字拆解外部配置

6. @ConfigurationProperties VS @Value

两者都是用于将application.yml等外部配置,映射绑定为Java Bean具体字段的值。相比于@Value只能用于字段、方法、方法入参等做对象的单一字段映射绑定,@ConfigurationProperties则要更为强大和灵活,支持校验、对象多字段映射等...不过根据开发需要按需使用就好,毕竟能解决当下问题就是最好的。

结尾

本文章源自《Learn SpringBoot》专栏,感兴趣的话还请关注点赞收藏.

上一篇文章:《SpringBoot核心特性——ApplicationRunner && CommandLineRunner使用