springBoot 源码五:springboot启动源码补充和配置优先级

springboot启动源码补充和配置优先级

上一篇分析了springboot的启动流程,其中还有一些部分没有分析完全。接下来通过一下方面进行分析

  • @SpringBootApplication注解
  • 启动流程中的第五步,准备环境变量部分 这部分和springboot配置优先级有关
  • callRunners(context, applicationArguments); 方法解释
  • 对启动流程的总结

@SpringBootApplication注解分析

先来看下@SpringBootApplication注解有哪些内容
在这里插入图片描述
在这里插入图片描述
@SpringBootApplication包含了以上几个注解和属性。

先来分析几个常用的属性
exclude()和excludeName()都是用来排除自动配置类的。注意这里排除的是自动配置类,不是我们定义的bean。这部分源码在springboot自动配置源码分析中有分析过
scanBasePackages()和scanBasePackageClasses()配置对应的扫描路径,这部分的内容是spring的配置扫描的内容Bean的生命周期源码解析中分析过扫描流程
nameGenerator() 指定使用BeanNameGenerator类型,BeanNameGenerator类型用来创建bean的名字的,一般都不会配置
proxyBeanMethods() 这个是spring的内容,在spring配置类解析源码解析中提到过

在来看看SpringBootApplication的注解
@SpringBootConfiguration
实际上就是 @Configuration spring的内容 标注它的类被spring当成配置类来解析
在这里插入图片描述
@EnableAutoConfiguration 开启自动配置的注解
在这里插入图片描述
其中@Import(AutoConfigurationImportSelector.class)在自动配置底层源码分析中分析了
@AutoConfigurationPackage 的两个属性的值 就是@SpringBootApplication对应的属性配置的值
在这里插入图片描述
在这里插入图片描述
然后来看下@Import(AutoConfigurationPackages.Registrar.class)做了什么?

static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {

		@Override
		public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
			register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
		}

		@Override
		public Set<Object> determineImports(AnnotationMetadata metadata) {
			return Collections.singleton(new PackageImports(metadata));
		}

	}

其中new PackageImports(metadata).getPackageNames().toArray(new String[0])

PackageImports(AnnotationMetadata metadata) {
			//把AutoConfigurationPackage的属性值封装成AnnotationAttributes 对象
			AnnotationAttributes attributes = AnnotationAttributes
					.fromMap(metadata.getAnnotationAttributes(AutoConfigurationPackage.class.getName(), false));
			//从AnnotationAttributes 获取属性名为basePackages的值 也就是获取配置的扫描路径
			List<String> packageNames = new ArrayList<>(Arrays.asList(attributes.getStringArray("basePackages")));
			//遍历配置的扫描类 把对应包路径添加到packageNames中
			for (Class<?> basePackageClass : attributes.getClassArray("basePackageClasses")) {
				packageNames.add(basePackageClass.getPackage().getName());
			}
			// 添加主启动类的包路径 也就是MyApplicatoin类所在的包
			if (packageNames.isEmpty()) {
				packageNames.add(ClassUtils.getPackageName(metadata.getClassName()));
			}
			this.packageNames = Collections.unmodifiableList(packageNames);
		}

获取主启动类的包路径和配置了basePackages和basePackageClasses的包路径。
register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));

public static void register(BeanDefinitionRegistry registry, String... packageNames) {
		if (registry.containsBeanDefinition(BEAN)) {
			//BeanDefinition注册器中如果有AutoConfigurationPackages这个BeanDefinition
			//从BeanDefinition注册器中获取AutoConfigurationPackages的BeanDefinition
			BasePackagesBeanDefinition beanDefinition = (BasePackagesBeanDefinition) registry.getBeanDefinition(BEAN);
			//把扫描路径添加到AutoConfigurationPackages的BeanDefinition中
			beanDefinition.addBasePackages(packageNames);
		}
		else {
			//创建一个BeanDefinition 名字为autoConfigurationPackages
			registry.registerBeanDefinition(BEAN, new BasePackagesBeanDefinition(packageNames));
		}
	}

