SpringBoot成长记4:Run方法中配置文件的处理

file

上一节,我们主要了解了SpringBoot的一个扩展点设计SpringApplicationRunListeners。并没有找到我们想要找到的Spring容器创建和web容器启动、自动装配配置的这些核心功能。

之前我们说过,xxxxEnvironment表示了配置文件的封装,这一节就让我们来看下,SpringBoot启动过程中,如何通过处理配置文件,设置到Environment对象中的。

file

我们接着往下继续分析run方法,会看到如下代码:

public ConfigurableApplicationContext run(String... args) {
    //1、扩展点 SpringApplicationRunListeners listeners.starting();
    //2、配置文件的处理(待分析)
     ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
     ConfigurableEnvironment environment = prepareEnvironment(listeners,
      applicationArguments);
     configureIgnoreBeanInfo(environment);
     Banner printedBanner = printBanner(environment);
     //其他逻辑
}

我们还是抓大放小,核心关注这一段逻辑,明显是反复出现了environment这个词汇相关的代码,肯定就是这里处理的配置文件。

这段代码主要的逻辑可以概括如下:

1)DefaultApplicationArguments命令行参数的解析和封装,也不是我们关注的重点

2)prepareEnvironment这个方法应该就是真正配置文件的处理逻辑

3)后面这两个方法,configureIgnoreBeanInfo、printBanner,明细就是基于配置设置一个参数、打印一下Banner日志输出而已,一看就不是重点。

即如下图所示:

file

通过上面的初步分析,我们可以看到,核心关注的应该是prepareEnvironment这个方法的逻辑,那接下来就让我们看看它是如何处理的配置文件吧。

SpringBoot的配置文件解析如何设计的?

在分析SpringBoot prepareEnvironment方法是如何处理配置文件之前,你可以思考下,如果让你编写配置文件的解析,你会怎么考虑呢?你可能会考虑:

从哪里找到配置文件,之后根据文件格式解析下配置文件,那每个配置文件我都可以抽象为一个对象,对象中可以有配置文件的名称,位置,具体配置值等等。最后配置文件可能是多个 ,需要一个集合来存放这些对象。构成一个列表,表示多个配置文件解析后的结果。

这个思路其实你稍微思考下,就可以得出。

那么SpringBoot其实也没有什么高深的,它大体也是这个思路,你有这个思路,再去理解代码,其实就会轻松很多。这个思想是我想要教给你们的,对理解代码会非常有用。

让我们来一起看下代码:

	private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments) {
		// Create and configure the environment
		ConfigurableEnvironment environment = getOrCreateEnvironment();
		configureEnvironment(environment, applicationArguments.getSourceArgs());
		ConfigurationPropertySources.attach(environment);
		listeners.environmentPrepared(environment);
		bindToSpringApplication(environment);
		if (!this.isCustomEnvironment) {
			environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
					deduceEnvironmentClass());
		}
		ConfigurationPropertySources.attach(environment);
		return environment;
	}

这个方法的脉络,大体就是:

1)创建了一个ConfigurableEnvironment

2)之后不断往里面设置或者放了一些值,很可能就是各种配置文件的解析后的结果。configureEnvironment、attach之类的。

3)中间还执行了关键的listeners的扩展点的一个方法—environmentPrepared()

整体如下图所示:

file

上面的逻辑看着还是比较多的,但是核心就是创建了ConfigurableEnvironment,之后给它设置了一堆属性而已。

创建的ConfigurableEnvironment配置对象抽象了哪些东西?

ConfigurableEnvironment是通过一个方法getOrCreateEnvironment()创建得来的。

你可以打开接口看下它的脉络,而ConfigurableEnvironment这个类时一个接口,定义了一些和配置相关方法。

profile表示多环境配置看,MutablePropertySources是个很关键的对象,内部包含了一个List对象,这个你可以猜想到它就是对配置文件的抽象对象,每一个PropertySource表示一个配置文件。其余的就是一些get方法了,整体类图如下所示:

file

了解了这个接口,你大体应该有个印象了, 配置文件抽象的关键点,就是PropertySource+profile封装到ConfigurableEnvironment这个接口实现类中。

