屬性注入 vs 建構函式注入以及 @Required 的使用

工程 | Alef Arendsen | 2007 年 7 月 11 日 | ...

幾個月前,我們開始在 www.springframework.org 上釋出民意調查,邀請大家就 Spring、其部分特性以及如何使用這些特性提供反饋。我釋出的第一個問題是,大家是否檢查了必需的依賴項,如果檢查了,使用了什麼機制。我很快就這個問題繼續詢問社群使用了什麼事務管理策略。

令我高興的是,當我第一次檢視三月份的投票結果時,許多人在第一次投票中表示他們正在使用 @Required 註解。關於事務管理的第二次投票很快顯示,許多人正在使用 @Transactional 註解。您可以在下面找到關於檢查必需依賴項的部分投票結果。這些結果連同關於事務管理的投票(約 30% 的受訪者使用 @Transactional 註解來劃分事務邊界)一致表明,人們大量使用了 Spring 2.0,這對我們來說是個非常好的訊息。由於將使用 Spring 1.x 的應用程序升級到 Spring 2.0 應該不是問題,我們真心希望人們不要停留在 Spring 1.x 上,事實上,人們大規模地進行了升級。

您如何檢查必需的依賴項

8% 我在業務方法中檢查它們
9% 使用 init-method 和斷言機制(參閱 Assert)
9% 在 XML 中使用 dependency-check 屬性
13% 我不需要,我使用建構函式注入
15% 使用 InitializingBean 和斷言機制
17% 使用 Spring 2.0 @Required 註解
29% 我不檢查必需的依賴項

然而有趣的是,有 29% 的人沒有檢查必需的依賴項。在隨討論釋出的論壇帖中,出現了一些有趣的建議,說明為什麼有些人沒有這樣做以及人們如何透過其他方式解決這個問題。讓我們回顧其中的一些。

建構函式注入

我想先回顧一下建構函式注入。任何具有接受引數的建構函式的物件(顯然)不能在不傳入引數的情況下構建。在 Java 中,只要我們自己沒有新增建構函式,就會給類新增一個預設或隱式建構函式。這個預設或隱式建構函式不接受引數,因此只要您根本不新增帶引數的建構函式,或者專門新增一個不帶任何引數的建構函式,Spring(或您類的任何其他使用者)就可以在不傳入任何內容的情況下例項化您的類。

換句話說,我們可以強制我們類的一個使用者(同樣,這可能是 Spring,但也可能是直接例項化你的類的單元測試)在例項化時傳入引數。


public class Service {

  public Collaborator collaborator;

  // constructor with arguments, you *have* to
  // satisfy the argument to instantiate this class
  public Service(Collaborator collaborator) {
    this.collaborator = collaborator;
  }
}

當需要檢查必需的依賴項時,我們可以利用這一點。如果我們修改上面的程式碼示例以包含斷言,我們可以 100% 確定該類永遠不會在沒有注入其協作者的情況下例項化。


public Service(Collaborator collaborator) {
  if (collaborator == null) {
    throw new IllegalArgumentException("Collaborator cannot be null");
  }
  this.collaborator = collaborator;
}

換句話說,如果我們使用建構函式注入並結合我上面展示的斷言機制,我們就不需要依賴檢查機制。

為什麼大多數人不使用建構函式注入

當然現在的問題是,如果它是完成任務的最簡單方法,為什麼這麼少的人使用建構函式注入來強制必需的依賴項!這有兩個原因——一個更具歷史性,另一個則是 Spring Framework 本身的性質。

歷史原因

2003 年初,當 Spring 首次作為開源專案釋出時,它主要專注於 setter 注入。其他框架也開創了依賴注入的方法,其中之一是 PicoContainer,它強烈專注於建構函式注入。Spring 保持了對 setter 注入的關注,因為當時我們認為,建構函式引數缺乏預設引數和引數名稱會導致開發人員不夠清晰。然而,我們也實現了建構函式注入,以便能夠為那些希望例項化和管理非自己控制的物件提供該特性。

這就是為什麼您在 Spring Framework 本身中看到大量 setter 注入的原因之一。Spring 本身使用了 setter 注入的事實,以及我們主要倡導它,也導致許多第三方軟體開始使用 setter 注入,以及部落格和文章開始提及 setter 注入。

(順便問一下,大家還記得控制反轉的 1 類、2 類和 M 類嗎 ;-) )

框架需要更具可配置性

setter 注入比你預期的更常被使用的第二個原因是,像 Spring 這樣的框架通常更適合透過 setter 注入進行配置,而不是透過建構函式注入。這主要是因為需要配置的框架通常包含許多可選值。使用建構函式注入來配置可選值會導致不必要的混亂和建構函式氾濫,尤其是在與類繼承結合使用時。

正因為這兩個原因,我認為建構函式注入對於應用程式程式碼比對於框架程式碼更具可用性。在應用程式程式碼中,你天生對需要配置的可選值的需求較少(你的應用程式程式碼不太可能在許多情況下使用,這需要可配置的屬性)。其次,應用程式程式碼使用類繼承的頻率遠低於框架程式碼。例如,在應用程式中,特化在應用程式程式碼中發生的頻率不如在框架程式碼中那樣頻繁——再次強調,應用程式程式碼的使用場景數量要少得多。

那麼你應該使用什麼?

