這些不知道,別說你熟悉 Spring

2022-10-09 21:02:32

大家好,這篇文章跟大家來聊下 Spring 中提供的常用擴充套件點、Spring SPI 機制、以及 SpringBoot 自動裝配原理,重點介紹下 Spring 基於這些擴充套件點怎麼跟設定中心(Apollo、Nacos、Zookeeper、Consul)等做整合。

寫在前面

我們大多數 Java 程式設計師的日常工作基本都是在做業務開發,俗稱 crudboy。

作為 crudboy 的你有沒有這些煩惱呢?

  1. 隨著業務的迭代,新功能的加入,程式碼變得越來越臃腫,可維護性越來越低,慢慢變成了屎山

  2. 遇到一些框架層的問題不知道怎麼解決

  3. 面試被問到使用的框架、中介軟體原理、原始碼層東西,不知道怎麼回答

  4. 寫了 5 年程式碼了,感覺自己的技術沒有理想的長進

如果你有上述這些煩惱,我想看優秀框架的原始碼會是一個很好的提升方式。通過看原始碼,我們能學到業界大佬們優秀的設計理念、編碼風格、設計模式的使用、高效資料結構演演算法的使用、魔鬼細節的巧妙應用等等。這些東西都是助力我們成為一個優秀工程師不可或缺的。

如果你打算要看原始碼了,優先推薦 Spring、Netty、Mybatis、JUC 包。

Spring 擴充套件

我們知道 Spring 提供了很多的擴充套件點,第三方框架整合 Spring 其實大多也都是基於這些擴充套件點來做的。所以熟練的掌握 Spring 擴充套件能讓我們在閱讀原始碼的時候能快速的找到入口,然後斷點偵錯,一步步深入框架核心。

這些擴充套件包括但不限於以下介面:

BeanFactoryPostProcessor:在 Bean 範例化之前對 BeanDefinition 進行修改

BeanPostProcessor:在 Bean 初始化前後對 Bean 進行一些修改包裝增強,比如返回代理物件

Aware:一個標記介面,實現該介面及子介面的類會收到 Spring 的通知回撥,賦予某種 Spring 框架的能力,比如 ApplicationContextAware、EnvironmentAware 等

ApplicationContextInitializer:在上下文準備階段,容器重新整理之前做一些初始化工作,比如我們常用的設定中心 client 基本都是繼承該初始化器,在容器重新整理前將設定從遠端拉到本地,然後封裝成 PropertySource 放到 Environment 中供使用

ApplicationListener:Spring 事件機制,監聽特定的應用事件(ApplicationEvent),觀察者模式的一種實現

FactoryBean:用來自定義 Bean 的建立邏輯(Mybatis、Feign 等等)

ImportBeanDefinitionRegistrar:定義@EnableXXX 註解,在註解上 Import 了一個 ImportBeanDefinitionRegistrar,實現註冊 BeanDefinition 到容器中

InitializingBean:在 Bean 初始化時會呼叫執行一些初始化邏輯

ApplicationRunner/CommandLineRunner:容器啟動後回撥,執行一些初始化工作

上述列出了幾個比較常用的介面,但是 Spring 擴充套件遠不於此,還有很多擴充套件介面大家可以自己去了解。

Spring SPI 機制

在講接下來內容之前,我們先說下 Spring 中的 SPI 機制。Spring 中的 SPI 主要是利用 META-INF/spring.factories 檔案來實現的,檔案內容由多個 k = list(v) 的格式組成,比如:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.dtp.starter.adapter.dubbo.autoconfigure.ApacheDubboTpAutoConfiguration,\
  com.dtp.starter.adapter.dubbo.autoconfigure.AlibabaDubboTpAutoConfiguration

org.springframework.boot.env.EnvironmentPostProcessor=\
  com.dtp.starter.zookeeper.autoconfigure.ZkConfigEnvironmentProcessor

這些 spring.factories 檔案可能是位於多個 jar 包中,Spring 容器啟動時會通過 ClassLoader.getResources() 獲取這些 spring.factories 檔案的全路徑。然後遍歷路徑以位元組流的形式讀取所有的 k = list(v) 封裝到到一個 Map 中,key 為介面全限定類名,value 為所有實現類的全限定類名列表。

上述說的這些載入操作都封裝在 SpringFactoriesLoader 類裡。該類很簡單,提供三個載入方法、一個範例化方法,還有一個 cache 屬性,首次載入到的資料會儲存在 cache 裡,供後續使用。

