源碼剖析Spring依賴註入:今天你還不會,你就輸了

来源:https://www.cnblogs.com/guoxiaoyu/p/17963111
-Advertisement-
Play Games

今天我們主要講解的是Spring依賴註入。在本文中,我們主要圍繞bean填充屬性的欄位和setter方法展開討論。要記住的是,在進行屬性註入時,我們首先需要找到註入點併進行緩存,然後才會真正進行屬性註入。需要註意的是,靜態欄位或方法是不會進行依賴註入的。最後,我們簡單地介紹了一下關鍵源碼,以及對@R... ...


在之前的講解中,我樂意將源碼拿出來並粘貼在文章中,讓大家看一下。然而,我最近意識到這樣做不僅會占用很多篇幅,而且實際作用很小,因為大部分人不會花太多時間去閱讀源碼。

因此,從今天開始,我將採取以下幾個步驟:首先,我會提前畫出一張圖來展示本章節要講解的內容的調用鏈路,供大家參考。其次,在文章中,我只會展示最核心的代碼或關鍵的類。剩下的內容將主要用來講解原理。如果你真的在學習Spring源碼,我希望你能打開你的項目,並跟著我一起深入閱讀源碼。現在,讓我們開始吧。今天的重點是Spring的依賴註入。

基本使用

首先,值得註意的是,在Spring框架中,依賴註入是在bean生成後進行屬性賦值的。由於我們的bean通常都是單例模式,所以每個類的屬性都必須進行註入。在這個過程中,會涉及到代理、反射等技術的應用。如果你對這些概念不太熟悉的話,建議你提前補充一下相關的前提知識。瞭解這些基本概念將有助於你更好地理解和掌握Spring框架的依賴註入機制。

首先需要註意的是,儘管圖示可能只展示了類之間的簡單調用關係,但這並不代表實際的依賴註入過程就是如此簡單。實際上,Spring框架的版本和配置方式可能會導致不同的鏈路調用。然而,無論具體的版本差異如何,Spring框架的依賴註入機制的基本邏輯大致是一樣的。

本節課的鏈路調用圖例地址:https://viewer.diagrams.net/index.html?tags={}&highlight=0000ff&edit=_blank&layers=1&nav=1&title=未命名繪圖.drawio#Uhttps%3A%2F%2Fraw.githubusercontent.com%2FStudiousXiaoYu%2Fdraw%2Fmain%2F未命名繪圖.drawio

Spring的依賴註入有兩種方式:手動註入、自動註入。下麵我們詳細講解一下這兩種方式。

手動註入

在手動註入中,離不開XML配置。有兩種常見的方式可以實現手動註入:通過屬性和通過構造器。手動就是我們人為控制註入的值,下麵是兩種配置方式:

<bean id="user" class="com.xiaoyu.service.UserService" >
		<property name="orderService" ref="orderService"/>
</bean>

上面是通過使用set方法進行依賴註入的方式來實現。

<bean id="user" class="com.xiaoyu.service.UserService">
		<constructor-arg index="0" ref="orderService"/>
</bean>

上面是通過使用構造方法進行依賴註入的方式來實現。

自動註入

XML配置

XML也有自動分配的機制,只要不是我們手動指定註入類,那就是自動註入,讓我們一起瞭解如何進行設置。

在XML中,我們可以通過在定義一個Bean時指定自動註入模式來進行優化。這些模式包括byType、byName、constructor、default和no。通過使用這些模式,我們可以更靈活地控制Bean的註入方式。

<bean id="user" class="com.xiaoyu.service.UserService" autowire="byType"/>
<bean id="user" class="com.xiaoyu.service.UserService" autowire="byName"/>

剩下的不舉例了,這兩種類型,都需要我們的UserService對象有相應的set方法。因為註入的點就是先找到set方法,然後在填充屬性之前,Spring會去解析當前類,把當前類的所有方法都解析出來。Spring會解析每個方法,得到對應的PropertyDescriptor對象。PropertyDescriptor對象中包含了幾個屬性:

name:獲取截取後的方法名稱:截取規則如下:

  • get開頭,則去除get,比如“getXXX”,那麼name=XXX(首字母小寫),需無參或者第一個參數為int類型
  • is開頭不並且返回值為boolean類型,比如“isXXX”,那麼name=XXX(首字母小寫),需無參
  • set開頭並且有無返回值,比如“setXXX”,那麼name=XXX(首字母小寫),前提是得有入參,如果無入參是解析不到set開頭的方法的

readMethodRef:如果是get開頭或者is開頭的方法,都是readMethodRef,並且存儲的引用。

readMethodName:是get開頭或者is開頭的方法名。包含get/is

writeMethodRef:set開頭的方法引用。

writeMethodName:set開頭的方法名,包含set。

