Springboot漫游日志(22)
上次看到【ConfigFileApplicationListener】作为一个【EnvironmentPostProcessor】
第504行的【load】方法。
ApplicationEnvironmentPreparedEvent 事件的listener(三)
- FileEncodingApplicationListener
- AnsiOutputApplicationListener
- ConfigFileApplicationListener
- ClasspathLoggingApplicationListener
- LoggingApplicationListener
ConfigFileApplicationListener
ConfigFileApplicationListener
private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter,
DocumentConsumer consumer) {
Resource[] resources = getResources(location);
for (Resource resource : resources) {
try {
if (resource == null || !resource.exists()) {
if (this.logger.isTraceEnabled()) {
StringBuilder description = getDescription("Skipped missing config ", location, resource,
profile);
this.logger.trace(description);
}
continue;
}
if (!StringUtils.hasText(StringUtils.getFilenameExtension(resource.getFilename()))) {
if (this.logger.isTraceEnabled()) {
StringBuilder description = getDescription("Skipped empty config extension ", location,
resource, profile);
this.logger.trace(description);
}
continue;
}
if (resource.isFile() && hasHiddenPathElement(resource)) {
if (this.logger.isTraceEnabled()) {
StringBuilder description = getDescription("Skipped location with hidden path element ",
location, resource, profile);
this.logger.trace(description);
}
continue;
}
String name = "applicationConfig: [" + getLocationName(location, resource) + "]";
List<Document> documents = loadDocuments(loader, name, resource);
if (CollectionUtils.isEmpty(documents)) {
if (this.logger.isTraceEnabled()) {
StringBuilder description = getDescription("Skipped unloaded config ", location, resource,
profile);
this.logger.trace(description);
}
continue;
}
List<Document> loaded = new ArrayList<>();
for (Document document : documents) {
if (filter.match(document)) {
addActiveProfiles(document.getActiveProfiles());
addIncludedProfiles(document.getIncludeProfiles());
loaded.add(document);
}
}
Collections.reverse(loaded);
if (!loaded.isEmpty()) {
loaded.forEach((document) -> consumer.accept(profile, document));
if (this.logger.isDebugEnabled()) {
StringBuilder description = getDescription("Loaded config file ", location, resource,
profile);
this.logger.debug(description);
}
}
}
catch (Exception ex) {
StringBuilder description = getDescription("Failed to load property source from ", location,
resource, profile);
throw new IllegalStateException(description.toString(), ex);
}
}
}
再贴一波,location的可能取值
- file:./config/
- file:./config/*/
- file:./
- classpath:/config/
- classpath:/
name的取值为【application】
一步一看。
private Resource[] getResources(String location) {
try {
if (location.contains("*")) {
return getResourcesFromPatternLocation(location);
}
return new Resource[] { this.resourceLoader.getResource(location) };
}
catch (Exception ex) {
return EMPTY_RESOURCES;
}
}
如果路径中含有星星,走【getResourcesFromPatternLocation】
显然只有【file:./config/*/】是含有星星的
看一看。
private Resource[] getResourcesFromPatternLocation(String location) throws IOException {
String directoryPath = location.substring(0, location.indexOf("*/"));
Resource resource = this.resourceLoader.getResource(directoryPath);
File[] files = resource.getFile().listFiles(File::isDirectory);
if (files != null) {
String fileName = location.substring(location.lastIndexOf("/") + 1);
Arrays.sort(files, FILE_COMPARATOR);
return Arrays.stream(files).map((file) -> file.listFiles((dir, name) -> name.equals(fileName)))
.filter(Objects::nonNull).flatMap((Function<File[], Stream<File>>) Arrays::stream)
.map(FileSystemResource::new).toArray(Resource[]::new);
}
return EMPTY_RESOURCES;
}
先把*/之前的截取了。
【file:./config/*/】就截取成了【file:./config/】
然后找到这个路径下的所有文件,只要文件夹,其他不要。
如果一个都没有,那就返回【EMPTY_RESOURCES】
private static final Resource[] EMPTY_RESOURCES = {};
这里的location是已经拼接后的location
比如:file:./config/*/application-default.yml
所以截取最后一个斜杠之后的内容,就是拿到了【application-default.yml】
然后把文件夹排序,排序规则比较绝对路径,这个是啥效果,先不看。
接着遍历每个文件夹,找到里面和【application-default.yml】名字一样的文件
一维数组变二维数组
然后过滤掉不是null的文件数组
然后把二维数组整合成一维数组
然后对一维数组的每个元素调用【FileSystemResource】的构造方法,转换成Resource
然后得到一个Resource数组,返回。
总结就是,找到config文件夹下子文件夹里面和拼接的配置文件名字一样的文件
读取到内存了。
这时候应该还没有把配置内容解析。
回过头,看上一层方法的返回值
如果没有包含星星的路径
return new Resource[] { this.resourceLoader.getResource(location) };
就看一下默认的resourceLoader【DefaultResourceLoader】
因为Loader构造方法里面已经赋值了
Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
this.environment = environment;
this.placeholdersResolver = new PropertySourcesPlaceholdersResolver(this.environment);
this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader(null);
this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
getClass().getClassLoader());
}
往上溯源,拿到的是【application.getResourceLoader()】
打了个断点看了一下,这是一个null
/**
* Create a new DefaultResourceLoader.
* @param classLoader the ClassLoader to load class path resources with, or {@code null}
* for using the thread context class loader at the time of actual resource access
*/
public DefaultResourceLoader(@Nullable ClassLoader classLoader) {
this.classLoader = classLoader;
}
接着看加载location
@Override
public Resource getResource(String location) {
Assert.notNull(location, "Location must not be null");
for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
Resource resource = protocolResolver.resolve(location, this);
if (resource != null) {
return resource;
}
}
if (location.startsWith("/")) {
return getResourceByPath(location);
}
else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
}
else {
try {
// Try to parse the location as a URL...
URL url = new URL(location);
return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
}
catch (MalformedURLException ex) {
// No URL -> resolve as resource path.
return getResourceByPath(location);
}
}
}
第一步遍历的【getProtocolResolvers()】就是空集合了
接着看
如果路径以斜杠开头
配置中都木有,就不看了
String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;
public static final String CLASSPATH_URL_PREFIX = "classpath:";
就是以classpath开头
返回【ClassPathResource】
如果是file,就返回【FileUrlResource】,不是就返回【UrlResource】
一路返回到【ConfigFileApplicationListener】506行。
遍历拿到的【resources】
根据不同的情况,打印不同的日志,都是trace级别,可以把spring的日志级别设置为trace就可以看到这些日志。
然后到533行,到541行。
正菜来了。
private List<Document> loadDocuments(PropertySourceLoader loader, String name, Resource resource)
throws IOException {
DocumentsCacheKey cacheKey = new DocumentsCacheKey(loader, resource);
List<Document> documents = this.loadDocumentsCache.get(cacheKey);
if (documents == null) {
List<PropertySource<?>> loaded = loader.load(name, resource);
documents = asDocuments(loaded);
this.loadDocumentsCache.put(cacheKey, documents);
}
return documents;
}
从缓存中获取documents,当然是没有的。
所以进入if里面。
loader.load()的loader类型是【PropertySourceLoader】
这是一个接口,是之前从spring.properties里面加载的。
总共有两个实现类。
- PropertiesPropertySourceLoader
- YamlPropertySourceLoader
他们有支持的加载类型。
【PropertiesPropertySourceLoader】支持【properties】【xml】
【YamlPropertySourceLoader】支持【yml】【yaml】
至于里面的load方法就不看了,看了头皮发麻。
只贴一下返回值
public class PropertiesPropertySourceLoader implements PropertySourceLoader {
private static final String XML_FILE_EXTENSION = ".xml";
@Override
public String[] getFileExtensions() {
return new String[] { "properties", "xml" };
}
@Override
public List<PropertySource<?>> load(String name, Resource resource) throws IOException {
Map<String, ?> properties = loadProperties(resource);
if (properties.isEmpty()) {
return Collections.emptyList();
}
return Collections
.singletonList(new OriginTrackedMapPropertySource(name, Collections.unmodifiableMap(properties), true));
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private Map<String, ?> loadProperties(Resource resource) throws IOException {
String filename = resource.getFilename();
if (filename != null && filename.endsWith(XML_FILE_EXTENSION)) {
return (Map) PropertiesLoaderUtils.loadProperties(resource);
}
return new OriginTrackedPropertiesLoader(resource).load();
}
}
public class YamlPropertySourceLoader implements PropertySourceLoader {
@Override
public String[] getFileExtensions() {
return new String[] { "yml", "yaml" };
}
@Override
public List<PropertySource<?>> load(String name, Resource resource) throws IOException {
if (!ClassUtils.isPresent("org.yaml.snakeyaml.Yaml", null)) {
throw new IllegalStateException(
"Attempted to load " + name + " but snakeyaml was not found on the classpath");
}
List<Map<String, Object>> loaded = new OriginTrackedYamlLoader(resource).load();
if (loaded.isEmpty()) {
return Collections.emptyList();
}
List<PropertySource<?>> propertySources = new ArrayList<>(loaded.size());
for (int i = 0; i < loaded.size(); i++) {
String documentNumber = (loaded.size() != 1) ? " (document #" + i + ")" : "";
propertySources.add(new OriginTrackedMapPropertySource(name + documentNumber,
Collections.unmodifiableMap(loaded.get(i)), true));
}
return propertySources;
}
}
返回值都是【OriginTrackedMapPropertySource】
到这里,application.properties里面的内容就被加载到spring里面了,当然了,是被某一次循环加入的。
回到【ConfigFileApplicationListener】543行。
到550行。
List<Document> loaded = new ArrayList<>();
for (Document document : documents) {
if (filter.match(document)) {
addActiveProfiles(document.getActiveProfiles());
addIncludedProfiles(document.getIncludeProfiles());
loaded.add(document);
}
}
过滤掉已经加载的文件。
然后给属性赋值,是【ConfigFileApplicationListener】里面的属性。
void addActiveProfiles(Set<Profile> profiles) {
if (profiles.isEmpty()) {
return;
}
if (this.activatedProfiles) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Profiles already activated, '" + profiles + "' will not be applied");
}
return;
}
this.profiles.addAll(profiles);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Activated activeProfiles " + StringUtils.collectionToCommaDelimitedString(profiles));
}
this.activatedProfiles = true;
removeUnprocessedDefaultProfiles();
}
private void addIncludedProfiles(Set<Profile> includeProfiles) {
LinkedList<Profile> existingProfiles = new LinkedList<>(this.profiles);
this.profiles.clear();
this.profiles.addAll(includeProfiles);
this.profiles.removeAll(this.processedProfiles);
this.profiles.addAll(existingProfiles);
}
接着往下看
Collections.reverse(loaded);
if (!loaded.isEmpty()) {
loaded.forEach((document) -> consumer.accept(profile, document));
if (this.logger.isDebugEnabled()) {
StringBuilder description = getDescription("Loaded config file ", location, resource,
profile);
this.logger.debug(description);
}
}
这里都是函数回调。
遍历loaded集合,然后把document加入到某个地方,看一下consumer是个啥。
他是从【ConfigFileApplicationListener】350行的load方法传入的。
load(profile, this::getPositiveProfileFilter, addToLoaded(MutablePropertySources::addLast, false));
第三个参数就是,之前已经看过了。
private DocumentConsumer addToLoaded(BiConsumer<MutablePropertySources, PropertySource<?>> addMethod,
boolean checkForExisting) {
return (profile, document) -> {
if (checkForExisting) {
for (MutablePropertySources merged : this.loaded.values()) {
if (merged.contains(document.getPropertySource().getName())) {
return;
}
}
}
MutablePropertySources merged = this.loaded.computeIfAbsent(profile,
(k) -> new MutablePropertySources());
addMethod.accept(merged, document.getPropertySource());
};
}
就是return返回的匿名方法,第一个参数是profile,第二个是document。
checkForExisting是false,就直接看下面的。
从loaded里面找到以profile为键的值,找不到就新new一个【MutablePropertySources】
【addMethod】是【MutablePropertySources::addLast】,也是一个回调方法。
再贴一波代码。
/**
* Add the given property source object with lowest precedence.
*/
public void addLast(PropertySource<?> propertySource) {
synchronized (this.propertySourceList) {
removeIfPresent(propertySource);
this.propertySourceList.add(propertySource);
}
}
这里document被加入了【propertySourceList】
这里的document其实是【OriginTrackedMapPropertySource】
回到【ConfigFileApplicationListener】553行。
到这里,配置文件就被加载完毕了。