搶佔先機
VMware 提供培訓和認證,助你加速進步。
瞭解更多幾個月前,在我還沒寫部落格的時候,Cedric 和 Bob 曾討論過“Getter 注入”。
基本概念是,IoC 容器可以在部署時覆蓋受管理物件的抽象或具體方法。容器注入的是一個方法(例如 getter 方法),而不是像 Setter 注入那樣注入引用或基本型別。碰巧的是,我當時正在為 Spring 1.1 開發一個容器方法覆蓋機制,該機制後來在 Spring 1.1 RC1 中釋出。這是一個有趣的概念,肯定是一個完整的 IoC 容器的一部分。然而,我認為這個概念更普遍,需要一個更通用的名稱。此外,它應該只在相當狹窄的場景範圍內使用。
你為什麼要這樣做?Cedric 的動機是 setter 方法是“無用的”,並且“在 Java 物件中擁有你永遠不會呼叫的方法是一種設計上的壞味道”。在他看來,物件中最重要的方法實際上是 getters,它們通常返回儲存在 setter 中的物件引用。因此,他建議讓容器實現 getter 方法,並取消 setter。實際上,這意味著容器將實際覆蓋作為應用程式程式碼一部分定義的 getter 方法,否則將無法使用它們。因此,容器最終將使用類似於 CMP 2.x 的機制來實現它(儘管希望任何相似之處僅限於此)。
我不太贊同“無用方法”的論點,因為 IoC 容器使用依賴注入 會 呼叫 setter 方法,並且在單元測試中,完全不使用容器也 會 呼叫它們。如果物件在容器外部使用,應用程式程式碼也會呼叫它們。此外,getter/setter 組合是一種很好的設定預設值的方式,以防你選擇不配置一個或多個 setter 被呼叫:如果你需要,setter 就在那裡。雖然我理解 Cedric 的動機,但這裡有一個權衡:如果我們去除那些據說無用的 setter,剩下的就是不完整的類。如果 getter 是抽象的,我們就回到了 CMP 2.x 的測試場景,需要測試抽象物件。如果 getter 是具體的,我們就是在常規地編寫那些將在執行時被覆蓋的方法。在我看來,這才是真正的無用程式碼。(一般來說,我不太喜歡覆蓋具體方法,並儘可能避免它。我想我第一次在 UML Reference Manual 中讀到這個建議,它很有道理。)“setter 注入”中也存在一些魔法元素。如果我可以使用一個簡單的 POJO,而無需複雜的容器子類化,我更喜歡那樣。正如 Cedric 自己去年五月在 TSSS 的一次小組討論中說得很好:“只有在科學失敗時才使用魔法。”
我認為這個概念應該改名為 方法注入,並且它在其他一些不那麼常見的場景中價值更大。
在使用依賴注入配置物件的典型場景中,我不會將其用作 Setter 注入或建構函式注入的替代方案。Setter 方法和建構函式是普通的 Java 結構,它們在容器中工作得很好,但不依賴於容器。這很好。IoC 容器提供的“魔法”方法會增加一點對容器的依賴,儘管當然仍然可以在容器外部對物件進行子類化,而且它們仍然只是 Java。
本質上,我認為方法注入在某些特殊情況下可以替代子類化,在這些情況下,超類應與容器依賴隔離,並且容器可以比常規子類更容易地實現必要的行為。相關方法不必是 getter 方法(如 Setter 注入中的 getter),儘管通常它會是返回某種值的方法。
我認為由容器實現的方法主要有三種情況:
它們可以將容器依賴從應用程式程式碼中移除。它們可以依賴於直到部署時才知道的基礎設施。它們可以根據執行時環境定製遺留程式碼的行為。然而,普通的子類化在這裡也說得通。容器子類化也比常規子類化更具動態性。我們可以潛在地使用同一個基類,並以不同的方式部署它,而無需管理多個類的原始碼。然而,由於它的“魔法”成分高於常規子類化、策略介面或各種替代方案,我認為不應過於熱衷地使用方法注入。
對我來說,方法注入的主要吸引力在於它是一種消除我在使用 Spring 1.0 時有時不得不承擔的容器依賴的方式,並且它適用於任何支援“非單例”或“原型”物件概念的容器。(也就是說,一個根據配置,可以在請求時選擇獲取 IoC 管理物件的共享例項或新例項的容器。)我喜歡使用 Spring,但我討厭為了配置而必須匯入 Spring API。
促使我實現這一功能的具體用例是,當一個透過 Spring 配置的“單例”物件需要建立非單例物件(例如,一個單執行緒、一次性使用的處理物件)的例項,同時又希望該物件使用依賴注入進行配置,而不是僅僅使用 new。例如,想象一下 ThreadSafeService 需要建立一個 SingleShotHelper 的例項,而 SingleShotHelper 本身也是透過依賴注入配置的。在 Spring 1.0.x 中,ThreadSafeService 需要實現 BeanFactoryAware 生命週期介面,儲存 BeanFactory 引用並呼叫
(SingleShotHelper) beanFactory.getBean("singleShotHelper")
每次需要建立一個助手時。這很好,測試也不太難(BeanFactory 是一個簡單的介面,所以很容易模擬),但這引入了 Spring 依賴,而如果能更接近一個完全無侵入的框架,那就太理想了。型別轉換也略顯不優雅,儘管不是什麼大問題。
我通常在大概 10 個類中會遇到一次這種情況。我有時會將這種情況重構為一個提取出來的方法,像這樣:
protected SingleShotHelper createSingleShotHelper() { return (SingleShotHelper) context.getBean("singleShotHelper"); }
我現在可以透過子類化來實現這個方法,並將 Spring 依賴從超類中移除,但這似乎有點過度。
這類方法是理想的容器實現而非應用程式開發人員實現的物件。它返回一個容器知曉的物件;實際上,整個過程用配置來表達比程式碼更簡潔(當你考慮到儲存 BeanFactory 引用所需的那一點程式碼時)。
藉助 Spring 1.1 中引入的新方法注入功能,可以使用抽象(或具體)方法,例如:
protected abstract SingleShotHelper createSingleShotHelper();
並告訴容器在部署時覆蓋該方法,以從相同或父工廠返回特定 bean,如下所示:
<lookup-method name="createSingleShotHelper" bean="singleShotHelper" >
這些方法可以是 protected 或 public 的。可以覆蓋任意數量的方法。<lookup-method> 元素可以像 property 或 constructor-arg 元素一樣用在 bean 定義元素內部。
我認為方法注入最引人注目的用例是返回查詢由容器管理的命名物件的結果。(當然這並非 Spring 特有的:任何容器都可以實現這一點。)查詢通常是一個非單例 bean(用 Spring 的說法)。
透過這種方式,應用程式程式碼中不再依賴於 Spring 或任何其他 IoC 容器。無需匯入 Spring API,就可以解決一個特殊情況。正如我所說,這個功能是直接由我正在參與的一個客戶專案中的需求推動的,並且在實踐中證明了它的實用性。
Lookup 方法可以與 Setter 注入或建構函式注入結合使用。它們不接受引數,因此方法過載不是問題。
實現使用了 CGLIB 來對類進行子類化。(只有當 CGLIB 在 classpath 中時才可用,這是為了避免讓 Spring 核心容器依賴於 CGLIB。)
Spring 更進一步,允許你為被覆蓋的方法定義任意行為——不僅僅是 bean 查詢。你可能希望這樣做,例如,使用基於執行時基礎設施的通用行為——比如使用 Spring 的 TransactionInterceptor 類進行事務回滾。(當然,通常應使用回滾規則來避免這種情況。)或者可能存在通用覆蓋行為的令人信服的用例——例如,“如果存在活躍事務,則返回事務資料來源 DS1,否則返回非事務資料來源 DS2”。同樣,如果我們可以將這種邏輯從應用程式程式碼中隱藏起來,那將是一個勝利。在這裡,我們已經超出了純粹“getter”的範圍:例如,我們可以覆蓋方法來發布事件。
對於任意的容器覆蓋,通常存在替代方案,例如對類進行子類化並以常規方式覆蓋方法(科學而非魔法),或者使用 AOP。在像示例中那樣進行 bean 查詢的情況下,由容器執行覆蓋有明顯的好處,因為它消除了對 Spring API 的依賴。在 XML 中描述也更簡單。對於更一般的情況,需要有一種方法來解析過載方法。
這篇文章已經比我計劃的要長——而且花了不少時間!——所以,如果有人感興趣,我將把 Spring 1.1 的任意覆蓋機制(包括它如何解析過載方法)留到以後的文章中討論。我對那些像 Dion 和 Matt Raible 這樣不知疲倦的博主們油然而生敬意,他們似乎每天都要寫 3 篇部落格。