propertyTypeRef:如果是讀方法,則獲取的是返回值類型,如果是set寫方法,則獲取的是入參類型。

具體實現可自行查看源碼:java.beans.Introspector#getTargetPropertyInfo()

@Autowired註解

這個註解大家都很熟悉,我簡單介紹一下它的基礎用法。最後,通過查看源碼,我們將依賴註入的過程完整地連起來。

屬性註入

基本用法示例:

@Component
public class UserService {
  @Autowired
	public OrderService orderService;
}

setter方法註入

基本用法示例:

@Component
public class UserService {

	public OrderService orderService;
	
	@Autowired
	public void setOrderService(OrderService orderService){
		System.out.println(0);
		this.orderService = orderService;
	}
}

構造器註入

基本用法示例:

@Component
public class UserService {

	public OrderService orderService;
	
	@Autowired
	public UserService(OrderService orderService){
		this.orderService = orderService;
	}
	
}

依賴註入關鍵源碼解析

尋找註入點

在創建一個Bean的過程中,Spring會利用AutowiredAnnotationBeanPostProcessor的postProcessMergedBeanDefinition()方法來找出註入點併進行緩存。具體的找註入點的流程如下:

  1. 如果一個Bean的類型是String,那麼則根本不需要進行依賴註入
  2. 遍歷目標類中的所有Field欄位,field上是否存在@Autowired、@Value、@Inject中的其中一個
  3. static 欄位不是註入點,不會進行自動註入
  4. 構造註入點,獲取@Autowired中的required屬性的值,將欄位封裝到AutowiredFieldElement對象。
  5. 遍歷目標類中的所有Method方法。
  6. method上是否存在@Autowired、@Value、@Inject中的其中一個
  7. static method不是註入點,不會進行自動註入
  8. set方法最好有入參,沒有入參或提示日誌。
  9. 構造註入點,獲取@Autowired中的required屬性的值,將方法封裝到AutowiredMethodElement對象。
  10. 查看是否還有父類,如果有再次迴圈直到沒有父類。
  11. 將剛纔構造好的註入點全都封裝到InjectionMetadata,作為當前Bean對於的註入點集合對象,並緩存。

static欄位或方法為什麼不支持註入

在源碼中,Spring會判斷欄位或方法是否是static來決定是否進行註入。如果欄位或方法是static的,Spring不會進行註入操作。這是因為靜態欄位或方法是屬於類的,而不是屬於具體的實例。因此,在進行依賴註入時,Spring會註入給具體的實例,而不是整個類。

我們知道Spring是支持創建原型bean的,也就是多例模式。

@Component
@Scope("prototype")
public class UserService {
  @Autowired
  private static OrderService orderService;
  public void test() {
  System.out.println("test123");
  }
}

確實,如果OrderService是prototype類型的,並且Spring支持註入static欄位,那麼每次註入OrderService到UserService時都會創建一個新的實例。這樣做確實違背了static欄位的本意,因為static欄位是屬於類的,而不是實例的。

註入點註入

在依賴註入的過程中,註入點的註入肯定會在populateBean方法中進行屬性註入。在這個過程中,會調用AutowiredAnnotationBeanPostProcessor的postProcessProperties()方法,該方法會直接給對象中的屬性賦值。這個方法會遍歷每個註入點(InjectedElement),併進行依賴註入操作。

image

屬性欄位註入

  1. 遍歷所有AutowiredFieldElement對象。
  2. 將對應的欄位封裝到DependencyDescriptor。
  3. 調用beanFactory.resolveDependency來獲取真正需要註入的bean。
  4. 最後將此次封裝的DependencyDescriptor和beanname緩存起來,主要考慮到了原型bean的創建
  5. 利用反射給filed賦值

setter方法註入

  1. 遍歷所有AutowiredMethodElement對象。
  2. 調用resolveMethodArguments方法
  3. 遍歷每個方法參數,找到匹配的bean對象,將方法對象封裝到DependencyDescriptor中。
  4. 調用beanFactory.resolveDependency來獲取真正需要註入的bean。
  5. 最後將此次封裝的DependencyDescriptor和beanname緩存起來,主要考慮到了原型bean的創建
  6. 利用反射給filed賦值

