本章讲解Spring Boot的配置文件,即我们创建项目时在resources目录下生成的application.properties。其实这个配置文件不仅仅只能放在resources目录下,也不仅仅只能是properties文件。
1 配置文件位置
1 resources目录
2 resources/config目录
3 项目根目录
4 项目根目录下config目录
假如我们四个目录下都有配置文件默认是取哪一个呢?下面我们演示一下,现在将四个目录都放拷贝一份文件。结构如下:
文件中都配置一个属性,值为目录,比如resources目录下。
测试代码如下:
@SpringBootTest
class SpringBootDemoApplicationTests {
@Value("${demo.location}")
private String location;
@Test
void contextLoads() {
System.out.println(location);
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
}
运行测试方法,可见最先读取的是项目根目录下的config目录。
然后把项目根目录下config目录下的文件删除,再次运行。一一测试后,配置文件位置的优先级为:
项目根目录下config目录 > 项目根目录 > resources/config > resources
2 配置文件类型
Spring Boot默认支持3种配置文件:
1.properties
2.yaml
3.xml(不推荐)
上文中已经演示了properties,这里就不再次演示,下面我们看看yaml也可以是yml:
注意:yaml文件的缩进只能使用空格,且冒号后面也必须有空格。
xml配置:
如果目录下同时存在yaml、yml、properties、xml四种类型的文件,如:
优先级为: properties > xml > yml > yaml
由上面可以看出xml配置起来比较麻烦,所以基本没有使用。对于properties和yaml各有优缺点把,看自己喜好。
3 配置详解
3.1 多环境配置
在开发当中,通常一个项目都至少会经过开发、测试、生产三个环境,不同的环境数据库,服务器端口等配置可能都不一样,如果在切环境的时再来一一修改,比较麻烦,也容易出错,所以一般是一套环境对于一个配置文件。
3.1.1 多文件配置
Spring Boot也给我们提供了这样的功能。针对多环境场景下,我们会给每个环境创建一个配置文件application-${profile}.yaml。其中,${profile} 为环境名,对应到 Spring Boot 项目生效的Profile。
创建3个文件application.yaml、application-dev.yaml、application-prod.yaml,如下:
application.yaml
spring:
profiles:
active: prod #生效的环境
application-dev.yaml
server:
port: 8081
demo:
location: yaml配置 dev
filename: application dev
application-prod.yaml
server:
port: 8082
demo:
location: yaml配置 prod
filename: application prod
如上在application.yaml 中配置生效的环境为prod,所以加载application-prod.yaml文件,所以服务端口为8082。
激活指定环境配置:
- 直接在application.yml的配置文件中使用 spring.profiles.active=dev|prod
- 设置虚拟机参数 -Dspring.profiles.active=dev|prod
- 设置命令行参数 --spring.profiles.active=dev|prod
- 也可直接启用多个文件spring.profiles.active=dev,prod,用逗号分隔开
3.1.2 多模块文件
上面是将不同的配置放到了不同的配置文件中,Spring Boot还支持在同一个配置文件中分模块配置,然后指定启用的模块。
spring:
profiles:
active: prod #启用模块
---
spring:
profiles: dev #指定模块名称
demo:
location: yaml配置 dev
filename: application dev
server:
port: 8084
---
spring:
profiles: prod #指定模块名称
demo:
location: yaml配置 prod
filename: application prod
server:
port: 8085
可以通过三个短横线为分隔,将其分为不同模块。由上面注释的方式指定模块名称,和指定启用模块。也可同时启用两个模块的文件用逗号分隔。
spring:
profiles:
active: prod,dev #启用模块
3.2 配置文件名称
上文提到过Spring Boot会在启动时默认加载resources目录下名为application的文件(如:application.properties、application.yaml),当然我们也可自定义文件名。
启动类设置属性:
System.setProperty(ConfigFileApplicationListener.CONFIG_NAME_PROPERTY, “demo,application”);
@SpringBootApplication
public class SpringBootDemoApplication {
public static void main(String[] args) {
// 设置加载的配置文件名称
System.setProperty(ConfigFileApplicationListener.CONFIG_NAME_PROPERTY, "demo,application");
SpringApplication.run(SpringBootDemoApplication.class, args);
}
}
以上配置将同时加载demo.yaml和appliation.yaml。
3.3 配置随机值
Spring Boot提供随机值配置功能,通过RandomValuePropertySource 实现。
在application.yaml中配置如下:
demo:
randomInt: ${
random.int} #随机int类型整数
randomLong: ${
random.long} #随机Long类型整数
注入属性配置类
@Component
public class SpringbootConfig {
@Value("${demo.randomInt}")
private int randomInt;
@Value("${demo.randomLong}")
private long randomLong;
public int getRandomInt() {
return randomInt;
}
public void setRandomInt(int randomInt) {
this.randomInt = randomInt;
}
public long getRandomLong() {
return randomLong;
}
public void setRandomLong(long randomLong) {
this.randomLong = randomLong;
}
}
@RestController
public class ConfigController {
@Autowired
private SpringbootConfig springbootConfig;
@RequestMapping("/config")
public String configName() {
return "随机int:"+springbootConfig.getRandomInt()+",随机long:"+springbootConfig.getRandomLong();
}
}
结果如下:
RandomValuePropertySource提供了如下随机值:
${
random.int} #随机int型
${
random.long} #随机long型
${
random.int(m)} #随机小于m的int型
${
random.int[n,m]} #随机大于n小于m的int型
${
random.uuid} #uuid字符串
${
random.value} #随机字符串
其中随机字符串不一定是value,只要不等于其它物种类型,就属于随机字符串。
3.3 加载自定义properties
在resources目录下新建tes.properties文件,内容如下:
person.name=tom
person.age=19
person.address=中国
PersonConfig
@Component
@PropertySource("classpath:test.properties")
public class PersonConfig {
@Value("${person.name}")
private String name;
@Value("${person.age}")
private int age;
@Value("${person.address}")
private String address;
@Override
public String toString() {
return "PersonConfig{" +
"name='" + name + '\'' +
", age=" + age +
", address='" + address + '\'' +
'}';
}
}
@RestController
public class ConfigController {
@Autowired
private PersonConfig personConfig;
@RequestMapping("/person")
public String getPerson() {
return personConfig.toString();
}
}
结果如下:
这种@value方式如果属性比较多,配置起来就显得很麻烦,这时候可以采用@EnableConfigurationProperties加@ConfigurationProperties注解
启动类配置如下:
@SpringBootApplication
@EnableConfigurationProperties(PersonConfig.class)
@PropertySource("classpath:test.properties")
public class SpringBootDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootDemoApplication.class, args);
}
}
PersonConfig
@ConfigurationProperties(prefix = "person")
public class PersonConfig {
// @Value("${person.name}")
private String name;
// @Value("${person.age}")
private int age;
// @Value("${person.address}")
private String address;
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public void setAddress(String address) {
this.address = address;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public String getAddress() {
return address;
}
@Override
public String toString() {
return "PersonConfig{" +
"name='" + name + '\'' +
", age=" + age +
", address='" + address + '\'' +
'}';
}
}
这种方式就避免了每个属性都写一个@value。
3.4 加载自定义yaml
@PropertySource默认是不支持yaml文件的,因为其加载properties文件是在PropertySourceFactory接口种完成的,而该接口只有一个默认实现类DefaultPropertySourceFactory,所以要加载yaml文件需要们自定义实现PropertySourceFactory接口。
PropertySourceFactory的作用是将创建的PropertiesSource暴露到spring 的environment种,那如何读取yaml文件呢?spring提供了两个类加载yaml文档,YamlPropertiesFactoryBean和YamlMapFactoryBean。YamlPropertiesFactoryBean将yaml加载为properties,YamlMapFactoryBean将yaml加载为map。
示例代码如下:
public class MyPropertySourceFactory extends DefaultPropertySourceFactory {
@Override
public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
String resourceName = name == null?resource.getResource().getFilename():name;
// 判断是否yaml文件
if (resource.getResource().getFilename().endsWith("yaml")) {
// 加载yaml文档
YamlPropertiesFactoryBean yamlPropertiesFactoryBean = new YamlPropertiesFactoryBean();
yamlPropertiesFactoryBean.setResources(resource.getResource());
// 初始化
yamlPropertiesFactoryBean.afterPropertiesSet();
//加载为properties
Properties properties = yamlPropertiesFactoryBean.getObject();
//封装成 PropertiesPropertySource 返回
return new PropertiesPropertySource(resourceName, properties);
}
// properties文件
return super.createPropertySource(resourceName, resource);
}
}
在使用@PropertySource时,指定自定义的PropertySourceFactory实现。
@SpringBootApplication
@PropertySource(value = "classpath:yamlTest.yaml",factory = MyPropertySourceFactory.class)
public class SpringBootDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootDemoApplication.class, args);
}
}
注入文件属性:
@Component
public class PersonConfig {
@Value("${person.name}")
private String name;
@Value("${person.age}")
private int age;
@Value("${person.address}")
private String address;
@Override
public String toString() {
return "PersonConfig{" +
"name='" + name + '\'' +
", age=" + age +
", address='" + address + '\'' +
'}';
}
}
4 配置加载过程源码分析
Spring Boot加载application配置文件是由ConfigFileApplicationListener来完成的。
由上图可以看出ConfigFileApplicationListener既是一个应用监听器(ApplicationListener),也是一个环境的后置处理器(EnvironmentPostProcessor)。
ConfigFileApplicationListener部分属性如下:
private static final String DEFAULT_PROPERTIES = "defaultProperties";
// 默认的配置文件路径
private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/*/,file:./config/";
// 默认的配置文件名称
private static final String DEFAULT_NAMES = "application";
private static final Set<String> NO_SEARCH_NAMES = Collections.singleton((Object)null);
private static final Bindable<String[]> STRING_ARRAY = Bindable.of(String[].class);
private static final Bindable<List<String>> STRING_LIST = Bindable.listOf(String.class);
private static final Set<String> LOAD_FILTERED_PROPERTY;
// 配置生效配置文件的属性名称 如spring.profiles.active=dev则表示application-dev.properties 参考前文3.1
public static final String ACTIVE_PROFILES_PROPERTY = "spring.profiles.active";
public static final String INCLUDE_PROFILES_PROPERTY = "spring.profiles.include";
// 自定义配置文件名称的属性名称,如设置spring.config.name=dubbo,则加载dubbo.properties
public static final String CONFIG_NAME_PROPERTY = "spring.config.name";
// 自定义配置文件位置,如设置spring.config.location=classpath:/config/app,则可以将配置文件放如/resources/config/app目录下
public static final String CONFIG_LOCATION_PROPERTY = "spring.config.location";
public static final String CONFIG_ADDITIONAL_LOCATION_PROPERTY = "spring.config.additional-location";
public static final int DEFAULT_ORDER = -2147483638;
private final DeferredLog logger = new DeferredLog();
private static final Resource[] EMPTY_RESOURCES;
private static final Comparator<File> FILE_COMPARATOR;
private String searchLocations;
private String names;
private int order = -2147483638;
首先这个类是在创建SpringApplication实例时,通过spring的spi机制加载到SpringApplication中的。
我们知道Spring Boot的启动类就是SpringApplication调用其静态run方法,静态run方法中创建SpringApplication实例调用成员run方法。
SpringApplication构造方法:
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
this.webApplicationType = WebApplicationType.deduceFromClasspath();
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 从META-INF/spring.factories中加载所有ApplicationListener,并添加到SpringApplication的listeners集合中。
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
上面代码中调用getSpringFactoriesInstances(ApplicationListener.class)方法加载了ConfigFileApplicationListener
META-INF/spring.factories 配置如下:
SpringApplication的run方法
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 准备环境,加载Properties、yaml文件
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] {
ConfigurableApplicationContext.class }, context);
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}
该方法里调用了prepareEnvironment(listeners, applicationArguments)方法准备环境,其中调用了 listeners.environmentPrepared(environment)方法,发布ApplicationEnvironmentPreparedEvent事件,触发了ConfigFileApplicationListener的onApplicationEvent(ApplicationEvent event)方法。
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationEnvironmentPreparedEvent) {
//加载配置文件
onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
}
if (event instanceof ApplicationPreparedEvent) {
onApplicationPreparedEvent(event);
}
}
事件类型为onApplicationEnvironmentPreparedEvent进入onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
将ConfigFileApplicationListener添加到后置处理器集合
postProcessors.add(this);
AnnotationAwareOrderComparator.sort(postProcessors);
for (EnvironmentPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
}
}
调用了环境的后置处理器方法postProcessEnvironment,上文说过ConfigFileApplicationListener自身也是一个环境的后置处理器,进入器postProcessEnvironment方法,该方法什么也没做,调用了自身addPropertySources(environment, application.getResourceLoader())方法。
protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
// 向环境中添加RandomValuePropertySource,支持获取随机数,参考上文3.3
RandomValuePropertySource.addToEnvironment(environment);
// 创建Loader实例,利用spring的spi机制加载了读取配置文件的两个Loader,PropertiesPropertySourceLoader、YamlPropertySourceLoader
new Loader(environment, resourceLoader).load();
}
该方法中添加了加载随机数的RandomValuePropertySource,通过该SPI
加载了读取配置文件的PropertiesPropertySourceLoader、YamlPropertySourceLoader,最终调用这两个Loader的load方法读取配置文件。
PropertiesPropertySourceLoader
YamlPropertySourceLoader
其中的getFileExtensions方法返回的是支持的配置文件类型,所以Spring Boot默认支持四种类型的配置文件,参考上文2。
最后调用了PropertiesPropertySourceLoader和YamlPropertySourceLoader的load(String name, Resource resource)方法读取了配置文件。