把获取到的扫描路径生成BeanDefinition保存起来,一方面给后续执行扫描逻辑时使用,一方面给第三方框架获取扫描路径使用
总结起来就是@EnableAutoConfiguration注解,一方面开启自动配置功能,一方面获取主启动类所在的包作为扫描路径

@ComponentScan
spring的扫描注解,在springboot中

@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })

主要是配置了两个过滤器AutoConfigurationExcludeFilter和TypeExcludeFilter
以AutoConfigurationExcludeFilter为例
在这里插入图片描述
这个过滤器会通过match方法判断当前加载bean是否有@Configuration注解,并且是否是META-INF/spring.factories下的自动配置类当中的。如果是返回true,表示springboot不处理这个类,相当于过滤掉,如果不是返回false,springboot回去解析这个类。
总结下来就是:AutoConfigurationExcludeFilter的作用是扫描到的配置类名字如果在自动配置类名集合中,就不解析
TypeExcludeFilter
同理

@Override
	public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory)
			throws IOException {
		if (this.beanFactory instanceof ListableBeanFactory && getClass() == TypeExcludeFilter.class) {

			// 从Spring容器中获取TypeExcludeFilter,然后进行匹配,匹配的则不解析
			for (TypeExcludeFilter delegate : getDelegates()) {
				if (delegate.match(metadataReader, metadataReaderFactory)) {
					return true;
				}
			}
		}
		return false;
	}

到此整个@SpringBootApplication就分析完毕

callRunners(context, applicationArguments); 方法解释

这是在springboot启动路程中的第十步,过程大概是,获取Spring容器中的ApplicationRunner类型和CommandLineRunner类型的Bean,执行它们的run()。

private void callRunners(ApplicationContext context, ApplicationArguments args) {
		List<Object> runners = new ArrayList<>();
		runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
		runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
		AnnotationAwareOrderComparator.sort(runners);

		for (Object runner : new LinkedHashSet<>(runners)) {

			if (runner instanceof ApplicationRunner) {
				callRunner((ApplicationRunner) runner, args);
			}

			if (runner instanceof CommandLineRunner) {
				callRunner((CommandLineRunner) runner, args);
			}
		}
	}

那么ApplicationRunner和CommandLineRunner这两个接口是用来干嘛的呢,实际上就是对命令行参数的处理。举个例子

@SpringBootApplication
public class Application {
	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);

	}

	@Bean
	public ApplicationRunner applicationRunner(){
		return new ApplicationRunner() {
			@Override
			public void run(ApplicationArguments args) throws Exception {
				//输出带有--的参数 key 和value
				System.out.println(args.getNonOptionArgs());
				//输出不带--的key
				System.out.println(args.getOptionNames());
				//输出不带--的key对应的value
				System.out.println(args.getOptionValues("aa"));
			}
		};
	}

}

springboot启动类中定义了一个bean ,之前说过args的值就是命令行参数。所以在idea中模拟命令行参数
在这里插入图片描述
启动springboot
在这里插入图片描述

启动流程中的第五步,准备环境变量部分和SpringBoot配置优先级

先来看下这部分源码

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
			DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
		// Create and configure the environment
		// 创建ApplicationServletEnvironment,里面添加了四个PropertySource
		// 1. StubPropertySource {name='servletConfigInitParams'}
		// 2. StubPropertySource {name='servletContextInitParams'}
		// 3. PropertiesPropertySource {name='systemProperties'}
		// 4. SystemEnvironmentPropertySource {name='systemEnvironment'}
		ConfigurableEnvironment environment = getOrCreateEnvironment();

		// 添加SimpleCommandLinePropertySource {name='commandLineArgs'},放在首位
		configureEnvironment(environment, applicationArguments.getSourceArgs());

		// 把所有的PropertySources封装为一个ConfigurationPropertySourcesPropertySource
		// 然后添加到environment中,放在首位
		ConfigurationPropertySources.attach(environment);

		// 发布ApplicationEnvironmentPreparedEvent事件,表示环境已经准备好了
		// 默认EnvironmentPostProcessorApplicationListener会处理这个事件,会从spring.factories中拿出EnvironmentPostProcessor进一步处理Environment
		listeners.environmentPrepared(bootstrapContext, environment);

		// 最后,把defaultProperties移到最后
		DefaultPropertiesPropertySource.moveToEnd(environment);
		Assert.state(!environment.containsProperty("spring.main.environment-prefix"),
				"Environment prefix cannot be set via properties.");
		bindToSpringApplication(environment);
		if (!this.isCustomEnvironment) {
			environment = convertEnvironment(environment);
		}
		ConfigurationPropertySources.attach(environment);
		return environment;
	}

