Setter 注入與建構子注入以及 @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 機制 (c.f. Assert)
9% 在 XML 中使用 dependency-check 屬性
13% 我不需要,我使用建構子注入
15% 使用 InitializingBean 和 assert 機制
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 注入的原因之一。setter 注入在 Spring 本身中使用的事實,以及我們主要提倡它的事實也導致許多第三方軟體開始使用 setter 注入,以及部落格和文章開始提及 setter 注入。

(順便問一下,人們還記得 type 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>

這個版本將驗證器提及為屬性名稱和 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 介面或 Spring 的任意 init 方法,您可以在 XML 中配置(使用 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 社群中所有即將舉辦的活動。

查看全部