(四)SpringBoot啟動過程的分析-預處理ApplicationContext

不會發芽的種子發表於2021-03-26

-- 以下內容均基於2.1.8.RELEASE版本

緊接著上一篇(三)SpringBoot啟動過程的分析-建立應用程式上下文,本文將分析上下文建立完畢之後的下一步操作:預處理上下文容器。

預處理上下文容器

預處理上下文容器由prepareContext()方法完成,本篇內容全部都是基於這個方法所涉及的內容進行分析。

// SpringApplication.java

private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {

	// 設定環境物件,傳入的物件是解析完畢profiles的物件,Context內部則是不完整的物件
	context.setEnvironment(environment);
	// 設定上下文引數
	postProcessApplicationContext(context);
	// 載入ApplicationContextInitializers
	applyInitializers(context);
	// 觸發開始準備上下文事件
	listeners.contextPrepared(context);
	if (this.logStartupInfo) {
		logStartupInfo(context.getParent() == null);
		logStartupProfileInfo(context);
	}
	// 將啟動引數包裝為名為springApplicationArguments的DefaultApplicationArguments物件,並以單例模式註冊
	ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
	beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
	
	// 設定列印的Banner
	if (printedBanner != null) {
		beanFactory.registerSingleton("springBootBanner", printedBanner);
	}
	
	// 設定是否允許覆蓋BeanDefinition
	if (beanFactory instanceof DefaultListableBeanFactory) {
		((DefaultListableBeanFactory) beanFactory).setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
	}
	
	// 載入資源
	Set<Object> sources = getAllSources();
	Assert.notEmpty(sources, "Sources must not be empty");
	load(context, sources.toArray(new Object[0]));
	
	// 觸發上下文載入完畢事件
	listeners.contextLoaded(context);
}

設定上下文引數

// SpringApplication.java

protected void postProcessApplicationContext(ConfigurableApplicationContext context) {

	// 以單例模式註冊beanNameGenerator
	if (this.beanNameGenerator != null) {
		context.getBeanFactory().registerSingleton(AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR, this.beanNameGenerator);
	}
	
	// 為上下文設定資源載入器和類載入器
	if (this.resourceLoader != null) {
		if (context instanceof GenericApplicationContext) {
			((GenericApplicationContext) context).setResourceLoader(this.resourceLoader);
		}
		if (context instanceof DefaultResourceLoader) {
			((DefaultResourceLoader) context).setClassLoader(this.resourceLoader.getClassLoader());
		}
	}
	
	// 為上下文設定轉換服務
	if (this.addConversionService) {
		context.getBeanFactory().setConversionService(ApplicationConversionService.getSharedInstance());
	}
}

將前面步驟初始化的屬性賦值給上下文容器,程式碼中的this代表的是SpringApplication。

載入ApplicationContextInitializers

在SpringApplication.run()的一開始它就通過SPI獲取到所有的ApplicationContextInitializers,在這裡他們將被執行。

protected void applyInitializers(ConfigurableApplicationContext context) {
	// 呼叫每一個實現類的initialize方法
	for (ApplicationContextInitializer initializer : getInitializers()) {
		Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(initializer.getClass(), ApplicationContextInitializer.class);
		Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
		initializer.initialize(context);
	}
}

// 將之前獲取到的集合進行排序並返回只讀集合
public Set<ApplicationContextInitializer<?>> getInitializers() {
	return asUnmodifiableOrderedSet(this.initializers);
}

DelegatingApplicationContextInitializer

用於初始化配置檔案(屬性名:context.initializer.classes)中指定的ApplicationContextInitializer實現類

public void initialize(ConfigurableApplicationContext context) {
	// 獲取環境物件
	ConfigurableEnvironment environment = context.getEnvironment();
	// 獲取context.initializer.classes屬性指定的實現類
	List<Class<?>> initializerClasses = getInitializerClasses(environment);
	if (!initializerClasses.isEmpty()) {
		// 呼叫其initialize()方法
		applyInitializerClasses(context, initializerClasses);
	}
}