SpringBoot 核心要點

上面講的 SPI 其實就是我們 SpringBoot 自動裝配的核心。

何為自動裝配?

自動裝配對應的就是手動裝配,在沒 SpringBoot 之前,我們使用 Spring 就是用的手動裝配模式。在使用某項第三方功能時,我們需要引入該功能依賴的所有包,並測試保證這些引入包版本相容。然後在 XML 檔案裡進行大量標籤設定,非常繁瑣。後來 Spring4 裡引入了 JavaConfig 功能,利用 @Configuration + @Bean 來代替 XML 設定,雖然對開發來說是友好了許多,但是這些模板式設定程式碼還是很繁瑣,會浪費大量時間做設定。Java 重可能也就是這個時候給人留的一種印象。

在該背景下出現了 SpringBoot,SpringBoot 可以說是穩住了 Java 的地位。SpringBoot 提供了自動裝配功能,自動裝配簡單來說就是將某種功能(如 web 相關、redis 相關、logging 相關等)打包在一起,統一管理依賴包版本,並且約定好相關功能 Bean 的裝配規則,使用者只需引入一個依賴,通過少量註解或簡單設定就可以使用第三方元件提供的功能了。

在 SpringBoot 中這類功能元件有一個好聽的名字叫做 starter。比如 spring-boot-starter-web、spring-boot-starter-data-redis、spring-boot-starter-logging 等。starter 裡會通過 @Configuration + @Bean + @ConditionalOnXXX 等註解定義要注入 Spring 中的 Bean,然後在 spring.factories 檔案中設定為 org.springframework.boot.autoconfigure.EnableAutoConfiguration 的實現,就可以完成自動裝配了。

具體裝配流程怎麼樣的呢?

其實也很簡單,基本都是 Spring 中的知識,沒啥新穎的。主要依託於@EnableAutoConfiguration 註解,該註解上會 Import 一個 AutoConfigurationImportSelector,看下繼承關係,該類繼承於 DeferredImportSelector。

主要方法為 getAutoConfigurationEntry()

	protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
      // 1
      if (!isEnabled(annotationMetadata)) {
        return EMPTY_ENTRY;
      }
      AnnotationAttributes attributes = getAttributes(annotationMetadata);
      // 2
      List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
      configurations = removeDuplicates(configurations);
      // 3
      Set<String> exclusions = getExclusions(annotationMetadata, attributes);
      checkExcludedClasses(configurations, exclusions);
      configurations.removeAll(exclusions);
      // 4
      configurations = getConfigurationClassFilter().filter(configurations);
      fireAutoConfigurationImportEvents(configurations, exclusions);
      return new AutoConfigurationEntry(configurations, exclusions);
	}

方法解讀

  1. 通過 spring.boot.enableautoconfiguration 設定項判斷是否啟用自動裝配,預設為 true

  2. 使用上述說的 SpringFactoriesLoader.loadFactoryNames() 載入所有 org.springframework.boot.autoconfigure.EnableAutoConfiguration 的實現類的全限定類名,藉助 HashSet 進行去重

  3. 獲取 @EnableAutoConfiguration 註解上設定的要 exclude 的類,然後排除這些特定類

  4. 通過 @ConditionalOnXXX 進行過濾,滿足條件的類才會留下,封裝到 AutoConfigurationEntry 裡返回

那 getAutoConfigurationEntry() 方法在哪兒呼叫呢?

public void refresh() throws BeansException, IllegalStateException {
				// Allows post-processing of the bean factory in context subclasses.
				postProcessBeanFactory(beanFactory);

				StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
				// Invoke factory processors registered as beans in the context.
				invokeBeanFactoryPostProcessors(beanFactory);

				// Register bean processors that intercept bean creation.
				registerBeanPostProcessors(beanFactory);
				beanPostProcess.end();

				// Initialize message source for this context.
				initMessageSource();

				// Initialize event multicaster for this context.
				initApplicationEventMulticaster();

				// Initialize other special beans in specific context subclasses.
				onRefresh();

				// Check for listener beans and register them.
				registerListeners();

				// Instantiate all remaining (non-lazy-init) singletons.
				finishBeanFactoryInitialization(beanFactory);

				// Last step: publish corresponding event.
				finishRefresh();
	}

