IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> Springboot配置文件加载原理及流程 -> 正文阅读

[Java知识库]Springboot配置文件加载原理及流程

序言

  • Springboot开箱即用,使用起来十分的便捷,但是如果我们只是知其然不知其所以然,那我们将很难熟练的驾驭它,接下来我就为大家以debug的形式为大家讲解一下Springboot配置文件的加载流程。并为大家讲解一下springboot是怎样将我们配置在yml文件中的变量赋值给对应的属性的。本文篇幅有点长,还请大家耐心观看。

环境准备

  • 准备一个Springboot项目,并配置多个配置文件
  • 在application.yml配置文件中指定项目以dev环境进行启动
  • 在每个配置文件中都配置一个相同的配置【test.profile】,每个文件配置的属性值不同
  • 编写一个测试接口,并为其注入该属性值,输出当前指定环境下的值
  • 源码地址【】
    在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

调用测试接口后输出结果:

testProfileValue:application--config-dev-jack------applicationName:demo-config-dev

显然,【TestService.testProfileValue】这个属性是从config/application-dev.yml中文件中读取并配置的,那么我们接下来从源码的角度来剖析一下Springboot的配置文件的加载流程和变量赋值原理。

(一)配置文件加载流程-准备

  1. 在主启动类打上断点,并以debug的形式启动项目,这里系统会通过new SpringApplication(primarySources)的形式创建一个SpringBoot上下文实例。
    在这里插入图片描述在这里插入图片描述
  2. SpringApplication构造器加载完Initializers和Listenter后开始调用run(String… args)方法启动Springboot上下文。我们debug进入到run方法中并将断点打在prepareEnvironment(listeners,applicationArguments)这一行,运行程序进入到断点中。
    在这里插入图片描述
  3. SpringApplication#prepareEnvironment( listeners,applicationArguments),这个方法是配置文件加载的入口,它会去执行SpringApplication构造器加载到的Listener。这里我们重要关注BootstrapApplicationListenerConfigFileApplicationListener这两个监听器。
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) {
    // 给容器创建一个Environment
    ConfigurableEnvironment environment = getOrCreateEnvironment();
    configureEnvironment(environment, applicationArguments.getSourceArgs());
    // 执行引入jar包类路径下META-INF/spring.factories文件中配置的监听器
    listeners.environmentPrepared(environment);
    // 将加载完成的环境变量信息绑定到Spring IOC容器中
    bindToSpringApplication(environment);
    if (!this.isCustomEnvironment) {
        environment = new EnvironmentConverter(getClassLoader())
                .convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
    }
    ConfigurationPropertySources.attach(environment);
    return environment;
}

在这里插入图片描述
4.SpringApplication#prepareEnvironment()触发执行监听器,优先执行BootstrapApplicationListener监听器,再执行ConfigFileApplicationListener监听器。如果想学习Spring的事件监听原理的可以看一下我的另一篇博客【Spring事件监听流程分析】。

  • BootstrapApplicationListener:来自Springcloud,优先级最高,用于启动/建立Springcloud的应用上下文。须要注意的是,到此时Springboot的上下文还未创建完成,因为在创建Springboot上下文的时候通过BootstrapApplicationListener去开启了Springcloud上下文的创建流程。 这个流程“嵌套”特别像Bean初始化流程:初始化Bean A时,遇到了Bean B,就须要先去完成Bean B的初始化,再回头来继续完成Bean A的步骤。

  • 在建立SpringCloud的应用的时候,使用的也是SpringApplication#run()完成的(非web),所以也会走下一整套SpringApplication的生命周期逻辑,因此请你务必区分,特别是这种case会让“绝大多数”初始化器、监听器等执行多次,若你有那种只须要执行一次的需求(好比只想让Springboot容器生命周期内执行,Springcloud生命周期不执行),请务必自行处理,不然会被执行多次而带来不可预知的结果。

  • Springcloud和Springboot应用上下文都是使用ConfigFileApplicationListener来完成加载和解析的。