private List<Class<?>> getInitializerClasses(ConfigurableEnvironment env) {
	// 通過指定屬性去獲取,此處常量屬性值為context.initializer.classes
	String classNames = env.getProperty(PROPERTY_NAME);
	List<Class<?>> classes = new ArrayList<>();
	if (StringUtils.hasLength(classNames)) {
		// 根據程式碼可以推斷出,context.initializer.classes的值可以用逗號拼接
		for (String className : StringUtils.tokenizeToStringArray(classNames, ",")) {
			classes.add(getInitializerClass(className));
		}
	}
	return classes;
}

這裡看起來好像很奇怪,本身當前的類就是一個ApplicationContextInitializer, 它已經被上游程式碼呼叫了initializer()方法,在initializer()方法中它又去獲取ApplicationContextInitializer,然後接著呼叫initializer(),好像很繞。不過它的類描述已經說明了問題,它用於載入從配置檔案指定
的那些ApplicationContextInitializer。如果閱讀過第一篇概覽SpringApplication.java #構造方法的話就會明白,當前物件就是在SpringApplication類的構造方法中通過SPI方式獲取到的,而當前方法則是通過配置檔案指定的方式
來獲取。由此就可以得出一個結論:在SpringBoot中實現ApplicationContextInitializer並確保其被載入有三種方法,一是通過SpringApplication公開的addInitializers()方法直接新增,二是以SPI方式配置,三是以配置檔案方式配置。

在SpringBoot中三種配置ApplicationContextInitializer的方法:

  1. 直接以程式碼方式新增
    new SpringApplication(Example.class).addInitializers(new 實現類());
  1. 以SPI方式
    在/META-INF/spring.factories檔案中配置org.springframework.context.ApplicationContextInitializer=實現類A, 實現類B
  1. 以配置檔案方式(語法取決於你使用什麼樣的配置檔案properties or xml or yml or yaml or 配置中心)
    context.initializer.classes = 實現類A, 實現類B

SharedMetadataReaderFactoryContextInitializer

在ConfigurationClassPostProcessor和Spring Boot之間建立共享的CachingMetadataReaderFactory, 幹啥的我也不知道,後續debug在研究。

ContextIdApplicationContextInitializer

用於設定SpringApplication.getId()的值,如果配置檔案中指定了spring.application.name,則本類不起作用,反之。

ConfigurationWarningsApplicationContextInitializer

新增BeanFactoryPostProcessor:ConfigurationWarningsPostProcessor用於列印配置錯誤的日誌

ServerPortInfoApplicationContextInitializer

用於設定server.ports屬性的值,它是Web伺服器實際監聽的應用程式埠。同時實現了ApplicationListener介面用於監聽WebServerInitializedEvent事件,WebServer初始化完畢之後設定埠。

// ServerPortInfoApplicationContextInitializer.java

// ①
public void initialize(ConfigurableApplicationContext applicationContext) {
	applicationContext.addApplicationListener(this);
}

// ②
public void onApplicationEvent(WebServerInitializedEvent event) {
	String propertyName = "local." + getName(event.getApplicationContext()) + ".port";
	setPortProperty(event.getApplicationContext(), propertyName, event.getWebServer().getPort());
}

// ③
private String getName(WebServerApplicationContext context) {
	String name = context.getServerNamespace();
	return StringUtils.hasText(name) ? name : "server";
}

// ④
private void setPortProperty(ApplicationContext context, String propertyName, int port) {
	if (context instanceof ConfigurableApplicationContext) {
		setPortProperty(((ConfigurableApplicationContext) context).getEnvironment(), propertyName, port);
	}
	if (context.getParent() != null) {
		setPortProperty(context.getParent(), propertyName, port);
	}
}

@SuppressWarnings("unchecked")
private void setPortProperty(ConfigurableEnvironment environment, String propertyName, int port) {
	MutablePropertySources sources = environment.getPropertySources();
	PropertySource<?> source = sources.get("server.ports");
	if (source == null) {
		source = new MapPropertySource("server.ports", new HashMap<>());
		sources.addFirst(source);
	}
	((Map<String, Object>) source.getSource()).put(propertyName, port);
}

① - 向上下文監聽器列表中新增當前類(作為監聽器加入)
② - 在監聽事件中設定埠,預設屬性為local.server.port
③ - 若設定了伺服器Namespace,則屬性值為local.Namespace的值.port
④ - 將監聽埠資訊填入server.ports屬性下

ConditionEvaluationReportLoggingListener