以上是 Spring 容器重新整理時的幾個關鍵步驟,在步驟二 invokeBeanFactoryPostProcessors() 中會呼叫所有已經註冊的 BeanFactoryPostProcessor 進行處理。此處呼叫也是有順序的,優先會呼叫所有 BeanDefinitionRegistryPostProcessor#postProcessBeanDefinitionRegistry(),BeanDefinitionRegistryPostProcessor 是一個特殊的 BeanFactoryPostProcessor,然後再呼叫所有 BeanFactoryPostProcessor#postProcessBeanFactory()。

ConfigurationClassPostProcessor 是 BeanDefinitionRegistryPostProcessor 的一個實現類,該類主要用來處理 @Configuration 註解標註的類。我們用 @Configuration 標註的類會被 ConfigurationClassParser 解析包裝成 ConfigurationClass 物件,然後再呼叫 ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForConfigurationClass() 進行 BeanDefination 的註冊。

其中 ConfigurationClassParser 解析時會遞迴處理源設定類上的註解(@PropertySource、@ComponentScan、@Import、@ImportResource)、 @Bean 標註的方法、介面上的 default 方法,進行 ConfigurationClass 類的補全填充,同時如果該設定類有父類別,同樣會遞迴進行處理。具體程式碼請看 ConfigurationClassParser#doProcessConfigurationClass() 方法

protected final SourceClass doProcessConfigurationClass(
			ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
			throws IOException {
      
		// Process any @PropertySource annotations

		// Process any @ComponentScan annotations

		// Process any @Import annotations
		processImports(configClass, sourceClass, getImports(sourceClass), filter, true);

		// Process any @ImportResource annotations

		// Process individual @Bean methods
		Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
		for (MethodMetadata methodMetadata : beanMethods) {
			 configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
		}

		// Process default methods on interfaces
		processInterfaces(configClass, sourceClass);

		// Process superclass, if any
		if (sourceClass.getMetadata().hasSuperClass()) {
			 String superclass = sourceClass.getMetadata().getSuperClassName();
			 if (superclass != null && !superclass.startsWith("java") &&
					!this.knownSuperclasses.containsKey(superclass)) {
				  this.knownSuperclasses.put(superclass, configClass);
			  	// Superclass found, return its annotation metadata and recurse
				  return sourceClass.getSuperClass();
			}
		}

		// No superclass -> processing is complete
		return null;
	}

1)parser.parse(candidates) 解析得到完整的 ConfigurationClass 物件,主要填充下圖框中的四部分。

2)this.reader.loadBeanDefinitions(configClasses) 根據框中的四部分進行 BeanDefination 的註冊。

在上述 processImports() 過程中會將 DeferredImportSelector 的實現類放在 deferredImportSelectorHandler 中以便延遲到所有的解析工作完成後進行處理。deferredImportSelectorHandler 中就存放了 AutoConfigurationImportSelector 類的範例。process() 方法裡經過幾步走會呼叫到 AutoConfigurationImportSelector#getAutoConfigurationEntry() 方法上獲取到自動裝配需要的類,然後進行與上述同樣的 ConfigurationClass 解析封裝工作。

程式碼層次太深,呼叫太複雜,建議自己斷點偵錯原始碼跟一遍印象會更深刻。

ApplicationContextInitializer 呼叫時機

我們就以 SpringBoot 專案為例來看,在 SpringApplication 的建構函式中會進行 ApplicationContextInitializer 的初始化。

上圖中的 getSpringFactoriesInstances 方法內部其實就是呼叫 SpringFactoriesLoader.loadFactoryNames 獲取所有 ApplicationContextInitializer 介面的實現類,然後反射建立物件,並對這些物件進行排序(實現了 Ordered 介面或者加了 @Order 註解)。

	private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
      ClassLoader classLoader = getClassLoader();
      // Use names and ensure unique to protect against duplicates
      Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
      List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
      AnnotationAwareOrderComparator.sort(instances);
      return instances;
	}

至此,專案中所有 ApplicationContextInitializer 的實現已經載入並且建立好了。在 prepareContext 階段會進行所有已註冊的 ApplicationContextInitializer#initialize() 方法的呼叫。在此之前prepareEnvironment 階段已經準備好了環境資訊,此處接入設定中心就可以拉到遠端設定資訊然後填充到 Spring 環境中供應用使用。

SpringBoot 整合 Apollo

