Spring Boot 配置加载的优先级:
- 命令行参数。
- 来自 java:comp/env 的 JNDI 属性。
- Java 系统属性(System.getProperties())。
- 操作系统环境变量。
- Random ValuePropertySource 配置的 random.* 属性值。
- jar 包外部的 application-{profile}.properties 或者 application-{profile}.yml 配置文件。
- jar 包内部的 application-{profile}.properties 或者 application-{profile}.yml 配置文件。
- jar 包外部的 application.properties 或者 application.yml 配置文件。
- jar 包内部的 application.properties 或者 application.yml 配置文件。
- @Configuration 注解类上的 @PropertySource。
- 通过 SpringApplication.setDefaultProperties 指定的默认属性。
1. 自定义配置
1.1. @Value
二者区别 | @ConfigurationProperties | @Value |
---|---|---|
功能 | 批量注入配置文件中的属性 | 一个个指定 |
松散绑定(松散语法) | 支持 | 不支持 |
SpEL语法 | 不支持 | 支持 |
JSR303数据校验 | 支持 | 不支持 |
复杂类型封装 | 支持 | 不支持 |
1.1.1 默认值设置
下面第一种写法要求对应的配置文件中必须有相应的 key,没有的话会抛出异常;第二种写法添加了默认值,则不需要必须对应的 key。设置默认值的方式:@Value("${key:defaultVlaue}")
java
@Value("${test.field.empty}")
java
@Value("${test.field.empty:123}")
1.1.2. 字符串格式
(1)yml 文件: 不加引号:字符串默认不用加上单引号或者双引号,不加引号和加单引号效果一样。 单引号:会转义字符串里面的特殊字符,特殊字符最终只会作为一个普通的字符串数据输出。 双引号:不会转义字符串里面的特殊字符;特殊字符会作为本身想表示的意思输出。例如下面示例中换行符则会起到换行作用。
yaml
custom:
field:
string: abc \n def
string1: 'abc \n def'
string2: "abc \n def"
yml 文件对应控制台输出结果:
python
abc \n def
abc \n def
abc
def
(2)properties 文件: properties 文件不会转义字符串里面的特殊字符;特殊字符会作为本身想表示的意思输出。引号会被一起输出。
java
custom.field.string=abc \n def
custom.field.string1='abc \n def'
custom.field.string2="abc \n def"
properties 文件对应控制台输出结果:
python
abc
def
'abc
def'
"abc
def"
(3)java 代码
java
@Value("${custom.field.string}")
private String testString;
@Value("${custom.field.string1}")
private String testString1;
@Value("${custom.field.string2}")
private String testString2;
...
System.out.println(testString);
System.out.println(testString1);
System.out.println(testString2);
1.1.3. List 的注入
${...}
用于加载外部属性文件中的值。 #{...}
用于执行 SpEL 表达式,并将执行结果的内容赋值给属性。 yml 文件写法:
yaml
custom:
field:
list: 2,4,6,8,10
properties 文件写法:
java
custom.field.list=2,4,6,8,10
Java 代码:
java
@Value("#{'${custom.field.list}'.split(',')}")
private List<Integer> list;
...
System.out.println(list);
控制台输出:
java
[2, 4, 6, 8, 10]
1.1.4. Map 的注入
yml 文件写法: 注意在 Map 解析中,一定要用引号把 Map 所对应的 value 包起来,要不然解析会失败,因为 yaml 语法中如果一个值以{
开头,yaml 将认为它是一个字典。(外层使用双引号和单引号的区别参考 1.2.,针对特殊字符是否转义的区别)
yaml
custom:
field:
map: "{name:'tom', age:18}"
properties 文件写法: properties 文件中值则不需要使用引号包起来。如果用了引号,则会被当做一个字符串,无法转为 Map。
java
custom.field.map={name:"tom", age:18}
Java 代码:
java
@Value("#{${custom.field.map}}")
private Map<String, Object> map;
1.1.5. 综合示例
yml 文件
yaml
custom:
field:
boolean: true
string: abc
integer: 123
long: 456
float: 1.23
double: 4.56
array: 1,2,3
list: 4,5,6
set: 7,8,9
map: "{name:'tom', age:18}"
properties 文件:
java
custom.field.boolean=true
custom.field.string=abc
custom.field.integer=123
custom.field.long=456
custom.field.float=1.23
custom.field.double=4.56
custom.field.array=1,2,3
custom.field.list=4,5,6
custom.field.set=7,8,9
custom.field.map={name:"tom", age:18}
Java 代码:
java
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
@Data
@Component
public class Custom2 {
@Value("${custom.field.boolean}")
private Boolean testBoolean;
@Value("${custom.field.string}")
private String testString;
@Value("${custom.field.integer}")
private Integer testInteger;
@Value("${custom.field.long}")
private Long testLong;
@Value("${custom.field.float}")
private Float testFloat;
@Value("${custom.field.double}")
private Double testDouble;
@Value("#{'${custom.field.array}'.split(',')}")
private Integer[] testArray;
@Value("#{'${custom.field.list}'.split(',')}")
private List<Integer> testList;
@Value("#{'${custom.field.set}'.split(',')}")
private List<Integer> testSet;
@Value("#{${custom.field.map}}")
private Map<String, Object> testMap;
@Value("${custom.field.empty:111}")
private Integer testEmpty;
}
控制台输出:
java
Custom(testBoolean=true, testString=abc, testInteger=123, testLong=456, testFloat=1.23, testDouble=4.56, testArray=[1, 2, 3], testList=[4, 5, 6], testSet=[7, 8, 9], testMap={name=tom, age=18}, testEmpty=111)
注:未找到 @Value 注解支持注入 List<Map<String, Object>> 类型的方法。
1.2. @ConfigurationProperties
1.2.1. 属性介绍
属性 | 描述 |
---|---|
prefix | 可有效绑定到此对象的属性的名称前缀。有效的前缀由一个或多个单词使用点分隔而成(例如 "acme.system.feature")。 |
value | 和 prefix 属性用法相同,用一个就行 |
ignoreInvalidFields | 表示绑定到此对象时应忽略无效字段。无效的意思是字段类型错误或无法强制转换为正确类型。默认为 false。 |
ignoreUnknownFields | 表示绑定到此对象时应忽略未知字段。未知字段是配置文件和对象属性不匹配的字段。默认为 true。 |
1.2.2. 综合示例
yml 文件:
yaml
user:
field:
name: zhangsan
age: 25
sex: true
other:
email: abc@163.com
idcard: 123456
interset1: english,math
interset2:
- music
- sports
family:
- mom: li
dad: wang
- nan: chen
yey: wang
properties 文件:
java
user.field.name=zhangsan
user.field.age=25
user.field.sex=true
user.field.other.email=abc@163.com
user.field.other.idcard=123456
user.field.interset1=english,math
user.field.interset2[0]=music
user.field.interset2[1]=sports
user.field.family[0].mom= li
user.field.family[0].dad= wang
user.field.family[1].nan= chen
user.field.family[1].yey= wang
Java 代码:
1.2.2.1. Class绑定
java
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
@Data
@Component
@ConfigurationProperties(prefix = "user.field")
public class Custom {
private String name;
private Integer age;
private Boolean sex;
private Map<String, Object> other;
private List<String> interset1;
private List<String> interset2;
private List<Map<String, String>> family;
}
...
@Autowired
private Custom custom;
...
System.out.println(custom);
控制台输出:
java
Custom(name=zhangsan, age=25, sex=true, other={email=abc@163.com, idcard=123456}, interset1=[english, math], interset2=[music, sports], family=[{mom=li, dad=wang}, {nan=chen, yey=wang}])
1.2.2.2. Bean绑定
java
import lombok.Data;
import java.util.List;
import java.util.Map;
@Data
public class Custom {
private String name;
private Integer age;
private Boolean sex;
private Map<String, Object> other;
private List<String> interset1;
private List<String> interset2;
private List<Map<String, String>> family;
}
java
import com.example.demo.model.properties.Custom;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 自定义配置类
* @author wangbo
* @date 2020/7/16 15:02
*/
@Configuration
public class CustomConfiguration {
//不指定name的话默认使用方法名作为bean的名称
@Bean(name = "myCustom")
@ConfigurationProperties(prefix = "user.field")
public Custom buildCustom(){
return new Custom();
}
}
java
@Autowired
private Custom custom;
...
System.out.println(custom);
控制台输出:
java
Custom(name=zhangsan, age=25, sex=true, other={email=abc@163.com, idcard=123456}, interset1=[english, math], interset2=[music, sports], family=[{mom=li, dad=wang}, {nan=chen, yey=wang}])
1.2.3. 松散绑定
yml 和 properties 类似,下面仅以 properties 进行示例。
(1)@ConfigurationProperties 注解的 prefix 属性写的时候不支持松散绑定。只有对应的字段值支持松散绑定。
(2)可以使用驼峰式,下划线,短横线,大写字母,或者这几种混合使用都可以实现 Java 驼峰字段绑定。驼峰和分隔符可以在任意位置,但是最好是在单词分隔处添加分隔符,单词首字母使用驼峰(首字母小写)。推荐短横线的写法,这个感觉最清晰明了。
java
#测试正常绑定
loose.binging.firstName=one
#测试下划线
loose.binging.two_name=two
#测试短横线(多单词推荐写法,清晰明了)
loose.binging.three-name=three
#测试大写
loose.binging.FOURNAME=four
#测试混合使用
loose.binging.FI-VENA_ME=five
Java 代码:
java
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "loose.binging")
public class Loose {
private String firstName;
private String twoName;
private String threeName;
private String fourName;
private String fiveName;
}
1.2.4. 自定义结构
属性文件: yml 和 properties 类似,下面仅以 properties 进行示例。
java
company.employee.man[0].name=小刚
company.employee.man[0].age=25
company.employee.man[1].name=小明
company.employee.man[1].age=27
company.employee.woman[0].name=小美
company.employee.woman[0].age=24
company.employee.woman[1].name=小娟
company.employee.woman[1].age=26
Java 代码:
java
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
@Data
@Component
@ConfigurationProperties(prefix = "company.employee")
public class Employee {
private List<People> man;
private List<People> woman;
@Data
public static class People{
private String name;
private Integer age;
}
}
...
@Autowired
private Employee employee;
...
System.out.println(employee);
控制台输出:
java
Employee(man=[Employee.People(name=小刚, age=25), Employee.People(name=小明, age=27)], woman=[Employee.People(name=小美, age=24), Employee.People(name=小娟, age=26)])
1.2.5. 参数校验
spring-boot-starter-web 依赖会自动引入 validation-api 依赖,在 validation-api 下的 javax.validation.constraints 包下有很多校验器,可以直接使用。
1.3. @PropertySource
如果想读取自定义 properties 文件的自定义属性值,可以使用 @PropertyResource 注解指定文件路径。
@PropertySource 默认不支持 yaml 文件读取,但是可以自己修改实现支持,只需要继承 DefaultPropertySourceFactory 类并修改就可以了,这里就不说了。
1.3.1. 属性介绍
属性 | 描述 |
---|---|
name | 指示此属性源的名称。如果省略,名字会根据底层资源的描述生成。 |
value | 指示要加载的属性文件的资源位置。支持 properties 和 xml 格式的属性文件,例如 "classpath:/com/myco/app.properties" 或者 "file:/path/to/file.xml"。不允许使用资源位置通配符,例如 **/*.properties。每个指定的位置必须只有一个属性文件。 |
ignoreResourceNotFound | 指定的配置文件不存在是否报错,默认是false。当设置为 true 时,若该文件不存在,程序不会报错。实际项目开发中,最好设置为 false。 |
encoding | 指定读取属性文件所使用的编码,通常使用的是 UTF-8。 |
factory | 指定一个自定义的 PropertySourceFactory,默认情况下,将使用标准资源文件的默认工厂。 |
1.3.2. 综合示例
在 resources 目录下新建了一个自定义的 source.properties 文件。
java
user.field.name=lisi
user.field.age=29
user.field.sex=false
user.field.other.email=def@163.com
user.field.other.idcard=654321
user.field.interset1=english,math
user.field.interset2[0]=music
user.field.interset2[1]=sports
user.field.family[0].mom= li
user.field.family[0].dad= wang
user.field.family[1].nan= chen
user.field.family[1].yey= wang
Java 代码: 也可使用 @Value 注解进行字段绑定,这里只用 @ConfigurationProperties 注解进行示例。
java
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
@Data
@Component
@ConfigurationProperties(prefix = "user.field")
@PropertySource(value = "classpath:source.properties", encoding = "UTF-8")
public class Custom {
private String name;
private Integer age;
private Boolean sex;
private Map<String, Object> other;
private List<String> interset1;
private List<String> interset2;
private List<Map<String, String>> family;
}
...
@Autowired
private Custom custom;
...
System.out.println(custom);
控制台输出:
java
Custom(name=lisi, age=29, sex=false, other={idcard=654321, email=def@163.com}, interset1=[english, math], interset2=[music, sports], family=[{mom=li, dad=wang}, {yey=wang, nan=chen}])
1.4. 参数间引用
属性文件:
yml 和 properties 类似,下面仅以 properties 进行示例。
java
blog.name=一线大码
blog.title=SpringBoot自定义配置和多环境配置
blog.desc=${blog.name}正在写${blog.title}
Java 代码: 也可使用 @Value 注解进行字段绑定,这里只用 @ConfigurationProperties 注解进行示例。
java
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "blog")
public class Blog {
private String name;
private String title;
private String desc;
}
...
@Autowired
private Blog1 blog;
...
System.out.println(blog);
控制台输出:
java
Blog(name=一线大码, title=SpringBoot自定义配置和多环境配置, desc=一线大码正在写SpringBoot自定义配置和多环境配置)
中间遇到了中文乱码问题,可以设置 IDEA 的文件编码为 UTF-8 即可。
1.5. 使用随机数
属性文件:
yml 和 properties 类似,下面仅以 properties 进行示例。
java
# 随机字符串
test.random.string=${random.value}
# 随机int
test.random.number=${random.int}
# 随机long
test.random.bignumber=${random.long}
# 10以内的随机数
test.random.minnumber=${random.int(10)}
# 10-20的随机数
test.random.rangenumber=${random.int[10,20]}
Java 代码:
也可使用 @Value 注解进行字段绑定,这里只用 @ConfigurationProperties 注解进行示例。
java
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "test.random")
public class TestRandom {
private String string;
private Integer number;
private Long bigNumber;
private Integer minNumber;
private Integer rangeNumber;
}
...
@Autowired
private TestRandom testRandom;
...
System.out.println(testRandom);
控制台输出:
java
TestRandom(string=83299a6016a3583c890eb472ce1ac5cc, number=-1814272333, bigNumber=-7943516043135095470, minNumber=1, rangeNumber=11)
2. 多环境配置
2.1. 命令行设置属性值
Spring Boot 项目 Jar 包启动命令:java -jar xxx.jar --server.port=8888
,通过使用--server.port
属性来设置xxx.jar
应用的端口为8888
。
在命令行运行时,连续的两个减号--
就是对 application.properties 中的属性值进行赋值的标识。所以,java -jar xxx.jar --server.port=8888
命令,等价于在 application.properties 中添加属性 server.port=8888。application.yml 中类似。
通过命令行来修改属性值固然提供了不错的便利性,但是通过命令行就能更改应用运行的参数,那岂不是很不安全?是的,所以 Spring Boot 也提供了屏蔽命令行访问属性的设置:SpringApplication.setAddCommandLineProperties(false)
。具体代码设置如下所示:
java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
//原有启动代码
//SpringApplication.run(DemoApplication.class, args);
//修改后的启动代码
SpringApplication application = new SpringApplication(DemoApplication.class);
//禁用命令行参数
application.setAddCommandLineProperties(false);
application.run(args);
}
}
可以通过 IDEA 设置 SpringBoot 项目启动参数。
2.2. 加载环境配置文件
yml 文件同 properties 文件一样,下面仅以 properties 文件进行说明。
在 SpringBoot 中多环境配置文件名需要满足application-{profile}.properties
的格式,其中{profile}
对应你的环境标识,环境标志可以自己定义字符串,比如:
java
application-dev.properties:开发环境
application-test.properties:测试环境
application-prod.properties:生产环境
具体哪个配置文件会被加载,需要在application.properties
文件中通过spring.profiles.active
属性来设置,其值对应{profile}
值。如:spring.profiles.active=dev
就会加载application-dev.properties
配置文件内容。不管加载哪个环境配置文件,基础配置文件application.properties
都会被加载。
可以得出以下使用方法:
(1)application.properties
中配置通用内容,比如设置spring.profiles.active=dev
,表示以开发环境为默认配置。 (2)application-{profile}.properties
中配置各个环境不同的内容。 (3)启动的时候通过命令行设置属性值的方式去激活不同环境的配置,比如
powershell
java -jar app.jar --spring.profiles.active=prod
表示启动的时候加载application-prod.properties
配置文件内容。
如果基础配置文件application.properties
中未配置spring.profiles.active
属性,项目启动时命令行也没设置spring.profiles.active
属性,则不会加载环境配置文件,只使用基础配置文件,如果基础配置文件中没设置端口,则会使用默认的 Tomcat 端口 8080。启动日志如下:
java
- No active profile set, falling back to default profiles: default
- Tomcat initialized with port(s): 8080 (http)
2.3. 多环境日志配置
比如我们 SpringBoot 项目的 logback 日志配置,也需要区分不同的环境,有时候还需要获取配置文件中的配置。 (1)springProperty
标签可以获取配置文件中的属性。比如我们需要在 logback-spring.xml 配置文件中获取配置文件的 spring.profiles.active 属性:
xml
<springProperty scope="context" name="profiles" source="spring.profiles.active"/>
在 logback-spring.xml 中的其他位置可以直接使用$
符号通过name
属性去获取对应的值,比如获取上面的配置:${profiles}
(2)springProfile
标签允许我们更加灵活配置文件,可选地包含或排除部分配置内容。元素中的任何位置均支持。使用该标签的name
属性可以指定具体的环境,可以使用逗号分隔列表指定多个环境的配置文件。具体的环境由 spring.profiles.active 属性指定。例如:
xml
<springProfile name="dev">
<!-- 开发环境时激活 -->
</springProfile>
<springProfile name="dev,test">
<!-- 开发,测试的时候激活-->
</springProfile>
<springProfile name="!prod">
<!-- 当生产环境时,该配置不激活。不是生产环境时激活-->
</springProfile>
(3)还可以将 logback.xml 文件拆分为 logback-dev.xml,logback-test.xml,logback-prod.xml 三个环境对应的配置文件(logback-{profile}.xml)。具体的环境由 spring.profiles.active 属性指定。 application.properties 里面还需要添加配置:
java
logging.config: classpath:logback-${spring.profiles.active}.xml
这样也可以实现多环境日志配置。
2.4. 多环境代码调用
在某些情况下,在不同环境中应用的某些业务逻辑可能需要有不同的实现。例如邮件服务,假设 EmailService 中包含的 send() 方法向指定地址发送电子邮件,但是我们只希望在生产环境中才执行真正发送邮件的代码,而开发和测试环境里则不发送,以免向用户发送无意义的垃圾邮件。我们可以借助 Spring 的注解 @Profile 注解实现这样的功能,需要定义两个 EmailService 接口的实现类。 EmailService 接口:
java
package com.example.demo.service;
/**
* @author wangbo
* @date 2020/7/16 11:26
*/
public interface EmailService {
/**
* 发送邮件
*/
void send();
}
开发环境和测试环境实现类: @Profile(value = {"dev", "test"})
表示只有 Spring 定义的 profile 为 dev 或者 test 时才会实例化 EmailServiceImpl1 这个类。
java
package com.example.demo.service.impl;
import com.example.demo.service.EmailService;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
/**
* @author wangbo
* @date 2020/7/16 11:28
*/
@Service
@Profile(value = {"dev", "test"})
public class EmailServiceImpl1 implements EmailService {
@Override
public void send() {
System.out.println("开发环境和测试环境发送邮件");
}
}
生产环境实现类: @Profile(value = {"prod"})
表示只有 Spring 定义的 profile 为 prod 时才会实例化 EmailServiceImpl2 这个类。
java
package com.example.demo.service.impl;
import com.example.demo.service.EmailService;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
/**
* @author wangbo
* @date 2020/7/16 11:29
*/
@Service
@Profile(value = {"prod"})
public class EmailServiceImpl2 implements EmailService {
@Override
public void send() {
System.out.println("生产环境发送邮件");
//具体的发送代码省略
}
}
调用该 Service 的代码:
java
@Autowired
private EmailService emailService;
...
emailService.send();
3. 静态成员变量注入
3.1. set 方法注入方式
有时候我们需要注入一个静态成员变量,例如在工具类中某个静态工具方法使用到某个配置项或者需要用到某个 bean。示例:
java
@Component
public class JwtUtil {
private static RedisTemplate redisTemplate;
@Autowired
public void setRedisTemplate(RedisTemplate redisTemplate) {
JwtUtil.redisTemplate = redisTemplate;
}
private static String accessKey;
@Value("${access.key}")
public void setAccessKey(String accessKey) {
JwtUtil.accessKey = accessKey;
}
}
注意项:
- 需要在该类上添加注解
@Component
。 - 静态属性不能直接注入,需要通过其 set 方法进行注入,注意自动生成的 set 方法需要去掉
static
。
3.2. @PostConstruct 注解方式
执行顺序:
Constructor(构造方法) -> @Autowired(依赖注入) / @Value -> @PostConstruct(初始化方法)
所以可以使用@PostConstruct
注解在 Spring 初始化之后再给静态变量赋值。
java
@Component
public class SmsVerificationCodeUtil {
@Value("${value}")
private String value;
private static String valueStatic;
/**
* 给静态变量赋值
*/
@PostConstruct
private void init() {
valueStatic = value;
}
// 即可使用 valueStatic
public static String method(){
return valueStatic;
}
}
4. 附录说明
(1)我们使用了@Component
或者@Configuration
注解配置类为 spring 管理的类,这样在别的类才可以进行 @Autowired 注入使用。在其他的一些文章中并没有使用这两个注解来将配置类注册到 Spring 容器中,而是通过在启动类上添加 @EnableConfigurationProperties({xxx.class, xxxx.class})
这样的方式实现配置类注入。这两种方式都可以。
(2)@Component
和@Configuration
都可以作为配置类注解,这两的区别可以参考以下文章: @Configuration和@Component区别 @Component和@Configuration作为配置类的差别
(3)可以了解下@ConditionalOnProperties
注解,参考文章: @ConfigurationProperties和@ConditionalOnProperties的理解与使用