我們通常建議大家對所有強制性的協作者使用建構函式注入,對所有其他屬性使用 setter 注入。再次強調,建構函式注入確保所有強制性屬性都已滿足,並且根本不可能在無效狀態下例項化物件(沒有傳入其協作者)。換句話說,使用建構函式注入時,你不需要使用專門的機制來確保必需的屬性已設定(除了正常的 Java 機制)。

不使用建構函式注入的另一個論點是建構函式中缺少引數名稱,以及這些名稱不出現在 XML 中。我認為在大多數應用程式中,這並沒有太大關係。首先考慮使用 setter 注入的變體。


<bean id="authenticator" class="com.mycompany.service.AuthenticatorImpl"/>

<bean id="accountService" class="com.mycompany.service.AccountService">
  <property name="authenticator" ref="authenticator"/>
</bean>

這個版本將 authenticator 作為屬性名和 bean 名提及。這是我經常遇到的模式。我認為在使用建構函式注入時,建構函式引數名稱的缺失(以及它們不出現在 XML 中)並不會讓我們感到困惑。


<bean id="authenticator" class="com.mycompany.service.AuthenticatorImpl"/>

<bean id="accountService" class="com.mycompany.service.AccountService">
  <constructor-arg ref="authenticator"/>
</bean>

使用替代機制

這讓我們回到了這篇博文的主題,其中也提到了 @Required。這是我們在 2006 年引入的新的 Spring 2.0 註解。@Required 允許你指示 Spring 為你檢查必需的依賴項。如果你無法使用建構函式注入,或者由於其他任何原因更喜歡 setter 注入,@Required 是一個不錯的選擇。只需註解屬性的 setter 並將 RequiredAnnotationBeanFactoryPostProcessor 註冊為應用程式上下文中的 bean 即可。

public class Service {

  private Collaborator collaborator;

  @Required
  public void setCollaborator(Collaborator c) {
    this.collaborator = c;
  }
}

<bean class="org.sfw.beans.factory.annotation.RequiredAnnotationBeanFactoryPostProcessor"/>

檢查必需依賴項的其他機制

還有其他幾種機制可以強制檢查必需的依賴項。其中大多數依賴於 Spring 的能力,允許你在物件的構建和初始化過程中的某些點獲取回撥,例如 Spring 的 InitializingBean 介面或你可以在 XML 中配置的任意 init 方法(使用 init-method 屬性)。這些機制都與建構函式注入非常相似,區別在於你依賴 Spring 來為你呼叫執行斷言的方法。

public class Service implements InitializingBean {

  private Collaborator collaborator;

  public void setCollaborator(Collaborator c) {
    this.collaborator = c;
  }

  // from the InitializingBean interface
  public void afterPropertiesSet() {
    if (collaborator == null) {
      throw new IllegalStateException("Collaborator must be set in order for service to work");
    }
  }
}

另一個類似於 Java 中的 @Required 的機制是 XML 中的 dependency-check 屬性,奇怪的是它並沒有被大量使用。透過修改此屬性(預設關閉)啟用依賴檢查,將告訴 Spring 開始檢查 bean 的某些依賴項。請參閱參考資料以獲取有關此特性的更多資訊。

那麼為什麼檢查必需的依賴項

確實有很多人不檢查依賴項是否已正確設定。人們不這樣做的最大原因是,他們認為只要啟動 ApplicationContext 並以某種方式使用了具有依賴項的類,他們就會很快發現問題。這當然非常真實。例如,如果您使用 Spring 的整合測試支援,可以讓 Spring 為您載入應用程式上下文。如果您還確保在整合測試中測試了一些實際程式碼,您很可能幾乎可以保證類工作所需的所有依賴項都已設定。不過,這種方法讓我有點頭疼。您必須對測試用例覆蓋程式碼的程度足夠自信,因為如果您的測試沒有測試依賴於設定協作者的程式碼,那你就慘了,因為你可能發現不了問題!當然,在部署應用程式時進行冒煙測試可能會立即奏效,但我可不想成為那個只在執行時才發現缺少依賴項的人!

結論

關於建構函式注入與 setter 注入,有很多話要說,而且我知道很多人仍然喜歡 setter 注入。然而,我認為(並且和我一樣,很多人都這麼認為)對於沒有大量可選和可配置值或協作者的程式碼來說,結合在建構函式中檢查依賴項的建構函式注入是強制檢查必需依賴項的更好方法。將其與 final 欄位結合使用,立即提供了在多執行緒環境中提高安全性的另一個好處,而且由於這通常不是什麼大問題,我不會在這篇博文中詳細討論。

有些情況下我不會使用建構函式注入。例如,其中一種情況是具有大量依賴項或其他可配置值的類。我個人認為一個帶有 20 個引數的建構函式不是一個好程式碼示例。當然,問題是,一個擁有 20 個依賴項的類是否承擔了過多的職責...

有一件事是肯定的——在業務方法中檢查必需的依賴項來強制執行,這是我絕對不會做的事情。

訂閱 Spring 資訊

透過 Spring 資訊保持聯絡

訂閱

搶佔先機

VMware 提供培訓和認證,助您快速提升。

瞭解更多

獲取支援

Tanzu Spring 在一個簡單的訂閱中提供對 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位制檔案。

瞭解更多

近期活動

檢視 Spring 社群的所有近期活動。

檢視全部