SpringBoot源码读取配置源码分析,配置优先级,加载Bean信息

一般在springboot项目中,我们只需要在appcaliton.properties或者appcaliton.yml中指定相关配置,在程序中就可以直接使用,这其中的原理是如何实现呢?

首先我们看SpringBoot项目启动流程逻辑:

// 默认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();
		// 从spring.factories文件中加载 ApplicationContextInitializer类型的类
		setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
		// 设置 this.listeners 通过从spring.factories中读取,其中ConfigFileApplicationListener用来读取配置
		setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
		this.mainApplicationClass = deduceMainApplicationClass();
	}

spring.factories中Application Listeners:

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener

我们看实际的run方法:

public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
		return new SpringApplication(primarySources).run(args);
}

可以看到,静态的run方法实际上是实例化了一个SpringApplication,调用其run方法:

public ConfigurableApplicationContext run(String... args) {
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();
		ConfigurableApplicationContext context = null;
		Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
		configureHeadlessProperty();
		// 得到一个SpringApplicationRunListeners,其属性listeners是通过读取spring.factories配置得到
		// 为 EventPublishingRunListener,其属性EventPublishingRunListener用来实际事件处理,
		// 会将SpringApplication.listeners赋值给SimpleApplicationEventMulticaster
		// 当SimpleApplicationEventMulticaster广播相关事件的时候,就调用SpringApplication.listeners进行对应事件的监听处理
		SpringApplicationRunListeners listeners = getRunListeners(args);
		listeners.starting();
		try {
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
			ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
			configureIgnoreBeanInfo(environment);
			Banner printedBanner = printBanner(environment);
			// 创建ApplicationContext,如果不是web相关,默认返回 AnnotationConfigApplicationContext
			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;
	}

这就是run方法的流程,我们看看主要的几个步骤:

1. getRunListeners:

// 得到一个SpringApplicationRunListeners,其属性listeners是通过读取spring.factories配置得到
// 为 EventPublishingRunListener,其属性EventPublishingRunListener用来实际事件处理,
// 会将SpringApplication.listeners赋值给SimpleApplicationEventMulticaster
// 当SimpleApplicationEventMulticaster广播相关事件的时候,就调用SpringApplication.listeners进行对应事件的监听处理	
private SpringApplicationRunListeners getRunListeners(String[] args) {
		Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
		return new SpringApplicationRunListeners(logger,
				getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
	}

这个后续很多功能都是基于这里的listener去实现

2. prepareEnvironment:

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments) {
		// Create and configure the environment
		// 默认返回 StandardEnvironment
		ConfigurableEnvironment environment = getOrCreateEnvironment();
		configureEnvironment(environment, applicationArguments.getSourceArgs());
		ConfigurationPropertySources.attach(environment);
		// 这里实际上就是调用EventPublishingRunListener.environmentPrepared,
		// 最终是spring.factories中ApplicationListener的onApplicationEvent对应方法来处理
		listeners.environmentPrepared(environment);
		bindToSpringApplication(environment);
		if (!this.isCustomEnvironment) {
			environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
					deduceEnvironmentClass());
		}
		ConfigurationPropertySources.attach(environment);
		return environment;
	}
	

首先会创建一个ConfigurableEnvironment :

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments) {
		// Create and configure the environment
		// 默认返回 StandardEnvironment,
		// 并且在父类AbstractEnvironment的默认构造中会调用子类模板方法customizePropertySources
		// 在StandardEnvironment.customizePropertySources中会读取虚拟机参数和系统的环境变量
		ConfigurableEnvironment environment = getOrCreateEnvironment();
		configureEnvironment(environment, applicationArguments.getSourceArgs());
		ConfigurationPropertySources.attach(environment);
		// 这里实际上就是调用EventPublishingRunListener.environmentPrepared,
		// 最终是spring.factories中ApplicationListener的onApplicationEvent对应方法来处理
		listeners.environmentPrepared(environment);
		bindToSpringApplication(environment);
		if (!this.isCustomEnvironment) {
			environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
					deduceEnvironmentClass());
		}
		ConfigurationPropertySources.attach(environment);
		return environment;
	}
	protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
		// 默认为true
		if (this.addConversionService) {
			ConversionService conversionService = ApplicationConversionService.getSharedInstance();
			environment.setConversionService((ConfigurableConversionService) conversionService);
		}
		// 这里主要是读取命令行参数,通过SimpleCommandLineArgsParser解析,必须是 --key=value格式
		configurePropertySources(environment, args);
		// 读取profile,通过读取spring.profiles.active设置activeProfile
		configureProfiles(environment, args);
	}