这个其实跟我们之前提到过的,让你自己设计配置文件的解析的思想很类似的。你可以思考下这里的亮点:

1)PropertySource name可以用作配置文件名称,T source属性是泛型,可以放常见的key-value的properties和yml的配置文件解析结果,此时T就是一个Map,也可以放其他配置格式解析成的对象,这里没有限制的。这点设计非常好。

2)通过List统一管理多个配置文件,并且通过MutablePropertySources封装对这个集合的常见操作。

3)至于profile的设计,就是支持多环境配置,只要通过一个Set profile集合就可以实现,这个思路其实很简单。

创建时候默认添加了那些配置文件的解析?

当你知道了上面的设计思想,那么理解代码就不困难了。首先是创建Environment的代码:

private ConfigurableEnvironment getOrCreateEnvironment() {
   if (this.environment != null) {
      return this.environment;
   }
   switch (this.webApplicationType) {
   case SERVLET:
      return new StandardServletEnvironment();
   case REACTIVE:
      return new StandardReactiveWebEnvironment();
   default:
      return new StandardEnvironment();
   }
}

根据webApplicationType应用类型,之前创建SpringApplication时推断过,默认是SERVLET。所以这里创建了一个实现类是StandardServletEnvironment。

而这个实现类,默认加载了几个配置。虽然默认构造方法为空,但是父类的构造方法调用了customizePropertySources,实际还是初始化了一些配置文件。代码如下:

public class StandardServletEnvironment extends StandardEnvironment implements ConfigurableWebEnvironment {
    public static final String SERVLET_CONTEXT_PROPERTY_SOURCE_NAME = "servletContextInitParams";
    public static final String SERVLET_CONFIG_PROPERTY_SOURCE_NAME = "servletConfigInitParams";
    public static final String JNDI_PROPERTY_SOURCE_NAME = "jndiProperties";

    public StandardServletEnvironment() {
    }

    protected void customizePropertySources(MutablePropertySources propertySources) {
        propertySources.addLast(new StubPropertySource("servletConfigInitParams"));
        propertySources.addLast(new StubPropertySource("servletContextInitParams"));
        if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
            propertySources.addLast(new JndiPropertySource("jndiProperties"));
        }

        super.customizePropertySources(propertySources);
    }

    public void initPropertySources(@Nullable ServletContext servletContext, @Nullable ServletConfig servletConfig) {
        WebApplicationContextUtils.initServletPropertySources(this.getPropertySources(), servletContext, servletConfig);
    }
}

public class StandardEnvironment extends AbstractEnvironment {

	/** System environment property source name: {@value}. */
	public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment";

	/** JVM system properties property source name: {@value}. */
	public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties";

	@Override
	protected void customizePropertySources(MutablePropertySources propertySources) {
		propertySources.addLast(
				new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
		propertySources.addLast(
				new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
	}

}

public abstract class AbstractEnvironment implements ConfigurableEnvironment {
    //省略其他
	public AbstractEnvironment() {
		customizePropertySources(this.propertySources);
	}
}

可以看出来默认,创建的时候调用了父类的构造函数,配置文件主要添加了如下:

1)servletConfigInitParams、servletContextInitParams,从名字上看,我们可以连蒙带猜下,stub应该是表示是桩,可能是预留存储servlet相关配置用的,默认都是空的值。jndiProperties配置根据条件加载,默认没有加载。它配置的值默认是空的Object()

2)systemEnvironment和systemProperties配置,从名字可以猜出来,是系统属性和系统环境变量相关配置,配置的值解析后为Map

最终将所有的配置解析好放入到了List中。

也就是执行完创建,此时已经相当于构建了4个配置对象了,也就是4个PropertySource了。如下图所示:

file

你可能有一个疑惑,这些不同的配置是怎么来的?

其实,你细心思考下,你会发现,这些配置有的是自己构建的,有的是从系统环境变量加载的,有的是从配置文件读取的。

通过抽象了一个接口PropertySource。定义不同的实现,来实现配置文件的解析,这个设计思想值得我们学习,这就是经典的java面向接口的多态编程思想。

目前为止配置文件处理创建就完成了:

file

ConfigurableEnvironment其他的添砖加瓦操作

