SpringBoot(基于2.3.0.RELEASE)源码分析(一)

写在前面:

整整一年多没写博客了,这一年事情有点多,没腾出时间来,希望后面能坚持下去,想把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中的先后顺序即可。

到这里,环境对象已经准备完毕,下一篇将开始容器创建过程。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值