SpringBoot核心特性——万字拆解外部配置
前言
在使用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
直接贴图,操作方式如下:
启动SpringApplication,请求 /hello,控制台输出如下:
可以看到命令行属性读取是成功的,这里补充一点,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,会发现程序无法正常启动了:
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格式的,设置如图:
--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属性,代码如下:
这里不再贴出控制台输出,实际运行结果输出name is MyApplication.
如果不想在application.properties或application.yaml中进行属性配置的话,也可以通过添加命令行参数来引导SpringBoot读取外部别的文件.
在resources目录下新建myconfig.yml文件
命令行参数如下:
--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移动到该文件夹:
启动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
application.yml添加配置spring.profiles.active: dev,表示当前需要启用application-dev.yml
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为空字符串,校验不会通过
启动失败
6. @ConfigurationProperties VS @Value
两者都是用于将application.yml等外部配置,映射绑定为Java Bean具体字段的值。相比于@Value只能用于字段、方法、方法入参等做对象的单一字段映射绑定,@ConfigurationProperties则要更为强大和灵活,支持校验、对象多字段映射等...不过根据开发需要按需使用就好,毕竟能解决当下问题就是最好的。
结尾
本文章源自《Learn SpringBoot》专栏,感兴趣的话还请关注点赞收藏.
上一篇文章:《SpringBoot核心特性——ApplicationRunner && CommandLineRunner使用》
下一篇文章:《SpringBoot核心特性——多环境配置的那些事》
转载自:https://juejin.cn/post/7258321382156533816