创建了ConfigurableEnvironment之后,执行了其他的一些方法。核心思路主要是给ConfigurableEnvironment设置其他的一些属性,比如转换器、profile,在增加一些PropertySource而已。

	private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments) {
        //1.创建Environment
		ConfigurableEnvironment environment = getOrCreateEnvironment();
        //2.ConfigurableEnvironment其他的添砖加瓦操作
		configureEnvironment(environment, applicationArguments.getSourceArgs());
		ConfigurationPropertySources.attach(environment);
		listeners.environmentPrepared(environment);
		bindToSpringApplication(environment);
		if (!this.isCustomEnvironment) {
			environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
					deduceEnvironmentClass());
		}
		ConfigurationPropertySources.attach(environment);
		return environment;
	}

上面代码的这些添砖加瓦的我就不仔细介绍了,主要就是设置一些属性,或者添加了配置文件对象。直接给大家画一个图概括下就好了,主要逻辑如下图红色箭头所示:

file

通过上图,可以看到,主要执行的逻辑,具体如下:

1)configureEnvironment

​ a) environment.setConversionService 设置(默认转换器Service) ApplicationConversionService 单例初始化,不是重点;

​ b) configurePropertySources() 命令行参数的覆盖一些属性,默认不传,什么都不做,不是重点;

​ c) configureProfiles() 判断是否有 spring.acitve.profiles,默认没有指定 ,设置activeprifles(List)就是空。这个值得了解下,启动时的参数如果增加了这个,在这里就会生效的,如spring.acitve.profiles=dev,prod,这里会记录下。activeprifles=dev,prod。(作用的话,其实是用于之后会额外补充加载配置文件,也就是补充PropertySource,这个一会儿在listeners.environmentPrepared分析中会看到的。)

2)ConfigurationPropertySources.attach(environment) 添加 一套配置配置文件到List开头,也就是默认添加了一个configurationProperties名字的PropertySource,或者说添加了一个Environment,因为Environment封装了PropertySource,这个配置从注释上看,可能是为了动态的添加和配置一些值用的,知道attach是补充了一个配置文件的对象封装就可以了,其他的暂时不是重点;

3)listeners.environmentPrepared(environment),事件发布Listener 发布一个环境事件 ApplicationEnvironmentPreparedEvent。配置文件的处理的扩展点核心做了些什么?按事件筛选过的listener,轮流进行一堆事件处理(和之前发布ApplicationStartEvent事件一样的机制),重要逻辑,补充了配置文件

ConfigFileApplicationListener加载配置的核心处理,很关键,通过Loader,propertySourceLoaders 解析property(properties,xml) yaml文件的(yml,yaml)从而补充了新的PropertySource。

AnsiOutputApplicationListener ANSI字符编码处理

LoggingApplicationListener 日志相关

ClasspathLoggingApplicationListener 什么都没做

Backgroundpreinitializer 什么都没做

DelegatingApplicationListener 什么都没做

FileEncodingApplicationListener 编码相关,不重要

4)bindToSpringApplication(environment) 设置environment到SpringApplication (但是实际没有设置上...比较奇怪,不重要的逻辑,不深究了),不是重点;

5)EnvironmentConverter 如果environment不是webApplicationType指定环境配置,这里转换成StandardServletEnvironment(默认SERVLET) 之前已经是,所以之类不转换了,不是重点;

6)ConfigurationPropertySources.attach(environment) 再次attach 担心转换后,这个属性不在了,不是重点;

通过上面的图和说明,基本解释了ConfigurableEnvironment的各种添砖加瓦的操作。其实如果你抓大放小其实这些都不是算是关键,真正的关键就是创建和封装了ConfigurableEnvironment,另一个比较重要的就是执行扩展点了。也就是执行listeners.environmentPrepared(environment),补充了Spring在ClassPath下的相关配置文件对象

小结

今天我们主要分析了SpringBoot在启动过程中:

1)配置文件对象的抽象封装如何设计的

2)listeners扩展点,对配置文件的处理做了扩展,补充了Spring在ClassPath下的相关配置文件对象

本文由博客群发一文多发等运营工具平台 OpenWrite 发布

posted @ 2021-10-01 22:29  _繁茂  阅读(197)  评论(0编辑  收藏  举报