用於設定ConditionEvaluationReportListener監聽器,此監聽器監聽ContextRefreshedEvent和ApplicationFailedEvent,分別列印出上下文容器成功重新整理和失敗的日誌報告

在這一章節中,可以看到ApplicationContextInitializer擴充套件介面的實際應用。通過Spring框架內建的一些initializer可以實現框架功能,同理我們也可以利用這一特性在上下文容器還未重新整理之前做一些擴充套件功能。

釋出ApplicationContextInitializedEvent事件

由SpringApplicationRunListeners.contextPrepared(ConfigurableApplicationContext context)方法觸發。

當上下文容器載入完畢所有的ApplicationContextInitializer之後,觸發該事件,通知那些關注了此事件的監聽器進行下一步操作。每次釋出事件的時候可能已經有新的監聽器被加進來,在上一章節中我們就看到會在ApplicationContextInitializer裡面新增監聽器
但我們只需要關注那些監聽了當前事件的監聽器即可。這裡僅僅作為一個提示,監聽器可能在任何地方被加進來。這裡並未發現有監聽器監聽此事件。

記錄活動的Profiles日誌

日誌列印的:The following profiles are active: dev,test 這一句就來自於此。程式碼比較簡單一看便知。

if (this.logStartupInfo) {
	logStartupInfo(context.getParent() == null);
	logStartupProfileInfo(context);
}

protected void logStartupProfileInfo(ConfigurableApplicationContext context) {
	Log log = getApplicationLog();
	if (log.isInfoEnabled()) {
		String[] activeProfiles = context.getEnvironment().getActiveProfiles();
		if (ObjectUtils.isEmpty(activeProfiles)) {
			String[] defaultProfiles = context.getEnvironment().getDefaultProfiles();
			log.info("No active profile set, falling back to default profiles: " + StringUtils.arrayToCommaDelimitedString(defaultProfiles));
		}
		else {
			log.info("The following profiles are active: "+ StringUtils.arrayToCommaDelimitedString(activeProfiles));
		}
	}
}

註冊啟動SpringApplication時的引數

將引數物件註冊為單例模式

// SpringApplication.java

ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);

是否允許覆蓋Bean定義

這個是和spring.main.allow-bean-definition-overriding引數有關,預設情況下在DefaultListableBeanFactory中是true,但是SpringBoot在此處給設定成了false。

if (beanFactory instanceof DefaultListableBeanFactory) {
	((DefaultListableBeanFactory) beanFactory).setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}

載入Bean到上下文中

載入需要兩個必備條件:誰來載入Bean,從哪載入Bean;此處載入BeanDefinition是由BeanDefinitionLoader來完成。

// SpringApplication.java#prepareContext()

// 獲取當前應用要載入的資源
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
// 載入Bean
load(context, sources.toArray(new Object[0]));

// 建立Bean載入器,並載入
protected void load(ApplicationContext context, Object[] sources) {
	if (logger.isDebugEnabled()) {
		logger.debug("Loading source " + StringUtils.arrayToCommaDelimitedString(sources));
	}
	// 建立BeanDefinitionLoader
	BeanDefinitionLoader loader = createBeanDefinitionLoader(getBeanDefinitionRegistry(context), sources);
	if (this.beanNameGenerator != null) {
		loader.setBeanNameGenerator(this.beanNameGenerator);
	}
	if (this.resourceLoader != null) {
		loader.setResourceLoader(this.resourceLoader);
	}
	if (this.environment != null) {
		loader.setEnvironment(this.environment);
	}
	loader.load();
}



初始化BeanDefinitionLoader

用於建立BeanDefinitionLoader,並從source載入類,它是對載入Bean的整體功能做了封裝,內部由不同的資源載入類來完成不同型別的資源載入,例如從基於註解的類來開始載入,從xml檔案開始載入

// SpringApplication.java

protected BeanDefinitionLoader createBeanDefinitionLoader(BeanDefinitionRegistry registry, Object[] sources) {
	return new BeanDefinitionLoader(registry, sources);
}

class BeanDefinitionLoader {
	
	// 類的來源
	private final Object[] sources;

	// JavaConfig類讀取器
	private final AnnotatedBeanDefinitionReader annotatedReader;

	// xml檔案類讀取器
	private final XmlBeanDefinitionReader xmlReader;

	// groovy類讀取器
	private BeanDefinitionReader groovyReader;

