建構函式注入與 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)。
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 框架本身的性質。

歷史原因

早在 2003 年初,Spring 首次作為開源專案釋出時,它主要關注 Setter 注入。其他框架也率先提出了依賴注入的方法,其中一個就是 PicoContainer,它非常側重於建構函式注入。Spring 保持其對 Setter 注入的關注,因為當時我們認為缺乏預設引數和建構函式引數的引數名導致了開發者的清晰度降低。但是,我們也實現了建構函式注入,以便能夠為那些想要例項化和管理他們不控制的物件(objects they didn't control)的開發者提供這項功能。

這是你在 Spring 框架本身中看到大量 Setter 注入的原因之一。Setter 注入在 Spring 本身中的使用,以及我們大力提倡它,也導致了許多第三方軟體開始使用 Setter 注入,以及部落格和文章開始提及 Setter 注入。

(順便問一下,大家還記得第一代、第二代和 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 介面,或者 Spring 中可以在 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 社群所有即將舉行的活動。

檢視所有