1、Springboot应用上下文读取的外部化配置文件名默认是:application
2、Springcloud应用上下文读取的外部化配置文件名默认是:bootstrap
  • BootstrapApplicationListener#onApplicationEvent()的部分核心源码如下:
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
    // 检查是否开启了Springcloud    
    ConfigurableEnvironment environment = event.getEnvironment();
    if (!environment.getProperty("spring.cloud.bootstrap.enabled", Boolean.class, true)) {
        return;
    }
    // 如果是Springcloud上下文触发的BootstrapApplicationListener这个监听器,就不执行这个监听器了
    // 避免重复执行
    if (environment.getPropertySources().contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
        return;
    }
    ConfigurableApplicationContext context = null;
    String configName = environment.resolvePlaceholders("${spring.cloud.bootstrap.name:bootstrap}");
    for (ApplicationContextInitializer<?> initializer : event.getSpringApplication().getInitializers()) {
        if (initializer instanceof ParentContextApplicationContextInitializer) {
            context = findBootstrapContext((ParentContextApplicationContextInitializer) initializer, configName);
        }
    }
    // 如果还未创建Springcloud上下文实例,则调用bootstrapServiceContext创建
    if (context == null) {
        context = bootstrapServiceContext(environment, event.getSpringApplication(), configName);
        event.getSpringApplication().addListeners(new CloseContextOnFailureApplicationListener(context));
    }
    apply(context, event.getSpringApplication(), environment);
}
  • BootstrapApplicationListener#bootstrapServiceContext()核心源码如下:
private ConfigurableApplicationContext bootstrapServiceContext(ConfigurableEnvironment environment, final SpringApplication application,String configName) {
		/*省略非必要代码*/
		// 通过SpringApplicationBuilder构建一个Springcloud上下文实例
		SpringApplicationBuilder builder = new SpringApplicationBuilder()
				.profiles(environment.getActiveProfiles()).bannerMode(Mode.OFF)
				.environment(bootstrapEnvironment)
				// Don't use the default properties in this builder
				.registerShutdownHook(false).logStartupInfo(false)
				.web(WebApplicationType.NONE);
		final SpringApplication builderApplication = builder.application();
		if(builderApplication.getMainApplicationClass() == null){
			builder.main(application.getMainApplicationClass());
		}
		if (environment.getPropertySources().contains("refreshArgs")) {
			builderApplication.setListeners(filterListeners(builderApplication.getListeners()));
		}
		builder.sources(BootstrapImportSelectorConfiguration.class);
		// 调用Springcloud上下文实例的run方法,使用的也是SpringApplication#run()完成的
		// 这个过程会将之前的步骤再执行一次
		final ConfigurableApplicationContext context = builder.run();
		context.setId("bootstrap");
		addAncestorInitializer(application, context);
		bootstrapProperties.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME);
		mergeDefaultProperties(environment.getPropertySources(), bootstrapProperties);
		return context;
	}
  1. 调用Springcloud上下文实例的run方法,会将之前的步骤在重复执行一次,程序又执行到遍历监听器并发布事件这里了,这里我们重点关注【ConfigFileApplicationListener】这个监听器,这个监听器会完成配置文件的加载。
    在这里插入图片描述

  2. 进入断点里面之前,我先做一些说明。因为这里会创建Springcloud和Springboot两个上下文实例, 由于Springboot和Springcloud上下文实例加载配置文件的流程都是相似的,这里我们就讲解Springboot容器配置文件的加载过程。

  • bootstrap.yml 可以用来定义应用级别的, 应用程序特有配置信息,可以用来配置后续各个模块中需使用的公共参数等。
  • 如果application.yml的内容标签与bootstrap的标签一致,application会覆盖bootstrap, 而application.yml 里面的内容可以动态替换。

(二)配置文件加载流程-加载解析

  1. ConfigFileApplicationListener#onApplicationEvent(),根据上面的流程可知,程序会触发ConfigFileApplicationListener的onApplicationEvent方法,从而加载配置文件。
    在这里插入图片描述

  2. ConfigFileApplicationListener#addPropertySources(),流程继续执行到addPropertySources,这里会去新建一个Loader内部类,并执行load方法。

	/**
	 * 将配置文件解析并加载到environment中
	 */
	protected void addPropertySources(ConfigurableEnvironment environment,ResourceLoader resourceLoader) {
		RandomValuePropertySource.addToEnvironment(environment);
		// 创建内部类Loader,并调用load方法加载配置
		new Loader(environment, resourceLoader).load();
	}
  1. ConfigFileApplicationListener#Loader#load()
private class Loader {
	// 配置文件的加载优先级顺序倒叙,file:./config/配置文件的优先级最高
	private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";
	// The "active profiles" property name.
	public static final String ACTIVE_PROFILES_PROPERTY = "spring.profiles.active";
	