可以看到,prepareEnvironment返回了一个(默认)StandardEnvironment,并将系统及环境变量,虚拟机环境变量,命令行环境变量都放到该env中。这里命令行的参数是放在首位,其次是虚拟机环境变量,然后是系统环境变量

然后触发listeners.environmentPrepared(environment);`` 实际读取配置文件在 ApplicationListener中ConfigFileApplicationListener`

public void onApplicationEvent(ApplicationEvent event) {
		if (event instanceof ApplicationEnvironmentPreparedEvent) {
			onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
		}
		if (event instanceof ApplicationPreparedEvent) {
			onApplicationPreparedEvent(event);
		}
	}

	private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
		List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
		postProcessors.add(this);
		AnnotationAwareOrderComparator.sort(postProcessors);
		for (EnvironmentPostProcessor postProcessor : postProcessors) {
			postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
		}
	}

	List<EnvironmentPostProcessor> loadPostProcessors() {
		return SpringFactoriesLoader.loadFactories(EnvironmentPostProcessor.class, getClass().getClassLoader());
	}

	@Override
	public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
		addPropertySources(environment, application.getResourceLoader());
	}

另外,这里还需要注意的一个点是调用了SpringbootApplicationl.oad(); 这里会装载我们在springApplication启动的时候传入的类,

// SpringbootApplicationl.java
protected void load(ApplicationContext context, Object[] sources) {
		if (logger.isDebugEnabled()) {
			logger.debug("Loading source " + StringUtils.arrayToCommaDelimitedString(sources));
		}
		// 创建 BeanDefinitionLoader ,BeanDefinitionLoader中包含了:
		// AnnotatedBeanDefinitionReader、XmlBeanDefinitionReader、ClassPathBeanDefinitionScanner
		// 这些AnnotationConfigApplicationContext常用到的类
		BeanDefinitionLoader loader = createBeanDefinitionLoader(getBeanDefinitionRegistry(context), sources);
		if (this.beanNameGenerator != null) {
			loader.setBeanNameGenerator(this.beanNameGenerator);
		}
		if (this.resourceLoader != null) {
			loader.setResourceLoader(this.resourceLoader);
		}
		if (this.environment != null) {
			loader.setEnvironment(this.environment);
		}
		// 通过loader的AnnotatedBeanDefinitionReader注册
		loader.load();
	}
	// BeanDefinitionLoader.java
private int load(Class<?> source) {
		if (isGroovyPresent() && GroovyBeanDefinitionSource.class.isAssignableFrom(source)) {
			// Any GroovyLoaders added in beans{} DSL can contribute beans here
			GroovyBeanDefinitionSource loader = BeanUtils.instantiateClass(source, GroovyBeanDefinitionSource.class);
			load(loader);
		}
		if (isComponent(source)) {
			this.annotatedReader.register(source);
			return 1;
		}
		return 0;
	}

会将在springApplication启动的时候传入的类注入到IOC容器中,而一般该类上都有SpringBootApplication注解,

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
  ..............
}

我们之前分析 spring源码解析AnnotationConfigApplicationContext启动流程分析,加载Bean信息 在这里可以看到,将带有SpringBootApplication注解的类注入到IOC容器中,能够触发后续Bean信息的载入

当执行prepareEnvironment方法的时候,会通过EventPublishingRunListener.environmentPrepared广播该事件,对应的事件为:ApplicationEnvironmentPreparedEvent,在ConfigFileApplicationListener中处理该事件的时候,首先会读取spring.factoriesEnvironmentPostProcessor

org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor,\
org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor

同时也会将ConfigFileApplicationListener自己加入到这个处理链中,具体处理逻辑addPropertySources

protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
		RandomValuePropertySource.addToEnvironment(environment);
		new Loader(environment, resourceLoader).load();
	}

这里又通过其内部类Loader的load方法来执行:

Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
			this.environment = environment;
			this.placeholdersResolver = new PropertySourcesPlaceholdersResolver(this.environment);
			this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader();
			// 加载spring.factoruies中PropertySourceLoader对应的类,为PropertiesPropertySourceLoader、YamlPropertySourceLoader
			this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
					getClass().getClassLoader());
		}

		public void load() {
			this.profiles = new LinkedList<>();
			this.processedProfiles = new LinkedList<>();
			this.activatedProfiles = false;
			this.loaded = new LinkedHashMap<>();
			// 默认返回null和default两个
			initializeProfiles();
			// 这里首先读取的就是默认的两个null和default,
			// 如果在读取的文件中发现还有其他的profile,会将其加入到this.profiles中
			// 这样,就完成了对其他profile的读取
			while (!this.profiles.isEmpty()) {
			// 默认先读取的是profile=null,也就是applicaiton.properties,然后读取的profile=default,也就是application-default.properties配置
				Profile profile = this.profiles.poll();
				if (profile != null && !profile.isDefaultProfile()) {
					addProfileToEnvironment(profile.getName());
				}
				load(profile, this::getPositiveProfileFilter, addToLoaded(MutablePropertySources::addLast, false));
				this.processedProfiles.add(profile);
			}
			resetEnvironmentProfiles(this.processedProfiles);
			load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
			addLoadedPropertySources();
		}
		// profile为空或者profile不为空且在environment的acceptsProfiles中,如果在application.properties中配置了spring.profiles配置,这里返回为false,application.properties无法加入配置
		private DocumentFilter getPositiveProfileFilter(Profile profile) {
			return (Document document) -> {
				if (profile == null) {
					return ObjectUtils.isEmpty(document.getProfiles());
				}
				return ObjectUtils.containsElement(document.getProfiles(), profile.getName())
						&& this.environment.acceptsProfiles(Profiles.of(document.getProfiles()));
			};
		}
		// profile为空且document的profile在acceptsProfiles中
		private DocumentFilter getNegativeProfileFilter(Profile profile) {
			return (Document document) -> (profile == null && !ObjectUtils.isEmpty(document.getProfiles())
					&& this.environment.acceptsProfiles(Profiles.of(document.getProfiles())));
		}

通过initializeProfiles();来读取profile配置:

// 默认返回两个,null和default
private void initializeProfiles() {
			this.profiles.add(null);
			Set<Profile> activatedViaProperty = getProfilesActivatedViaProperty();
			this.profiles.addAll(getOtherActiveProfiles(activatedViaProperty));
			addActiveProfiles(activatedViaProperty);
			if (this.profiles.size() == 1) { // only has null profile
				for (String defaultProfileName : this.environment.getDefaultProfiles()) {
					Profile defaultProfile = new Profile(defaultProfileName, true);
					this.profiles.add(defaultProfile);
				}
			}
		}

		private Set<Profile> getProfilesActivatedViaProperty() {
			if (!this.environment.containsProperty("spring.profiles.active")
					&& !this.environment.containsProperty(“spring.profiles.include”)) {
				return Collections.emptySet();
			}
			Binder binder = Binder.get(this.environment);
			Set<Profile> activeProfiles = new LinkedHashSet<>();
			activeProfiles.addAll(getProfiles(binder,“spring.profiles.include”));
			activeProfiles.addAll(getProfiles(binder, "spring.profiles.active"));
			return activeProfiles;
		}
···
在类构造实例化的时候,会加载`spring.factories`中`PropertySourceLoader`类:
```properties
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader

实际load方法中:

private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {

			// getSearchLocations 默认返回 DEFAULT_SEARCH_LOCATIONS 切分后的数组,即 [ classpath:/,classpath:/config/,file:./,file:./config/]
			getSearchLocations().forEach((location) -> {
				boolean isFolder = location.endsWith("/");
				// 这里得到默认的配置名称前缀 application
				Set<String> names = isFolder ? getSearchNames() : NO_SEARCH_NAMES;
				names.forEach((name) -> load(location, name, profile, filterFactory, consumer));
			});
		}
		private Set<String> getSearchNames() {
			if (this.environment.containsProperty(“spring.config.name”)) {
				String property = this.environment.getProperty(“spring.config.name”);
				return asResolvedSet(property, null);
			}
			return asResolvedSet(ConfigFileApplicationListener.this.names, "application");
		}

可以看到,默认就是 classpath:/,classpath:/config/,file:./,file:./config/对这几个目录下进行文件读取,默认名称为application,可以看到,这里读取配置名称是通过环境中是否有spring.config.name这个配置,如果没有则读取默认的,这里在spring-cloud的配置中心的实现中会用到这个。

private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory,
				DocumentConsumer consumer) {
			// 默认name为application,这个分支跳过
			if (!StringUtils.hasText(name)) {
				// this.propertySourceLoaders 为 spring.factoruies中PropertySourceLoader对应的类,为PropertiesPropertySourceLoader、YamlPropertySourceLoader
				for (PropertySourceLoader loader : this.propertySourceLoaders) {
					// PropertiesPropertySourceLoader可以加载 "properties", "xml" 后缀文件
					// YamlPropertySourceLoader可以加载 "yml", "yaml"
					// 判断路径下是否有符合propertySourceLoaders加载的后缀的文件
					if (canLoadFileExtension(loader, location)) {
						load(loader, location, profile, filterFactory.getDocumentFilter(profile), consumer);
						return;
					}
				}
			}
			//
			Set<String> processed = new HashSet<>();
			for (PropertySourceLoader loader : this.propertySourceLoaders) {
				for (String fileExtension : loader.getFileExtensions()) {
					if (processed.add(fileExtension)) {
						loadForFileExtension(loader, location + name, "." + fileExtension, profile, filterFactory,
								consumer);
					}
				}
			}
		}