我們只需要關註findAutowiringMetadata方法的實現,因為大家普遍瞭解註入的概念。我們主要關註的是它是如何找到註入點的。

	private InjectionMetadata findAutowiringMetadata(String beanName, Class<?> clazz, @Nullable PropertyValues pvs) {
		// Fall back to class name as cache key, for backwards compatibility with custom callers.
		String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());
		// Quick check on the concurrent map first, with minimal locking.
		InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey);
		if (InjectionMetadata.needsRefresh(metadata, clazz)) {
			synchronized (this.injectionMetadataCache) {
				metadata = this.injectionMetadataCache.get(cacheKey);
				if (InjectionMetadata.needsRefresh(metadata, clazz)) {
					if (metadata != null) {
						metadata.clear(pvs);
					}
					// 解析註入點並緩存
					metadata = buildAutowiringMetadata(clazz);
					this.injectionMetadataCache.put(cacheKey, metadata);
				}
			}
		}
		return metadata;
	}
	private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {
		// 如果一個Bean的類型是String...,那麼則根本不需要進行依賴註入
		if (!AnnotationUtils.isCandidateClass(clazz, this.autowiredAnnotationTypes)) {
			return InjectionMetadata.EMPTY;
		}

		List<InjectionMetadata.InjectedElement> elements = new ArrayList<>();
		Class<?> targetClass = clazz;

		do {
			final List<InjectionMetadata.InjectedElement> currElements = new ArrayList<>();

			// 遍歷targetClass中的所有Field
			ReflectionUtils.doWithLocalFields(targetClass, field -> {
				// field上是否存在@Autowired、@Value、@Inject中的其中一個
				MergedAnnotation<?> ann = findAutowiredAnnotation(field);
				if (ann != null) {
					// static filed不是註入點,不會進行自動註入
					if (Modifier.isStatic(field.getModifiers())) {
						if (logger.isInfoEnabled()) {
							logger.info("Autowired annotation is not supported on static fields: " + field);
						}
						return;
					}

					// 構造註入點
					boolean required = determineRequiredStatus(ann);
					currElements.add(new AutowiredFieldElement(field, required));
				}
			});

			// 遍歷targetClass中的所有Method
			ReflectionUtils.doWithLocalMethods(targetClass, method -> {

				Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
				if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
					return;
				}
				// method上是否存在@Autowired、@Value、@Inject中的其中一個
				MergedAnnotation<?> ann = findAutowiredAnnotation(bridgedMethod);
				if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
					// static method不是註入點,不會進行自動註入
					if (Modifier.isStatic(method.getModifiers())) {
						if (logger.isInfoEnabled()) {
							logger.info("Autowired annotation is not supported on static methods: " + method);
						}
						return;
					}
					// set方法最好有入參
					if (method.getParameterCount() == 0) {
						if (logger.isInfoEnabled()) {
							logger.info("Autowired annotation should only be used on methods with parameters: " +
									method);
						}
					}
					boolean required = determineRequiredStatus(ann);
					PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
					currElements.add(new AutowiredMethodElement(method, required, pd));
				}
			});

			elements.addAll(0, currElements);
			targetClass = targetClass.getSuperclass();
		}
		while (targetClass != null && targetClass != Object.class);

		return InjectionMetadata.forElements(elements, clazz);
	}

@Resource

說到這裡,可能有些小伙伴還會使用@Resource註解來進行依賴註入。其實,這和@Autowired註解的邏輯是一樣的,只是調用的是其他類的相關方法。具體來說,通過org.springframework.context.annotation.CommonAnnotationBeanPostProcessor#postProcessMergedBeanDefinition方法來查找註入點,然後在org.springframework.context.annotation.CommonAnnotationBeanPostProcessor#postProcessProperties方法中進行屬性填充。關於這些細節我們就不詳細討論了,如果感興趣的話,可以查看一下源碼。

@Qualifier

對於使用過@Autowired註解的同學來說,他們肯定也瞭解@Qualifier註解的作用。@Qualifier主要用於解決一個介面有多個實現類的情況。為了更好地理解,我們來舉一個簡單的例子:

public interface User {
}
@Component
@Qualifier("userF")
public class UserF implements User{
}
@Component
@Qualifier("userM")
public class UserM implements User{
}

在上述內容中,簡要定義了兩個實現。現在我們需要使用它們。

@Component
public class UserService {

	@Autowired
	@Qualifier("userM")
	public User user;
}

