写在前面:
整整一年多没写博客了,这一年事情有点多,没腾出时间来,希望后面能坚持下去,想把SpringBoot系列及常用starter的源码都好好分析一下,记录下来。由于SpringBoot框架实在太过庞大,如果把每一个细节都追踪到最底层,我估计可能要写好多年。不过原则上,重要的模块都要覆盖到。SpringBoot的源码还是有很多值得学习的地方,比如设计模式、各种缓存的设计、面向接口、职责划分清晰等。
不多BB了。
本文内容:
SpringApplication入口类分析,事件广播机制,环境对象创建,环境属性解析,环境属性占位符解析,配置优先级顺序
从SpringApplication类开始
1.SpringApplication构造方法
我们常见的主类如下:
@SpringBootApplication
public class SpringBootTestApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootTestApplication.class, args);
}
}
首先顺着构造方法来到这里:
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();
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
这里主要做了3件事,我们一个一个来过。
第一件事
WebApplicationType.deduceFromClasspath();推断当前应用类型,其实思路就是判断类路径下存在的类库去做if-else判断,如果存在reactive相关依赖,并且不存在springmvc和jersey相关依赖,则推断为reactive应用;否则判断Servlet相关的类(Servlet类和配置化的web容器)是否存在,只要有一个不存在就判断为非web应用;如果Servlet相关的类都存在,最后就判断为传统的Servlet应用。代码如下:
static WebApplicationType deduceFromClasspath() {
if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
return WebApplicationType.REACTIVE;
}
for (String className : SERVLET_INDICATOR_CLASSES) {
if (!ClassUtils.isPresent(className, null)) {
return WebApplicationType.NONE;
}
}
return WebApplicationType.SERVLET;
}
第二件事
从META-INF/spring.factories中加载ApplicationContextInitializer和ApplicationListener的实现类,然后排序,并进行实例化。这个有点类似于jdk的spi机制。先说一下ApplicationContextInitializer这类的作用,它会在准备spring容器阶段进行一下初始化的动作,这个方法中会传入spring容器,需要注意的是,该阶段还未创建我们自定义的bean。
然后再说一下ApplicationListener,这个是Spring事件的监听器接口类,如果我们想监听spring事件,可以实现该接口并注册到spring容器中以实现对Spring容器事件(ApplicationContextEvent)的就监听,如果想监听应用相关事件(SpringApplicationEvent),需要添加到spring.factories配置文件中。可以看到配置文件中配置的类有这些,用到的时候我们再来分析:
# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer,\
org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer
# 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
第三件事
推断主类,这里new了一个RunTimeException,然后获取栈信息,依次查找main方法所在的类。
需要注意的是,主类(mainApplicationClass)和主配置源(primarySources)可以不是同一个类,比如我们可以在A类上调用SpringApplication的构造方法并run,但是将@SpringApplication放在B类上,这样A就是主类,B就是主配置源。
2.SpringApplication.run()
分析完构造方法,来到run方法,这里将当前类作为主配置类传入run方法,顺着重载的run方法到最底层:
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
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;
}
首先使用了一个类似计时器的东西——StopWatch,用它来记录SpringBoot的启动耗时,这个类相当简单,start方法用于开始一个计时任务,保存当前任务名、开始时间;而当stop方法被调用时,会计算当前时间和start开始时间的总时间差,并保存历史任务的任务名、耗时,可以从这里看出这个计时器显然是支持多次启停的。我们在写一个服务类时一般也会打印一些耗时信息,对我们来说,这也是一个很好地工具类。
然后就正式开始应用的启动工作了,还是一样的套路,获取spring.factories中SpringApplicationRunListener的实现类并实例化,目前仅有一个实现类,EventPublishingRunListener,spring用它来运行事件监听器,其实就起到了将spring事件广播到所有监听器的作用。EventPublishingRunListener内部将广播的功能委托给一个SimpleApplicationEventMulticaster来处理,并在EventPublishingRunListener构造方法中将SimpleApplicationEventMulticaster实例化,并将之前获取到的ApplicationListener实现类添加到SimpleApplicationEventMulticaster实例中。调用listeners.starting(),将直接广播ApplicationStartingEvent事件。
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
Executor executor = getTaskExecutor();
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
if (executor != null) {
executor.execute(() -> invokeListener(listener, event));
}
else {
invokeListener(listener, event);
}
}
}
然后我们关注一下SimpleApplicationEventMulticaster,直接看到他的multicastEvent方法,可以看到他有一个错误处理器(ErrorHandler),并且也支持Executor方式的异步广播。默认是单线程执行,并且无错误处理器。然后我们看一下getApplicationListeners方法,这里设计了一个缓存,get的时候用了熟悉的双重检查锁,如果没有命中缓存,将调用所有实现类的supportsSourceType检查是否支持当前事件,将支持的监听器搜集起来并封装到ListenerRetriever实例,使之后get监听器的时候可以直接使用缓存,而不是依次调用supportsSourceType再去检查。
然后我们开始分析所有支持当前广播事件ApplicationStartingEvent的所有监听器,直接debug拿到它们的列表:
LoggingApplicationListener
优先以系统变量:org.springframework.boot.logging.LoggingSystem配置的实现类为准,若未配置,则使用LoggingSystem.SYSTEMS中的实现类,根据类路径是否存在相应的依赖过滤出可以使用的日志系统,默认返回第一个,即LogbackLoggingSystem。然后进行初始化,初始化就干了一件事情,将jul日志框架默认的ConsoleHandler替换成了SLF4JBridgeHandler。
BackgroundPreinitializer
直接看到onApplicationEvent方法,检查是否配置spring.backgroundpreinitializer.ignore,若无,并且是ApplicationStartingEvent事件,并且可用处理器数量大于1,并且CAS成功(用于判断是否已经开始初始化了),然后进入performPreinitialization方法,单线程的方式执行了几个Runable实例,来进行他们的预初始化。
DelegatingApplicationListener
啥也没有做。
LiquibaseServiceLocatorApplicationListener
因为没有引入相关依赖,所以也没有做任何事。
接下来开始创建并配置环境对象的过程:
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
// Create and configure the environment
// 根据web类型创建环境对象
ConfigurableEnvironment environment = getOrCreateEnvironment();
//配置环境对象
configureEnvironment(environment, applicationArguments.getSourceArgs());
//追加SpringConfiguration属性源
ConfigurationPropertySources.attach(environment);
//广播环境对象准备完毕的时事件
listeners.environmentPrepared(environment);
//绑定
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
deduceEnvironmentClass());
}
ConfigurationPropertySources.attach(environment);
return 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();
}
}
然后进行配置,将刚刚创建的环境对象和命令行参数传入,将其作为属性源加入到环境属性源集合中。可以看到,环境的属性是可变的(支持重复加载的),前后两次的配置项都会封装成一个混合的CompositePropertySource属性源,并且替换之前的单个属性源SimpleCommandLinePropertySource。
protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) {
MutablePropertySources sources = environment.getPropertySources();
if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) {
sources.addLast(new MapPropertySource("defaultProperties", this.defaultProperties));
}
if (this.addCommandLineProperties && args.length > 0) {
String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;
if (sources.contains(name)) {
PropertySource<?> source = sources.get(name);
CompositePropertySource composite = new CompositePropertySource(name);
composite.addPropertySource(
new SimpleCommandLinePropertySource("springApplicationCommandLineArgs", args));
composite.addPropertySource(source);
sources.replace(name, composite);
}
else {
sources.addFirst(new SimpleCommandLinePropertySource(args));
}
}
}
再然后进行Profiles的配置,该解析工作最终委托给了PropertySourcesPropertyResolver,它将遍历所有属性源,获取key为spring.profiles.active的值,并将该值进行转换,转换工作委托给默认的DefaultConversionService。
再看一下PropertySourcesPropertyResolver的getProperty方法:
protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
if (this.propertySources != null) {
for (PropertySource<?> propertySource : this.propertySources) {
if (logger.isTraceEnabled()) {
logger.trace("Searching for key '" + key + "' in PropertySource '" +
propertySource.getName() + "'");
}
Object value = propertySource.getProperty(key);
if (value != null) {
if (resolveNestedPlaceholders && value instanceof String) {
value = resolveNestedPlaceholders((String) value);
}
logKeyFound(key, propertySource, value);
return convertValueIfNecessary(value, targetValueType);
}
}
}
if (logger.isTraceEnabled()) {
logger.trace("Could not find key '" + key + "' in any property source");
}
return null;
}
该方法也包含了解析占位符的逻辑,比如我们常用的:${service.invoke.timeout:200},具体的占位符替换逻辑委托给PropertyPlaceholderHelper,它会通过String.indexOf()方法匹配前缀"${"、后缀"}"、默认值分隔符":"来解析出实际的值,但并不是所有的属性源都支持带默认值的占位符,这点需要注意,ComadLine型的就不支持。而且可以看出来它有递归的逻辑在这里,支持了嵌套的占位符解析。逻辑如下:
protected String parseStringValue(
String value, PlaceholderResolver placeholderResolver, @Nullable Set<String> visitedPlaceholders) {
//====== 寻找占位符前缀"${"
int startIndex = value.indexOf(this.placeholderPrefix);
if (startIndex == -1) {
return value;
}
StringBuilder result = new StringBuilder(value);
while (startIndex != -1) {
//====== 寻找占位符后缀"}"
int endIndex = findPlaceholderEndIndex(result, startIndex);
if (endIndex != -1) {
String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);
String originalPlaceholder = placeholder;
if (visitedPlaceholders == null) {
visitedPlaceholders = new HashSet<>(4);
}
if (!visitedPlaceholders.add(originalPlaceholder)) {
throw new IllegalArgumentException(
"Circular placeholder reference '" + originalPlaceholder + "' in property definitions");
}
// Recursive invocation, parsing placeholders contained in the placeholder key.
placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
// Now obtain the value for the fully resolved key...
//为占位符中的key找对应的value
String propVal = placeholderResolver.resolvePlaceholder(placeholder);
if (propVal == null && this.valueSeparator != null) {
//======默认值处理
int separatorIndex = placeholder.indexOf(this.valueSeparator);
if (separatorIndex != -1) {
String actualPlaceholder = placeholder.substring(0, separatorIndex);
String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length());
propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
if (propVal == null) {
propVal = defaultValue;
}
}
}
if (propVal != null) {
// Recursive invocation, parsing placeholders contained in the
// previously resolved placeholder value.
propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);
if (logger.isTraceEnabled()) {
logger.trace("Resolved placeholder '" + placeholder + "'");
}
startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length());
}
else if (this.ignoreUnresolvablePlaceholders) {
// Proceed with unprocessed value.
startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
}
else {
throw new IllegalArgumentException("Could not resolve placeholder '" +
placeholder + "'" + " in value \"" + value + "\"");
}
visitedPlaceholders.remove(originalPlaceholder);
}
else {
startIndex = -1;
}
}
return result.toString();
}
然后再看下listeners.environmentPrepared(environment),广播环境准备完毕事件,拿到监听器列表:
ConfigFileApplicationListener
首先加载所有spring.factores中配置的EnvironmentPostProcessor实现类,并且ConfigFileApplicationListener自身也是它的实现类,然后对这些实现类进行排序,依次执行所有后置处理器。说2个比较重要的处理器:
SpringApplicationJsonEnvironmentPostProcessor,它用来处理属性源中的spring.application.json和SPRING_APPLICATION_JSON的json类型的value,并把它提取出来,作为单独的属性源放到环境属性源中,比如我在命令行中配置了"--spring.application.json={key1:value1, key2:value2}",稍后在使用的时候我们可以直接使用${key1}或者直接get Key1来获取对应的value1。
另一个比较重要的就是ConfigFileApplicationListener本身,它从spring.factories加载PropertySourceLoader实例,来解析我们的application.properties和application.yml。
AnsiOutputApplicationListener
根据spring.output.ansi.enabled的值决定是否开启AnsiOutput,以及控制台的配置spring.output.ansi.console-available。
LoggingApplicationListener
日志框架的初始化逻辑,logging.config指定的配置文件将在这里处理。
ClasspathLoggingApplicationListener
这里只是打印了classpath,其他什么也没有做。
BackgroundPreinitializer
这里也什么都没做。
DelegatingApplicationListener
这里也维护了一个广播器SimpleApplicationEventMulticaster,当DelegatingApplicationListener收到应用环境准备完毕事件时,实例化在配置项context.listener.classes指定的监听器,并进行广播当前事件。不过我们默认没有配,所以也啥都没有做。
FileEncodingApplicationListener
这里判断了spring.mandatory-file-encoding如果不为空,并且和file.encoding不相同,则抛出异常。
接来下看一下attach方法,它创建了一个新的属性源,名为configurationProperties,并且添加到了第一个位置,然后又把所有的属性源引用放到了configurationProperties里,形成了一个不断循环的结构:
关于Bind API,大家可以在这里查看介绍,它相比以前getProperty的方式提供了更多的定制化内容。
https://spring.io/blog/2018/03/28/property-binding-in-spring-boot-2-0
关于配置的优先级:
这是一个相当重要的特性。其实从之前我们就应该留意到了添加属性源时的api:addFirst,addBefore,addAfter等。
首先看一下AbstractEnvironment的属性:
private final MutablePropertySources propertySources = new MutablePropertySources();
private final ConfigurablePropertyResolver propertyResolver = new PropertySourcesPropertyResolver(this.propertySources);
具体get key的过程其实委托给了上文提到的PropertySourcesPropertyResolver的getProperty方法,方法中的propertySources就是这里构造方法中传入的属性源列表。get的时候,对propertySources进行遍历,匹配到了,方法就直接结束了,所以属性源在propertySources中的位置越靠前,属性的优先级也就越高。
这里还有一个注意的点,那就是上文提到的循环依赖的结构,按正常的遍历肯定是会无限循环下去的,这样getProperty就会有问题,spring在这里进行了排除:
SpringConfigurationPropertySources.java :
private boolean isIgnored(PropertySource<?> candidate) {
return (candidate instanceof StubPropertySource
|| candidate instanceof ConfigurationPropertySourcesPropertySource);
}
当不记得优先级时,直接debug看一下sources中的先后顺序即可。
到这里,环境对象已经准备完毕,下一篇将开始容器创建过程。