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分别是
- StubPropertySource {name=‘servletConfigInitParams’} servlet的初始化参数的键值对
- StubPropertySource {name=‘servletContextInitParams’} web容器的初始化参数
- PropertiesPropertySource {name=‘systemProperties’} jvm参数和-D配置的参数
- 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 配置的参数
优先级自然从上到下。
到此整个环境配置就完成了