在這種情況下,會去匹配userM的實體類,而不會出現多個匹配類導致異常。那麼它是如何解決這個問題的呢?它是在什麼時候找到@Qualifier註解的呢?具體的源碼如下所示:

	protected boolean checkQualifier(
			BeanDefinitionHolder bdHolder, Annotation annotation, TypeConverter typeConverter) {
		// 檢查某個Qualifier註解和某個BeanDefinition是否匹配

		// annotation是某個屬性或某個方法參數前上所使用的Qualifier
		Class<? extends Annotation> type = annotation.annotationType();
		RootBeanDefinition bd = (RootBeanDefinition) bdHolder.getBeanDefinition();

		// 首先判斷BeanDefinition有沒有指定類型的限定符
		AutowireCandidateQualifier qualifier = bd.getQualifier(type.getName());
		if (qualifier == null) {
			qualifier = bd.getQualifier(ClassUtils.getShortName(type));
		}
		if (qualifier == null) {
			// First, check annotation on qualified element, if any
			Annotation targetAnnotation = getQualifiedElementAnnotation(bd, type);
			// Then, check annotation on factory method, if applicable
			if (targetAnnotation == null) {
				targetAnnotation = getFactoryMethodAnnotation(bd, type);
			}
			if (targetAnnotation == null) {
				RootBeanDefinition dbd = getResolvedDecoratedDefinition(bd);
				if (dbd != null) {
					targetAnnotation = getFactoryMethodAnnotation(dbd, type);
				}
			}
			if (targetAnnotation == null) {
				// Look for matching annotation on the target class
				if (getBeanFactory() != null) {
					try {
						// 拿到某個BeanDefinition對應的類上的@Qualifier
						Class<?> beanType = getBeanFactory().getType(bdHolder.getBeanName());
						if (beanType != null) {
							targetAnnotation = AnnotationUtils.getAnnotation(ClassUtils.getUserClass(beanType), type);
						}
					}
					catch (NoSuchBeanDefinitionException ex) {
						// Not the usual case - simply forget about the type check...
					}
				}
				if (targetAnnotation == null && bd.hasBeanClass()) {
					targetAnnotation = AnnotationUtils.getAnnotation(ClassUtils.getUserClass(bd.getBeanClass()), type);
				}
			}
			// 註解對象的equals比較特殊,JDK層面用到了動態代理,會比較value
			if (targetAnnotation != null && targetAnnotation.equals(annotation)) {
				return true;
			}
		}
		......
		return true;
	}

他其實是在我們上面所說的屬性註入的時候去匹配查找的。具體來說,他會調用beanFactory.resolveDependency方法來獲取真正需要註入的bean時進行查找。如果想要查看相關的源碼,可以去查看org.springframework.beans.factory.annotation.QualifierAnnotationAutowireCandidateResolver#isAutowireCandidate方法。在這個方法中會有更詳細的解釋。

總結

今天我們主要講解的是Spring依賴註入。在本文中,我們主要圍繞bean填充屬性的欄位和setter方法展開討論。要記住的是,在進行屬性註入時,我們首先需要找到註入點併進行緩存,然後才會真正進行屬性註入。需要註意的是,靜態欄位或方法是不會進行依賴註入的。最後,我們簡單地介紹了一下關鍵源碼,以及對@Resource和@Qualifier進行了簡單的分析。如果想要學習Spring源碼,一定要結合圖例去理解,否則很容易暈頭轉向。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • TLDR 修飾變數的時候,可以把 constexpr 對象當作加強版的 const 對象:const 對象表明值不會改變,但不一定能夠在編譯期取得結果;constexpr 對象不僅值不會改變,而且保證能夠在編譯期取得結果。如果一個 const 變數能夠在編譯期求值,將其改為 constexpr 能夠 ...
  • 老周一般很少玩游戲,在某寶上買了一堆散件,計劃在過年期間自己做個機械臂耍耍。頭腦中划過一道紫藍色的閃電,想起用游戲手柄來控制機械臂。機械臂是由樹莓派(大草莓)負責控制,然後客戶端通過 Socket UDP 來發送信號。優先考慮在 PC 和手機上測試,就順便折騰一下 XInput API。當然,讀取手 ...
  • 有這樣一個帶有搜索功能的用戶界面需求: 搜索流程如下所示: 這個需求涉及兩個實體: “評分(Rating)、用戶名(Username)”數據與User實體相關 “創建日期(create date)、觀看次數(number of views)、標題(title)、正文(body)”與Story實體相關 ...
  • 常見內置數值類型 數值類型是不可變類型(immutable type),它包括布爾類型、整數、浮點數與複數。 類型 英文名 構造方式 對應關鍵字 構造函數 布爾 Boolean var = True bool bool() 整數 Integer var = 5 int int() 浮點數 Float ...
  • Python 常見內置數據類型 在Python中,常用的類型是這些: Python 中查看數據類型的函數(function)為type()。 >>>text = "Is test a string type object?" >>>print(type(text)) <class 'str'> Py ...
  • ServerCnxnFactory 用於接收客戶端連接、管理客戶端session、處理客戶端請求。 ServerCnxn抽象類 代表一個客戶端連接對象: 從網路讀寫數據 數據編解碼 將請求轉發給上層組件或者從上層組件接收響應 管理連接狀態,比如:enableRecv、sessionTimeout、s ...
  • 本文深入介紹了Java 8的Stream API,包括創建、中間操作、終端操作等,強調了並行流在大數據處理中的性能提升。提供清晰實用的示例,為讀者理解流式計算提供有益指導。 ...
  • 在之前的文章中,我們簡單的介紹了線程誕生的意義和基本概念,採用多線程的編程方式,能充分利用 CPU 資源,顯著的提升程式的執行效率。其中java.lang.Thread是 Java 實現多線程編程最核心的類,學習Thread類中的方法,是學習多線程的第一步。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...