	// classpath類讀取器
	private final ClassPathBeanDefinitionScanner scanner;

	// 資源載入器
	private ResourceLoader resourceLoader;

	BeanDefinitionLoader(BeanDefinitionRegistry registry, Object... sources) {
		Assert.notNull(registry, "Registry must not be null");
		Assert.notEmpty(sources, "Sources must not be empty");
		this.sources = sources;
		this.annotatedReader = new AnnotatedBeanDefinitionReader(registry);
		this.xmlReader = new XmlBeanDefinitionReader(registry);
		if (isGroovyPresent()) {
			this.groovyReader = new GroovyBeanDefinitionReader(registry);
		}
		this.scanner = new ClassPathBeanDefinitionScanner(registry);
		this.scanner.addExcludeFilter(new ClassExcludeFilter(sources));
	}
	// ...省略部分程式碼
}

通過觀察它的內部屬性和構造方法可以看出,它支援載入基於程式設計方式配置的類,支援xml檔案配置的類,支援groovy和xml混合定義的類的載入。它使用門面模式封裝了這些具體的載入器。此處只需要先知道BeanDefinitionLoader用於載入Bean,且它內部封裝了多個類讀取器即可
不必深入。先了解大體的功能性即可。後續會開單獨篇章介紹。

使用BeanDefinitionLoader載入Bean

// BeanDefinitionLoader.java

// ①
public int load() {
	int count = 0;
	for (Object source : this.sources) {
		count += load(source);
	}
	return count;
}

// ②
private int load(Object source) {
	Assert.notNull(source, "Source must not be null");
	if (source instanceof Class<?>) {
		return load((Class<?>) source);
	}
	if (source instanceof Resource) {
		return load((Resource) source);
	}
	if (source instanceof Package) {
		return load((Package) source);
	}
	if (source instanceof CharSequence) {
		return load((CharSequence) source);
	}
	throw new IllegalArgumentException("Invalid source type " + source.getClass());
}

① - 記錄載入的資源數量
② - 根據傳入的資源型別選擇不同的載入方式

此處我們選擇SpringBoot典型的資源載入方式Class方式來分析,在應用程式啟動入口我們使用的是SpringApplication.run(Example.class, args);而Example.class是我們的main函式所在類,以它作為資源來載入。

// BeanDefinitionLoader.java

private int load(Class<?> source) {
	// ①
	if (isGroovyPresent() && GroovyBeanDefinitionSource.class.isAssignableFrom(source)) {
		// Any GroovyLoaders added in beans{} DSL can contribute beans here
		GroovyBeanDefinitionSource loader = BeanUtils.instantiateClass(source, GroovyBeanDefinitionSource.class);
		load(loader);
	}
	// ②
	if (isComponent(source)) {
		this.annotatedReader.register(source);
		return 1;
	}
	return 0;
}

① - 判斷是否支援Groovy
② - 判斷是否包含@Component註解,如果包含才開始註冊

// BeanDefinitionLoader.java

private boolean isComponent(Class<?> type) {

	if (AnnotationUtils.findAnnotation(type, Component.class) != null) {
		return true;
	}
	if (type.getName().matches(".*\\$_.*closure.*") || type.isAnonymousClass() || type.getConstructors() == null || type.getConstructors().length == 0) {
		return false;
	}
	return true;
}

// AnnotationUtils.java

public static <A extends Annotation> A findAnnotation(Class<?> clazz, @Nullable Class<A> annotationType) {
		return findAnnotation(clazz, annotationType, true);
}


@SuppressWarnings("unchecked")
@Nullable
private static <A extends Annotation> A findAnnotation(Class<?> clazz, @Nullable Class<A> annotationType, boolean synthesize) {

	Assert.notNull(clazz, "Class must not be null");
	if (annotationType == null) {
		return null;
	}
	
	// ①
	AnnotationCacheKey cacheKey = new AnnotationCacheKey(clazz, annotationType);
	A result = (A) findAnnotationCache.get(cacheKey);
	if (result == null) {
		result = findAnnotation(clazz, annotationType, new HashSet<>());
		if (result != null && synthesize) {
			result = synthesizeAnnotation(result, clazz);
			findAnnotationCache.put(cacheKey, result);
		}
	}
	return result;
}