ApolloApplicationContextInitializer 實現 ApplicationContextInitializer 介面,並且在 spring.factories 檔案中設定如下

org.springframework.context.ApplicationContextInitializer=\
com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer

initialize() 方法中會根據 apollo.bootstrap.namespaces 設定的 namespaces 進行設定的拉去,拉去到的設定會封裝成 ConfigPropertySource 新增到 Spring 環境 ConfigurableEnvironment 中。具體的拉去流程就不展開講了,感興趣的可以自己去閱讀原始碼瞭解。

SpringCloud 整合 Nacos、Zk、Consul

在 SpringCloud 場景下,SpringCloud 規範中提供了 PropertySourceBootstrapConfiguration 繼承 ApplicationContextInitializer,另外還提供了個 PropertySourceLocator,二者配合完成設定中心的接入。

initialize 方法根據注入的 PropertySourceLocator 進行設定的定位獲取,獲取到的設定封裝成 PropertySource 物件,然後新增到 Spring 環境 Environment 中。

Nacos、Zookeeper、Consul 都有提供相應 PropertySourceLocator 的實現

我們來分析下 Nacos 提供的 NacosPropertySourceLocator,locate 方法只提取了主要流程程式碼,可以看到 Nacos 啟動會載入以下三種組態檔,也就是我們在 bootstrap.yml 檔案裡設定的擴充套件設定 extension-configs、共用設定 shared-configs 以及應用自己的設定,載入到組態檔後會封裝成 NacosPropertySource 放到 Spring 的 Environment 中。

public PropertySource<?> locate(Environment env) {
		 loadSharedConfiguration(composite);
		 loadExtConfiguration(composite);
		 loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);
		 return composite;
	}

loadApplicationConfiguration 載入應用設定時,同時會載入以下三種設定,分別是

  1. 不帶擴充套件名字尾,application

  2. 帶擴充套件名字尾,application.yml

  3. 帶環境,帶擴充套件名字尾,application-prod.yml

並且從上到下,優先順序依次增高

private void loadApplicationConfiguration(
			CompositePropertySource compositePropertySource, String dataIdPrefix,
			NacosConfigProperties properties, Environment environment) {
		String fileExtension = properties.getFileExtension();
		String nacosGroup = properties.getGroup();
		// load directly once by default
		loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup,
				fileExtension, true);
		// load with suffix, which have a higher priority than the default
		loadNacosDataIfPresent(compositePropertySource,
				dataIdPrefix + DOT + fileExtension, nacosGroup, fileExtension, true);
		// Loaded with profile, which have a higher priority than the suffix
		for (String profile : environment.getActiveProfiles()) {
			String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;
			loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup,
					fileExtension, true);
		}
	}

載入過程中,通過 namespace, dataId, group 唯一定位一個組態檔

  1. 首先獲取本地快取的設定,如果有直接返回

  2. 如果步驟1從本地沒找到相應組態檔,開始從遠處拉去,Nacos 2.0 以上版本使用 Grpc 協定進行遠端通訊,1.0 及以下使用 Http 協定進行遠端通訊

  3. 對拉去到的字串進行解析,封裝成 NacosPropertySource 返回

具體細節就不展開講了,可以自己看原始碼瞭解

Zookeeper、Consul 的接入也是非常簡單,可以自己分析一遍。如果我們有自研的設定中心,需要在 SpringCloud 環境下使用,可以根據 SpringCloud 提供的這些擴充套件參考以上幾種實現快速的寫個 starter 進行接入。

總結

本篇文章主要講了下 Spring SPI 機制、SpringBoot 自動裝配原理,以及擴充套件點 ApplicationContextInitializer 在整合設定中心時的應用。篇幅有限,一些具體程式碼細節就沒展開講了,以後會出些文章針對某一個點進行詳細講解。

個人開源專案

DynamicTp 是一個基於設定中心實現的輕量級動態執行緒池管理工具,主要功能可以總結為動態調參、通知報警、執行監控、三方包執行緒池管理等幾大類。

目前累計 2k star,程式碼優雅,使用了大量設計模式,如果你覺得看這些大型框架原始碼費勁,那麼可以嘗試從 DynamicTp 原始碼入手,歡迎大家瞭解試用

官網https://dynamictp.cn

gitee地址https://gitee.com/dromara/dynamic-tp

github地址https://github.com/dromara/dynamic-tp