上篇我们学习了《SB实战18-Spring Boot的应用配置》,本篇我们学习Spring Boot的外部配置。
3.3 外部配置
Spring Boot可以从命令行、环境变量、properties文件、YAML文件等外部获得配置,而这个能力是Environment提供给我们的。我们可以通过三种方式来访问Environment中的属性:
- 使用@Value注解,我们在上一章已经演示过;
- 注入Environment的Bean,我们在上一章也演示过;
- 通过@ConfigurationProperties注解来访问,这节我们会讲解。
为了更深入的理解外部配置的原理,我们首先需要了解Environment抽象。
3.3.1 外部配置源与Environment
我们在上一章了解到,Environment包含两部分的内容:Profile和Property。Environment的定义如下:
public interface Environment extends PropertyResolver {
String[] getActiveProfiles();
String[] getDefaultProfiles();
boolean acceptsProfiles(Profiles profiles);
}
Environment的三个接口方法负责Profile相关内容,而它继承的PropertyResolver接口负责的是对Property的查询。
public interface PropertyResolver {
boolean containsProperty(String key);
@Nullable
String getProperty(String key);
String getProperty(String key, String defaultValue);
@Nullable
<T> T getProperty(String key, Class<T> targetType);
<T> T getProperty(String key, Class<T> targetType, T defaultValue);
String getRequiredProperty(String key) throws IllegalStateException;
<T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException;
String resolvePlaceholders(String text);
String resolveRequiredPlaceholders(String text) throws IllegalArgumentException;
}
在Environment中每一个配置属性都是PropertySource,多个PropertySource可聚集成PropertySources。
PropertyResolver的实现类PropertySourcesPropertyResolver负责对PropertySources进行查询操作,即Environment可对PropertySources进行查询操作。
Spring不支持YAML文件作为PropertySource,Spring Boot使用YamlPropertySourceLoader来读取YAML文件并获得PropertySource。
在Spring Boot下外部配置属性的加载顺序优先级如下,先列的属性配置优先级高,先列的配置属性可覆盖后列的配置属性。
- 命令行参数
- SPRING_APPLICATION_JSON
- ServletConfig 初始化参数
- ServletContext 初始化参数
- JNDI (java:comp/env)
- Java系统属性(System.getProperties() )
- 操作系统变量
- RandomValuePropertySource 随机值
- 应用部署jar包外部的application-{profile}.properties/yml
- 应用部署jar包内部的application-{profile}.properties/yml
- 应用部署jar包外部的application.properties/yml
- 应用部署jar包内部的application.properties/yml
- @PropertySource
- SpringApplication.setDefaultProperties
3.3.1.1 命令行参数
- 使用gradle命令行传参:
$ ./gradlew bootRun --args='--server.port=8888 --server.ip=192.168.1.5'
- 若打成jar包传参:
$ ./gradlew bootJar
$ java -jar build/libs/spring-boot-in-depth-0.0.1-SNAPSHOT.jar --server.port=8888 --server.ip=192.168.1.5
- 在IntellJ IDEA里:
- 代码校验结果:
@Value("${server.ip}")
private String serverIp;//1
@Bean
CommandLineRunner commandLineRunner(@Value("${server.port}") String serverPort ){//2
return args -> {
Stream.of(args).forEach(System.out::println); //3
System.out.println(serverPort);
System.out.println(serverIp);
};
}
- 使用@Value注入值到类的变量中,server.ip不是Spring Boot内置配置,可接受自用;
- 使用@Value注入值到方法参数中,server.port是Spring Boot内置配置,会对应用配置起效,更改当前容器的端口号;
- 可从args参数中获取参数;
以上三种方式的运行结果均为:
3.3.1.2 SPRING_APPLICATION_JSON
- 作为系统环境变量
$ SPRING_APPLICATION_JSON='{"server":{"ip":"192.168.1.5","port":"8888"}}' java -jar build/libs/spring-boot-in-depth-0.0.1-SNAPSHOT.jar
- 使用系统属性
$ java -Dspring.application.json='{"server":{"ip":"192.168.1.5","port":"8888"}}' -jar build/libs/spring-boot-in-depth-0.0.1-SNAPSHOT.jar
- 使用命令行参数执行:
$ java -jar build/libs/spring-boot-in-depth-0.0.1-SNAPSHOT.jar --spring.application.json='{"server":{"ip":"192.168.1.5","port":"8888"}}'
- 使用Intellij IDEA
3.3.1.5 RandomValuePropertySource随机值
RandomValuePropertySource为我们产生随机值,如:
$ ./gradlew bootRun --args='--server.port=${random.int[1024,10000]} --server.ip=192.168.1.5 --some.value=${random.value} --some.number=${random.int}'
使用下面代码验证:
@Value("${server.ip}")
private String serverIp;
@Value("${some.value}")
private String someValue;
@Value("${some.number}")
private String someNumber;
@Bean
CommandLineRunner commandLineRunner(@Value("${server.port}") String serverPort){
return args -> {
Stream.of(args).forEach(System.out::println);
System.out.println(serverPort);
System.out.println(serverIp);
System.out.println(someValue);
System.out.println(someNumber);
};
}
执行刚开始的命令,控制台显示:
3.3.2 外部文件配置
Spring Boot会从以下位置加载外部配置文件application.properties/yml并读取成PropertySouces加载到Environment:
- 入口类的当前目录的/config子目录;
- 入口类的当前目录;
- 类路径下的/config目录;
- 类路径的根目录。
Spring Boot给我们提供了大量的配置属性,通过设置这些配置属性达到对当前应用的配置,我们可以在resources/application.properties文件中配置:
spring.main.banner-mode=off
spring.main.lazy-initialization=true
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration
server.port=1234
这些属性设置都会被读取到Environment,给应用的运行行为进行配置。Spring Boot提供的所有的属性配置参考:https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#common-application-properties
3.3.2.1 YAML文件配置
在现代的云计算的环境里,很多配置都是基于YAML文件的,尽管Spring Boot支持基于properties和YAML文件的配置,我们在本书将全部使用YAML文件进行配置。
YAML是JSON格式的超级,对层级配置有极大的便利,我们比较一下properties和YAML的区别:
application.properties
server.port=1234
server.address=192.168.31.199
application.yml
server:
port: 8888
address: 192.168.31.199
从上面可以看出,层级结构下,我们不用写两个server,如果层级越多的配置,我们的配置的层次会更清晰。注意“:”后有一个空格,这是YAML要求的格式。
当我们的配置层级性不是很强的时候,我们在YAML文件里仍可以类似于properties文件那样配置,如:
server.port: 8888
spring.main.banner-mode: off
现在我们新建一个application.yml文件,后面的演示都将在这里编写。
3.3.2.2 占位符
配置文件可以从Environment中读取已定义的配置。
app:
name: spring boot in depth
desc: Chapter:${app.name} is hard to learn
我们用下列代码检查结果:
@Bean
CommandLineRunner placeholderClr(@Value("${app.name}") String name,
@Value("${app.desc}") String desc){
return args -> {
System.out.println(name);
System.out.println(desc);
};
}
3.3.2.3 类型安全的配置属性
我们前面获得配置属性都是通过使用@Value,Spring Boot提供的了大量的配置属性,Spring Boot自己是怎么获得并使用的呢?如server.port=1234,Servlet容器启动时端口会被修改成了1234,这是怎么做到的?我们讲“Spring Boot的魔法”的时候讲了“加载自动配置”和“如何自动配置”,在自动配置时Spring Boot给我们开放了一个口子去修改默认的自动配置:一个强类型的Bean *Properties和外部配置文件中的内容进行映射绑定,自动配置通过使用*Properties来配置应用的行为,如我们前面用到的server.port的配置是在ServerProperties绑定的:
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties {
private Integer port;
private InetAddress address;
...
}
那这个绑定是怎么实现的呢?很显然是通过@ConfigurationProperties来实现的。这个注解的功能是由一个BeanPostProcessor(ConfigurationPropertiesBindingPostProcessor)提供的,前提是*Properties是一个Bean,我们可以通过两种方式让他成为Bean。
在Spring Boot 2.2之前:
- 在*Properties上注解@Component让它成为Bean,我们在配置使用的地方直接像常规Bean一样注入;
- 像ServerProperties并没有通过注解标记成Bean,而是在使用ServerProperties的地方通过@EnableConfigurationProperties({*Properties.class})来动态注册Bean。
在Spring Boot 2.2:
@SpringBootApplication注解组合了@ConfigurationPropertiesScan注解,它使用ConfigurationPropertiesScanRegistrar为我们动态的扫描注册标注了在我们入口类包及其下级包中注解了@ConfigurationProperties的类,并将其注册成Bean。我们也可以通过:
@SpringBootApplication
@ConfigurationPropertiesScan({ "com.some.other", "top.wisely" })
public class SpringBootInDepthApplication {}
来覆盖默认的扫描。
下面我们做一个简单的例子来演示类型安全的配置属性的使用:
- 属性类:
@Getter
@Setter
@ConfigurationProperties(prefix = "author")
public class AuthorProperties {
private String name = "wyf";
private Integer age = 35;
private String motherTongue;
private String secondLanguage;
private String graduatedUniversity;
private Integer graduationYear;
private Address address = new Address();
private List<Book> books = new ArrayList<>();
private Map<String, String> remarks = new HashMap<>();
@Getter
@Setter
public static class Address {
private String province;
private String city;
}
@Getter
@Setter
public static class Book{
private String name;
private Integer price;
}
}
当前例子分别有普通属性、对象、List、和Map,可作为有代表性的配置。
- 配置类
@Configuration
//已自动扫描注册AuthorProperties
//无需使用@EnableConfigurationProperties({AuthorProperties.class})
public class AuthorConfiguration {
private final AuthorProperties authorProperties;
public AuthorConfiguration(AuthorProperties authorProperties) {
this.authorProperties = authorProperties;
}
@Bean
public String strBean(){
String str = authorProperties.getName() + "/"
+ authorProperties.getAge() + "/"
+ authorProperties.getMotherTongue() + "/"
+ authorProperties.getSecondLanguage() + "/"
+ authorProperties.getGraduatedUniversity() + "/"
+ authorProperties.getGraduationYear() + "/"
+ authorProperties.getAddress().getProvince() + "/"
+ authorProperties.getAddress().getCity() + "/";
System.out.println(str);
authorProperties.getBooks().forEach(book -> {
System.out.println(book.getName());
System.out.println(book.getPrice());
});
authorProperties.getRemarks().forEach((key,value) ->{
System.out.println(key + ":" +value);
});
return str;
}
}
- 如何配置为了我们在application.yml输入配置的时候会自动提示,我们需要添加annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'到build.gradle,IntelliJ IDEA需要开启注解处理器:
此时Spring Boot为我们生成了build/classes/java/main/META-INF/spring-configuration-metadata.json文件,IDE会根据这个文件给我们自动提示:
我们的配置内容如下:
author:
name: foo
age: 40
mother-tongue: Chinese # 1 烤串式
second_language: English # 2 下划线式
graduationYear: 2006 # 3 驼峰式
GRADUATED_UNIVERSITY: WHUT # 4 大写字母
address:
province: Anhui
city: Hefei
books:
- name: book1
price: 89
- name: book2
price: 109
remarks:
hobby: reading
some: value
Spring Boot提供了一种叫“松散绑定”的技术,我们的属性可以是烤串式、下划线式、驼峰式或者是大写字母。
运行的结果如下(注意前面我们的全局延迟加载不能打开spring.main.lazy-initialization=false):
3.3.3 Profile
3.3.3.1 单文件Profile
我们可以在一个application.yml内定义多个Profile,每个Profile用“—”隔开,通过spring.profiles给每个Profile命名,每个Profile还可以通过spring.profiles.include来包含组合其他的Profile,最终通过spring.profiles.active来指定激活的Profile,如:
spring:
profiles:
active: prod #指定激活的Profile是prod
---
spring.profiles: prod # Profile的名为prod
spring.profiles.include: # 组合prod-port和prod-lazy两个Profile
- prod-port
- prod-lazy
---
spring:
profiles: prod-port # Profile的名为prod-port
server:
port: 8888
---
spring:
profiles: dev-port # Profile的名为dev-port
server:
port: 8080
---
spring:
profiles: prod-lazy # Profile的名为prod-lazy
main:
lazy-initialization: true
启动时会执行端口号是8888、全局延迟加载的设置。我们也可以通过设置多个Profile获得相同的结果:
spring:
profiles:
active:
- prod-port
- pord-lazy
3.3.3.2 多文件Profile
我们还可以将上面的Profile拆到多个文件名称,即application-{profile}.yml,上面的可以拆成:
- application-prod.yml
spring.profiles.include:
- prod-port
- prod-lazy
- application-prod-port.yml
server:
port: 8888
- application-prod-lazy.yml
spring:
main:
lazy-initialization: true
- application-dev-port.yml
server:
port: 8080
我们最终同样需要在application.yml中指定要激活的Profile:
spring:
profiles:
active: prod
3.3.4 EnvironmentPostProcessor
我们在上一章对Bean行为的定制可以用BeanPostProcessor,对配置元数据的定制使用BeanFactoryPostProcessor,Spring Boot给我们提供了对Environment和SpringApplication进行定制的EnvironmentPostProcessor。
我们自定义一个EnvironmentPostProcessor实现类:
public class MyEnvironmentPostProcessor implements EnvironmentPostProcessor { // 1
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { // 2
Map<String, Object> map = new HashMap<>();
map.put("key1","value1");
map.put("key2","value2");
PropertySource propertySource = new MapPropertySource("map",map); //3
environment.getPropertySources().addLast(propertySource); //4
.setBannerMode(Banner.Mode.OFF); //5
}
}
- 实现EnvironmentPostProcessor接口;
- 重载postProcessEnvironment,它的入参让我们可以对Environment和SpringApplication进行设置;
- 新建一个PropertySource类型为MapPropertySource,将map的值设置给PropertySource;
- 将propertySource添加到Environment中;
- 同样在此处可以设置SpringApplication。
我们同样可以通过在本应用的META-INF/spring.factories启用该处理:
org.springframework.boot.env.EnvironmentPostProcessor=\
top.wisely.springbootindepth.processor.MyEnvironmentPostProcessor
我们再校验运行结果:
@Bean
CommandLineRunner environmentPostProcessorClr(@Value("${key1}") String value1,
@Value("${key2}") String value2){
return args -> {
System.out.println(value1);
System.out.println(value2);
};
}