这里面比较复杂,具体的步骤源码不再展开,具体说说大致流程。
ConfigurableEnvironment environment = getOrCreateEnvironment();
首先创建一个ConfigurableEnvironment 环境变量的对象。这个过程中往ConfigurableEnvironment 条件四个PropertySource。每个PropertySource相当于properties文件解析后或者理解成一个map。一开始这四个PropertySource分别是

  1. StubPropertySource {name=‘servletConfigInitParams’} servlet的初始化参数的键值对
  2. StubPropertySource {name=‘servletContextInitParams’} web容器的初始化参数
  3. PropertiesPropertySource {name=‘systemProperties’} jvm参数和-D配置的参数
  4. SystemEnvironmentPropertySource {name=‘systemEnvironment’} 操作系统的参数

当我们需要用到配置参数的时候就会从这些PropertySource里面拿,那么是怎么拿的呢?,springboot会从第一个开始,拿到了就直接使用,这就是优先级了。

configureEnvironment(environment, applicationArguments.getSourceArgs());
这个方法中,先判断有没有设置DefaultProperties,例如

@SpringBootApplication
public class Application {
	public static void main(String[] args) {
//		SpringApplication.run(Application.class, args);
		SpringApplication springApplication = new SpringApplication(Application.class);
		Map<String,Object> map = new HashMap<>();
		map.put("server.port",8009);
		springApplication.setDefaultProperties(map);
		springApplication.run(args);
	}

如果设置了,在最后一个PropertySource后面添加一个名字叫defaultProperties的PropertySource。此时就有五个PropertySource

servletConfigInitParams
servletContextInitParams
systemProperties
systemEnvironment
defaultProperties

然后把命令行参数放到名字叫commandLineArgs的PropertySource中,放到最前面。命令行参数也就是java -jar xxx.jar --server.port=8002此时就有6个PropertySource

commandLineArgs :命令行参数
servletConfigInitParams :servlet的初始化参数的键值对
servletContextInitParams :web容器的初始化参数
systemProperties:jvm参数和-D配置的参数
systemEnvironment :操作系统的参数
defaultProperties: SpringApplication 配置的参数

listeners.environmentPrepared(bootstrapContext, environment);
发布一个环境变量准备好的事件,有一个叫EnvironmentPostProcessorApplicationListener的监听器监听到这个事件后会进行后续处理。具体的处理代码

private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
		ConfigurableEnvironment environment = event.getEnvironment();
		SpringApplication application = event.getSpringApplication();

		// 从spring.factories中拿出EnvironmentPostProcessor进一步处理Environment
		// RandomValuePropertySourceEnvironmentPostProcessor
		// SystemEnvironmentPropertySourceEnvironmentPostProcessor
		// SpringApplicationJsonEnvironmentPostProcessor
		// CloudFoundryVcapEnvironmentPostProcessor
		// ConfigDataEnvironmentPostProcessor
		// IntegrationPropertiesEnvironmentPostProcessor
		// DebugAgentEnvironmentPostProcessor
		for (EnvironmentPostProcessor postProcessor : getEnvironmentPostProcessors(application.getResourceLoader(),
				event.getBootstrapContext())) {
			postProcessor.postProcessEnvironment(environment, application);
		}
	}

这个方法会从spring.factories拿到一系列的EnvironmentPostProcessor。对environment进行处理,例如第一个RandomValuePropertySourceEnvironmentPostProcessor,就会在systemEnvironment 这个PropertySource后面在增加加一个叫random的PropertySource。此时就有7个PropertySource