	// 构造器,初始化
	Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
		this.environment = environment;
		this.placeholdersResolver = new PropertySourcesPlaceholdersResolver(this.environment);
		this.resourceLoader = (resourceLoader != null) ? resourceLoader: new DefaultResourceLoader();
		this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class, getClass().getClassLoader());
	}
	
	// load方法,用于获取指定路径下的配置文件并添加到environment中
    public void load() {
		this.profiles = new LinkedList<>();
		this.processedProfiles = new LinkedList<>();
		this.activatedProfiles = false;
		this.loaded = new LinkedHashMap<>();
		// 初始化profiles
		// 若环境变量中已存在spring.profiles.active变量则将其添加到profiles中
		// 若环境变量中不存在spring.profiles.active变量,则默认添加一个defaultProfile,用于加载application.yml
		initializeProfiles();
		// 1、通过默认的default首先去加载application.yml配置文件
		// 2、读取application.yml是否有配置spring.profiles.active
		// 2.1、若没有找到spring.profiles.active配置项,则循环结束,开始执行下一步
		// 2.2、若在application.yml配置中找到了spring.profiles.active配置项,则会在profiles中添加
		// 一个profile,再次进入while,并使用active指定的文件去拼接配置文件,若找到则添加到配置中
		while (!this.profiles.isEmpty()) {
			Profile profile = this.profiles.poll();
			if (profile != null && !profile.isDefaultProfile()) {
				addProfileToEnvironment(profile.getName());
			}
			// 根据profile加载指定路径下的配置文件
			load(profile, this::getPositiveProfileFilter,
					addToLoaded(MutablePropertySources::addLast, false));
			this.processedProfiles.add(profile);
		}
		resetEnvironmentProfiles(this.processedProfiles);
		load(null, this::getNegativeProfileFilter,addToLoaded(MutablePropertySources::addFirst, true));
		// 将已解析到的配置PropertySources倒叙并添加到environment中
		addLoadedPropertySources();
	}
}
  1. 根据源码调用链路可知,程序继续调用Loader#load( profile, filterFactory, consumer),调用详情如下图所示:
    在这里插入图片描述

  2. Loader#load(location, name, profile, filterFactory, consumer)

  • location:总共分为"classpath:/,classpath:/config/,file:./,file:./config/",配置文件可配置的地址,加载优先级为倒序。
  • name:默认为“application”。
  • profile:若当前解析的不是spring.profiles.active指定的配置文件时默认为“null”,否则为spring.profiles.active指定的值。
  • filterFactory:
  • consumer:将加载的document添加到Loader#loaded属性集合中,用于最后的配置文件优先级排序。
private void load(String location, String name, Profile profile,DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
    if (!StringUtils.hasText(name)) {
        for (PropertySourceLoader loader : this.propertySourceLoaders) {
            if (canLoadFileExtension(loader, location)) {
                load(loader, location, profile,
                        filterFactory.getDocumentFilter(profile), consumer);
                return;
            }
        }
    }
    // 临时存储判断是否已经加载过了某种扩展名类型(propertis、xml、yml、yaml)
    // 的的配置,避免重复加载
    Set<String> processed = new HashSet<>();
    // this.propertySourceLoaders,分为PropertiesPropertySourceLoader和YamlPropertySourceLoader两种
    // PropertiesPropertySourceLoader:解析properties、xml类型配置
    // YamlPropertySourceLoader:解析yml、yaml类型
    for (PropertySourceLoader loader : this.propertySourceLoaders) {
    	// fileExtension由loder类型决定,优先级顺序为properties > xml > yml > ymal
    	// 配置文件拼接规则:location + name + "-" + profile + fileExtension;
        for (String fileExtension : loader.getFileExtensions()) {
            if (processed.add(fileExtension)) {
                loadForFileExtension(loader, location + name, "." + fileExtension,
                        profile, filterFactory, consumer);
            }
        }
    }
}
  1. Loader#load(loader, location, profile,filter, consumer)核心解析方法,根据已拼接好地址去获取配置文件(例如:classpath:/application-dev.yml)
  • 文件不存在:结束当前方法,继续执行下一次循环
  • 文件存在:解析配置文件,将解析到的配置文件保存到Loader#loaded变量中。
  • 文件存在时还需要尝试获取spring.profiles.active属性,规则如下:
1、若没有配置该属性值,则加载完当前fileExtension类型的配置(例如:application.properties、
xml、yml、ymal)后就不再尝试解析其他fileExtension类型的配置文件了,此时系统就默认使用加载到
的application.properties/yml配置。

2、若配置了该属性值,则读取该属性值(我们当前配置的是dev),将其添加到Loader#profiles属性
中(就是第三步中while循环的那个profiles变量值),同时Loader会将activatedProfiles属性值改为true
来标记系统已经有active这个属性值了,就算active中指定的配置文件中也配置了active这个属性,
Loader也不会再去解析该配置文件了。
  • 通过上面的分析可知,spring.profiles.active这个属性值只会被优先级最高的配置文件初始化一次。