① - 從快取獲取被載入的資源類是否有@Component註解的快取

預設情況下是沒有快取,此時將會去查詢Example.class是否包含@Component註解。那在預設情況下我們的啟動類並未直接標明這個註解, 一個典型的SpringBoot Web應用如下:


@RestController
@SpringBootApplication
public class Example {

	@RequestMapping("/")
	String home() {
		return "Hello World!";
	}

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

	}

}

實際上@RestController和@SpringBootApplication兩個註解都包含了@Component註解。因此Example.class自然也就具有@Component註解,感興趣的可以親自點進去看看這兩個註解內部。更多細節將來會單獨分析。在確定了它支援@Component註解之後,開始載入Bean。

// BeanDefinitionLoader#load(Class<?> source)

this.annotatedReader.register(source);

// AnnotatedBeanDefinitionReader.java

<T> void doRegisterBean(Class<T> annotatedClass, @Nullable Supplier<T> instanceSupplier, @Nullable String name,
			@Nullable Class<? extends Annotation>[] qualifiers, BeanDefinitionCustomizer... definitionCustomizers) {

	// ①
	AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(annotatedClass);
	// ②
	if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) {
		return;
	}

	// ③
	abd.setInstanceSupplier(instanceSupplier);
	// ④
	ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd);
	abd.setScope(scopeMetadata.getScopeName());
	String beanName = (name != null ? name : this.beanNameGenerator.generateBeanName(abd, this.registry));
	// ⑤
	AnnotationConfigUtils.processCommonDefinitionAnnotations(abd);
	// ⑥
	if (qualifiers != null) {
		for (Class<? extends Annotation> qualifier : qualifiers) {
			if (Primary.class == qualifier) {
				abd.setPrimary(true);
			}
			else if (Lazy.class == qualifier) {
				abd.setLazyInit(true);
			}
			else {
				abd.addQualifier(new AutowireCandidateQualifier(qualifier));
			}
		}
	}
	// ⑦
	for (BeanDefinitionCustomizer customizer : definitionCustomizers) {
		customizer.customize(abd);
	}

	// ⑧
	BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd, beanName);
	definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
	BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry);
}

① - 建立一個帶註解後設資料的類定義。
② - 判斷是否有@Conditional註解,以確定是否滿足條件需要排除。
③ - 設定一個建立Bean的回撥,基本都是空值。
④ - 獲取當前Bean的作用域後設資料,判斷依據是是否包含@Scope註解。ScopeMetadata包含類的作用域描述(singleton | prototype)和代理模式描述ScopedProxyMode,預設為單例模式,不使用代理建立。
⑤ - 處理常用註解,包括@Lazy、@Primary、@Role、@Description、@DependsOn,他們將被設定到BeanDefinition的屬性當中。
⑥ - 檢查傳入的Qualifiers。
⑦ - 暫時不知道幹啥的,以後再說
⑧ - 確認好當前Bean的代理模式,並註冊。

在真正將一個類以BeanDefinition來註冊的時候需要把可能涉及到的一些特性全部都檢查一遍。此處只載入了我們的示例類Example.class,並未載入其他類。

釋出ApplicationPreparedEvent事件

由EventPublishingRunListener.contextLoaded(ConfigurableApplicationContext context)方法觸發。

在真正觸發事件之前,它處理了ApplicationContextAware實現,為其設定了上下文。

為ApplicationContextAware擴充套件介面設定上下文

public void contextLoaded(ConfigurableApplicationContext context) {
	for (ApplicationListener<?> listener : this.application.getListeners()) {
		if (listener instanceof ApplicationContextAware) {
			((ApplicationContextAware) listener).setApplicationContext(context);
		}
		// 將所有監聽器賦值給上下文
		context.addApplicationListener(listener);
	}
	this.initialMulticaster.multicastEvent(new ApplicationPreparedEvent(this.application, this.args, context));
}

ConfigFileApplicationListener

新增了一個BeanFactoryPostProcessor:PropertySourceOrderingPostProcessor,用於重排序defaultProperties

LoggingApplicationListener

以單例模式註冊註冊當前日誌系統

總結

在預處理上下文時將先前載入的一些屬性賦值給context,執行了ApplicationContextInitializers的實現類,為ApplicationContextAware介面填充context物件。

相關文章