commandLineArgs :命令行参数
servletConfigInitParams :servlet的初始化参数的键值对
servletContextInitParams :web容器的初始化参数
systemProperties:jvm参数和-D配置的参数
systemEnvironment :操作系统的参数
random :存储一些随机数 通过random.int 可以获取到int的随机数 同理还有random.long 等等
defaultProperties: SpringApplication 配置的参数

其他的EnvironmentPostProcessor就不在分析,核心来分析下ConfigDataEnvironmentPostProcessor
ConfigDataEnvironmentPostProcessor
首先会调用ConfigDataEnvironmentPostProcessor.的postProcessEnvironment的方法

void postProcessEnvironment(ConfigurableEnvironment environment, ResourceLoader resourceLoader,
			Collection<String> additionalProfiles) {
		try {
			this.logger.trace("Post-processing environment to add config data");
			resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader();
			// 先生成ConfigDataEnvironment对象,核心是根据指定location构造类型为Kind.INITIAL_IMPORT的ConfigDataEnvironmentContributor
			// processAndApply()将进行解析
			getConfigDataEnvironment(environment, resourceLoader, additionalProfiles).processAndApply();
		}
		catch (UseLegacyConfigProcessingException ex) {
			this.logger.debug(LogMessage.format("Switching to legacy config file processing [%s]",
					ex.getConfigurationProperty()));
			configureAdditionalProfiles(environment, additionalProfiles);
			postProcessUsingLegacyApplicationListener(environment, resourceLoader);
		}
	}

该方法的核心方法是getConfigDataEnvironment(environment, resourceLoader, additionalProfiles).processAndApply();这个方法可以分成两部分,一部分getConfigDataEnvironment(environment, resourceLoader, additionalProfiles) 使用来创建 ConfigDataEnvironment对象的
另一部分就是通过ConfigDataEnvironment的processAndApply()进行配置解析。
其中ConfigDataEnvironment有一些默认路径来找application.properties 等这些配置文件
在这里插入图片描述
optional:前缀表示允许在该路径下找不到配置文件
也可以通过配置以下参数指定找配置文件的路径

spring.config.location 指定了默认路径就不生效
spring.config.import 在默认路径下新增
spring.config.additional-location 在默认路径下新增

当然这些参数配置在配置文件中是不生效,因为现在就是要去找寻配置文件。
那么这些路径的顺序是怎样的呢,先从那个路劲下开始找呢?
如果通过spring.config.import 或者 spring.config.additional-location配置了新的路径 那么该路径排最前面

新配置的路径
file./
file./config
file./config/*/
classpath:/
classpath:/config/

从上到下的顺序去找配置文件。那么找那些配置文件呢,可以通过spring.config.name进行配置,默认值为application。
所以会从上面几个路径依次去找application.yaml,application.yml,application.properties
按照上述文件顺序找到后,有一个倒序的操作
在这里插入图片描述
所以配置文件的优先级变成properties>yml。如果多个路径下有配置文件,那么先按照路径的优先级,再到文件的优先级。
此时找到的配置文件并没有解析所有配置文件放入到环境变量中,而是解析成PropertySource按顺序放到一个集合当中,判断有没有配置spring.profiles.active。比如说配置了spring.profiles.active=dve,那么就会去找application-dve的配置文件。所以spring.profiles.active=dve的配置需要放在application配置文件下。
找到以后再解析application-dve的配置文件,解析完成后放入到环境变量中,此时application-dve的优先级是高于application的。
此时环境变量的PropertySource 顺序如下

commandLineArgs :命令行参数
servletConfigInitParams :servlet的初始化参数的键值对
servletContextInitParams :web容器的初始化参数
systemProperties:jvm参数和-D配置的参数
systemEnvironment :操作系统的参数
random :存储一些随机数 通过random.int 可以获取到int的随机数 同理还有random.long 等等
application-dve.properties
application-dve.yml
application.properties
application.yml
defaultProperties: SpringApplication 配置的参数

优先级自然从上到下。
到此整个环境配置就完成了

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值