前言
之前我们说,SpringBoot不需要任何配置就可以成功启动Tomcat服务器,那这是否说明使用SpringBoot做Web项目就不需要任何配置了呢?
答案显然是否定了,确实,如我们之前所分析的,SpringBoot默认会帮我们加载很多配置类,定了很多默认的参数,但是有些值SpringBoot是没有办法定义的或者提前预知的,怎么说呢… SpringBoot就好比你的 “私人管家” 帮你管理需要重复做的一些事儿,比如DispatcherServlet前端控制的创建,内部资源访问视图解析器的创建等等,但是诸如数据源的配置,一些插件的安装都是需要 “主人” 亲历亲为的,就好比说数据库的连接,咱们的 “私人管家” 肯定是不知道账号和密码的。因此今天我们要学习的内容就是如何写SpringBoot的配置文件。
application.properties配置文件
SpringBoot一共有两种全局的配置文件,配置文件名是固定的,分别是application.properties以及application.yml,咱们就先介绍application.properties,其实application.properties本质上和.properties文件没啥区别,但是被 “包装” 了一下,因此也很容易上手。
第一步:创建Student
咱们先创建一个Student,然后在Student上添加一个注解@ConfigurationProperties(prefix="student")
,他代表的意思就是可以使用使用student前缀去IoC容器里面读取配置文件,嗯… 这么多大家可能会很懵,不过不要紧,继续往下看应该就知道答案啦
大家在创建@ConfigurationProperties(prefix="student")
注解时可能会碰到黄色警告,内容如下
说是需要在pom.xml中创建spring-boot-configuration-processor
,这有啥用呢?
当我们添加上上面的配置之后,才能在SpringBoot的配置文件中出现自动提示功能,后面在写配置文件的时候大家就能感受得到,大家看,就类似于下面这种,可以自动的提示你要配置的类的属性或者字段。
第二步:配置application.properties
接着往配置文件中通过以下方式给我们的student对象赋值
大家可以看到除了map的使用方式和对象的赋值方式差不多,其他的像数组,list以及set的赋值形式都是一样的,非常容易记忆。
第三步:单元测试
配置完成之后,咱们来测试下,看能不能获取到值
package com.marco;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import com.marco.domain.Student;
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringbootHelloApplicationTests {
@Autowired
//这里的变量名必须使用类名的首字母小写的形式,否则会抛出异常
private Student student;
@Test
public void contextLoads() {
System.out.println(student);
}
}
吓我一跳… 突然蹦出来一个bug,异常的内容是这样的nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.marco.domain.Student' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
,意思是说我们没有在IoC容器中找到Student类?
产生问题的根本原因就是忘了加@Component
注解,所以一定要注意添加@Component
之后,我们的类才会在IoC容器中被创建!
加上注解之后我们再来试试看~结果出来了对吧!
application.yml配置文件
知道了application.properties的用法,application.yml其实也很简单,其实配置的内容和方式都差不多,只是说application.yml对比application.properties支持配置文件中写中文(application.properties中写中文会转码)
且application.properties的配置结构和application.properties有点不一样,那么我们来使用中文将之前的application.properties的配置再来配一变吧~
student:
id: 1
name: marco
age: ${random.int(10,100)}
birth:
time: 66666666
hobby:
- 唱
- 跳
- rap
- 打篮球
map:
address: 武汉
job: 程序猿
list:
- sunnie
- jack
- rose
其实要说起来,这两种配置结构实际上就是我们Eclipse左边目录的结构,一共有两种 Flat 和 Hierarchical,还没有用过这个功能的朋友可以去试试。
我们再运行看看… 咦?怎么数据没有变化啊?
大家注意看我这里有两个配置文件,配置文件当然是有优先级别的,如果两个配置文件都存在的情况下,SpringBoot会默认去加载application.properties中文件的配置。因此要想使用yml,必须把application.properties删掉!
删掉之后咱们再来测试一下… 是不是就可以了?
关于配置文件加载的优先级底层原理分析
想必好奇心强的朋友也很想知道,SpringBoot中配置文件加载的优先级的原理到底是什么对吧?
那么文章的最后,咱们来略加分析一下吧!首先我们直接搜索SpringBoot文件配置类的监听器ConfigFileApplicationListener(这里我就不顺着的带大家去找这个类啦)
接着找到onApplicationEvent(ApplicationEvent event)
方法,在里边我们可以找到这么个方法onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event)
该方法在SpringBoot准备运行环境的时候就会被触发,因此我们的配置文件的加载肯定也在里面!
因为ConfigFileApplicationListener继承了EnvironmentPostProcesser,因此会加入postProcessors后置处理器中,然后循环所有的EnvironmentPostProcessor类型调用postProcessorEnvironmet方法加载配置文件。
private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();//加载所有后置处理器(运行环境所需)
postProcessors.add(this);//将当前的ConfigFileApplicationListener也加入到处理器中
AnnotationAwareOrderComparator.sort(postProcessors);
//循环所有的EnvironmentPostProcessor类型调用postProcessorEnvironmet方法加载配置文件
for (EnvironmentPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
}
}
得知这条线索之后,咱们再去找postProcessEnvironment()
的实现方法,因为我们现在的processor是ConfigFileApplicationListener,所以咱们看看ConfigFileApplicationListener中重写的这个方法
找到之后,我们发现postProcessEnvironment()
实质上就是调用了addPropertySources(environment, application.getResourceLoader())
,接着往下走
这里我们主要看new Loader(environment, resourceLoader).load()
方法,这个load()方法会初始化并加载默认的SpringBoot配置文件,然后再往里边添加我们我们自己定义的profile文件,因此我们的关注点就是while循环里边的load()方法。
好啦,终于咱们 “西天取经” 也快到头了,从下面的代码可以看出load方法就是通过getSearchLocations
循环获取配置文件的位置。并加载
通过Debug我们发现getSearchLocations
就是获取了咱们项目内部配置文件扫描的所有位置,然后我们再回过头看上面的代码的意思就是获取到所有的扫描路径之后搜索这些路径下的文件
以下目录的优先级从上到下依次降低
file:./config/ (当前项目路径config目录下)file:./ (当前项目路径下)
classpath:/config/ (类路径config目录下)
classpath:/ (类路径config下)
load方法下的getSearchNames()
则是获取我们扫描的的文件中所有文件的名称,我们接着Debug…
欸?发现扫描到我们的application文件了
我们再来仔细分析一下里边的load(String location, String name, Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer)
方法(PS:这load方法跟俄罗斯套娃似的,一层层套个没完…)
private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory,
DocumentConsumer consumer) {
//name默认为:application,所以这个if语句不会走
if (!StringUtils.hasText(name)) {
for (PropertySourceLoader loader : this.propertySourceLoaders) {
if (canLoadFileExtension(loader, location)) {
load(loader, location, profile, filterFactory.getDocumentFilter(profile), consumer);
return;
}
}
}
Set<String> processed = new HashSet<>();
//加载所有的sourceLoader
for (PropertySourceLoader loader : this.propertySourceLoaders) {
for (String fileExtension : loader.getFileExtensions()) {
//location: [./config/, file:./, classpath:/config/, classpath:/]
if (processed.add(fileExtension)) {
//相当于对(location, fileExtension, profile)做笛卡尔积,
//遍历每一种可能,然后加载
//加载文件的细节在loadForFileExtension中完成
loadForFileExtension(loader, location + name, "." + fileExtension, profile, filterFactory,
consumer);
}
}
}
}
通过Debug我们发现PropertySourceLoader一共加载了两个SourceLoader,分别是properties文件和yml文件的SourceLoader
其实大家可以去翻阅上面这两个ResourceLoader可以找到它两的getFileExtensions()
方法,就知道它们会去加载哪些后缀的配置文件了。
好,现在我们已经知道了,SpringBoot除了会加载默认的配置文件之外,还会在启动的时候加载我们的properties文件和yml文件,生成相对应的SourceLoader类,那我们再回到之前在ConfigFileApplicationListener中分析的load()方法。现在已经得值SourceLoader都加载完毕,接下来就是分析addLoadedPropertySources()
这个方法了。
这里我们仔细的分析一下addLoadedPropertySources()
方法,其实就是对我们的文件的优先级进行排序,因为此次实验没有涉及到SpringBoot开发模式转换,因此并没有额外的去配置application-dev.yml文件(开发使用模式),如果配置了pplication-dev.xml文件,大家会发现application-dev.xml会先进入循环,application.yml会后进入循环,优先级较application-dev.xml更低。
private void addLoadedPropertySources() {
//destination: 进入ConfigFileApplicationListener监听器前已有的配置
//内容为[MapPropertySource {name='Inlined Test Properties'},
//PropertiesPropertySource {name='systemProperties'},
//OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'},
//RandomValuePropertySource {name='random'}]
MutablePropertySources destination = this.environment.getPropertySources();
String lastAdded = null;
//loaded: 加载通过扫描文件获取的文件源
//[[OriginTrackedMapPropertySource {name='applicationConfig: [classpath:/application.yml]'}]]
List<MutablePropertySources> loaded = new ArrayList<>(this.loaded.values());
//文件倒序处理,因为此时我们这里就一个application.yml,因此用不着倒序
Collections.reverse(loaded);
//处理 application.yml,注意,先进入的优先级更高,反之后进入的优先级低
for (MutablePropertySources sources : loaded) {
for (PropertySource<?> source : sources) {
if (lastAdded == null) {
if (destination.contains(DEFAULT_PROPERTIES)) {
destination.addBefore(DEFAULT_PROPERTIES, source);
}
else {
destination.addLast(source);
}
}
else {
destination.addAfter(lastAdded, source);
}
lastAdded = source.getName();
}
}
}
执行完上述的代码之后,我们在EnvironmentSpring环境因子中可以找到文件源的List,优先级从上到下依次降低,因此这就是为什么SpringBoot在启动的时候会去加载application.properties和application.yml,以及为什么前者比后者的优先级要高的原因啦~