这里通过构造时候读取的PropertySourceLoader 来读取配置,先得到各个PropertySourceLoader 能够读取的文件的格式,依次进行加载:

private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension,
				Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
			DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null);
			DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile);
			if (profile != null) {
				// Try profile-specific file & profile section in profile file (gh-340)
				// 先加载application-profile.properties文件
				String profileSpecificFile = prefix + "-" + profile + fileExtension;
				load(loader, profileSpecificFile, profile, defaultFilter, consumer);
				load(loader, profileSpecificFile, profile, profileFilter, consumer);
				// Try profile specific sections in files we've already processed
				for (Profile processedProfile : this.processedProfiles) {
					if (processedProfile != null) {
						String previouslyLoaded = prefix + "-" + processedProfile + fileExtension;
						load(loader, previouslyLoaded, profile, profileFilter, consumer);
					}
				}
			}
			// Also try the profile-specific section (if any) of the normal file
			// 加载application.properties文件
			load(loader, prefix + fileExtension, profile, profileFilter, consumer);
		}
		private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter,
				DocumentConsumer consumer) {
			try {
				// 先判断对应的文件是否存在,如果不存在,则跳过
				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;
				}
				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);
					}
					return;
				}
				String name = "applicationConfig: [" + location + "]";
				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);
					}
					return;
				}
				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) {
				throw new IllegalStateException("Failed to load property " + "source from location '" + location + "'",
						ex);
			}
		}

这里可以看到,最后加载落在了loadDocuments:

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) {
			// .properties返回 OriginTrackedMapPropertySource,list只有一个,
			// 但是yaml则可能有多个,因为在yaml中可以配置多个profile,也可以通过spring.profiles指定有哪些profile
				List<PropertySource<?>> loaded = loader.load(name, resource);
				documents = asDocuments(loaded);
				this.loadDocumentsCache.put(cacheKey, documents);
			}
			return documents;
		}

可以看到最后还是通过PropertySourceLoader来进行实际读取。

// PropertiesPropertySourceLoader.java
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, properties));
	}

	@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();
	}

最后返回的是一个OriginTrackedMapPropertySource,然后会将获取到的OriginTrackedMapPropertySource添加到Loader的属性private Map<Profile, MutablePropertySources> loaded;,这里就加载完了各个配置文件,接下来看addLoadedPropertySources,通过这个将配置添加到了Env中:

MutablePropertySources destination = this.environment.getPropertySources();
			// this.loaded是一个LinkedHashMap,放入的时候是application-dev.properties,application.properties
			// 这里进行反转,使application.properties的配置靠前
			List<MutablePropertySources> loaded = new ArrayList<>(this.loaded.values());
			Collections.reverse(loaded);
			String lastAdded = null;
			Set<String> added = new HashSet<>();
			for (MutablePropertySources sources : loaded) {
				for (PropertySource<?> source : sources) {
					if (added.add(source.getName())) {
						addLoadedPropertySource(destination, lastAdded, source);
						lastAdded = source.getName();
					}
				}
			}
		}

		private void addLoadedPropertySource(MutablePropertySources destination, String lastAdded,
				PropertySource<?> source) {
			if (lastAdded == null) {
				if (destination.contains(DEFAULT_PROPERTIES)) {
					destination.addBefore(DEFAULT_PROPERTIES, source);
				}
				else {
					destination.addLast(source);
				}
			}
			else {
				destination.addAfter(lastAdded, source);
			}
		}

	}
可以看到,这里的配置顺序是: application.properties,application-dev.properties,但是添加到ENV中的时候,都是在后面追加添加的,即最后的配置顺序是:
命令行参数 > 虚拟机环境变量 > 系统环境变量 > application-dev.properties > application.properties
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值