private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter, DocumentConsumer consumer) {
    try {
        // 根据拼接的配置文件地址来加载配置文件
        // 例如location为classpath:application.yml
        Resource resource = this.resourceLoader.getResource(location);
        // 配置文件不存在,则返回继续查找
        if (resource == null || !resource.exists()) {
            if (this.logger.isTraceEnabled()) {
                StringBuilder description = getDescription(
                        "Skipped missing config ", location, resource, profile);
                this.logger.trace(description);
            }
            return;
        }
        String name = "applicationConfig: [" + location + "]";
        // 解析配置文件,并读取spring.profiles.active属性,将读取到的active属性赋值给document.getActiveProfiles()
        List<Document> documents = loadDocuments(loader, name, resource);
        // 保存已解析的配置文件
        List<Document> loaded = new ArrayList<>();
        for (Document document : documents) {
            if (filter.match(document)) {
                // 将解析到的spring.profiles.active添加到profiles中,下一次while循环就解析profile
                // 比如说这里的active为dev,则接下来就拼接并加载dev的配置文件
                // 这里要注意的是,我们把解析到active并添加到profiles中后,Loader会
                // 标注已经解析到了active属性,后续就算在后面的配置文件中解析到active属性
                addActiveProfiles(document.getActiveProfiles());
                addIncludedProfiles(document.getIncludeProfiles());
                loaded.add(document);
            }
        }
        Collections.reverse(loaded);
        if (!loaded.isEmpty()) {
        	// 将加载的document添加到Loader#loaded属性集合中,用于最后的配置文件优先级排序
        	// 根据当前加载顺序进行倒序排,由于application.yml比application-dev.yml
        	// 先加载,所以倒序后指定的application-dev.yml配置优先级更高
            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) {
        throw new IllegalStateException("Failed to load property "
                + "source from location '" + location + "'", ex);
    }
}
  1. 经过上面的步骤将所有的配置文件解析并添加到Loader#loaded属性中后,继续执行第3步中的addLoadedPropertySources() 方法,该方法会将现在loaded中保存的配置文件倒序后依次添加到environment中。
  • Loader#addLoadedPropertySources()源码及详解说明如下:
private void addLoadedPropertySources() {
    // 获取环境变量中已加载的配置信息
    MutablePropertySources destination = this.environment.getPropertySources();
    // 获取已本次Loader加载到的配置文件
    List<MutablePropertySources> loaded = new ArrayList<>(this.loaded.values());
    // 将已加载的配置文件倒序,更改优先级,spring.profile.active指定的优先级最高
    Collections.reverse(loaded);
    // 标注上一个添加到environment中的配置文件,用于确定当前配置文件插入的位置
    String lastAdded = null;
    // 利用set集合的属性,避免配置文件的重复添加
    Set<String> added = new HashSet<>();
    // 遍历并将配置添加到environment中
    for (MutablePropertySources sources : loaded) {
        for (PropertySource<?> source : sources) {
            if (added.add(source.getName())) {
            	// 将已加载的配置文件添加到environment的MutablePropertySources中
                addLoadedPropertySource(destination, lastAdded, source);
                lastAdded = source.getName();
            }
        }
    }
}

private void addLoadedPropertySource(MutablePropertySources destination, String lastAdded, PropertySource<?> source) {
    if (lastAdded == null) {
        // 如果系统中存在"defaultProperties"这个配置,则将第一个优先级的配置文件添加到这个配置文件的顺序之前
        // 如果系统中不存在"defaultProperties"这个配置,则将第一个优先级的配置文件添加到environment中的最后一个
        // defaultProperties实际为bootstrap.yml
        if (destination.contains(DEFAULT_PROPERTIES)) {
            destination.addBefore(DEFAULT_PROPERTIES, source);
        } else {
            destination.addLast(source);
        }
    } else {
        // 将当前配置文件添加到上一个配置文件之后
        destination.addAfter(lastAdded, source);
    }
}

为了大家能对这个过程有一个清晰的认识,我将这一部分源码的debug流程依次截图在了下面,方便大家理解。
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
到此我们的配置文件的加载过程就完毕了,下一篇我将为大家讲解一下Spring是怎么将这些变量赋值给我们配置的属性的【Springboot属性赋值原理】,谢谢大家的观看。

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-07-24 00:03:28  更:2021-07-24 00:04:07 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/7 6:07:12-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码