領域物件依賴注入功能的新改進

技術 | Ramnivas Laddad | 2008年1月24日 | ...

Spring 的依賴注入(DI)機制允許配置在應用上下文中定義的 bean。如果你想將相同的想法擴充套件到非 bean 物件呢?Spring 對領域物件 DI 的支援利用 AspectJ 織入技術將 DI 擴充套件到任何物件,即使它是由 Web 或 ORM 框架建立的。這使得建立具有豐富領域行為的物件成為可能,因為領域物件現在可以與注入的物件協作。在這篇部落格中,我將討論 Spring Framework 在這一領域的最新改進。

領域物件 DI 背後的核心思想非常簡單:透過 AspectJ 織入的切面選擇與符合特定規範的任何物件的建立反序列化對應的連線點。這些連線點的通知將依賴項注入到正在建立或反序列化的物件中。當然,細節決定成敗。例如,如何選擇與反序列化對應的連線點?或者如何確保每個物件只注入一次依賴項?透過提供一些預先編寫好的切面,Spring 使開發人員免受這些細節的影響。

目前,大多數 Spring 使用者使用 @Configurable 註解來指定可配置的類。隨著即將釋出的 Spring 2.5.2 中的最新改進(自每夜構建 379 開始可用),你有了更多選項,使此功能更加強大。新改進遵循“讓簡單的事情更簡單,讓複雜的事情成為可能”的原則。根據你對 AspectJ 的熟悉程度和預期的設計複雜性,其中一種選項會非常適用。圖 1 顯示了新的切面層次結構,它使得簡單性與靈活性的結合成為可能。

Domain Object Dependency Injection Aspects

圖 1:領域物件依賴注入切面的繼承層次結構。

那麼,這些切面分別提供什麼?我們從下往上看。

讓簡單的事情更簡單:AnnotationBeanConfigurerAspect

AnnotationBeanConfigurerAspect 使領域物件 DI 無需任何使用者 AspectJ 程式碼。因此,對於許多開發人員來說,它是最簡單的選擇。使用此切面,你可以使用 @Configurable 註解標註需要依賴注入的類。例如,你可以將 Order 類標註如下
 
@Configurable
public class Order {
    private transient MailSender mailSender;
    
    public void setMailSender(MailSender mailSender) {
        this.mailSender = mailSender;
    }
    
    public void process() {
        ... 
        mailSender.send(...);
        ...
    }
}

接下來,你指示 Spring 如何配置 Order 型別的物件。配置說明遵循原型 bean 的標準 bean 定義,如下所示


<context:spring-configured/>
    
<bean class="example.Order" scope="prototype">
    <property name="mailSender" ref="externalMailSender"/>
</bean>
    
<bean id="externalMailSender" ...>
    ...
</bean>

現在,對於任何 Order 物件的建立或反序列化,Spring 都將使用 externalMailSender bean 設定建立物件的 mailSender 屬性。

Spring 2.5 引入了一種新的基於註解的配置選項,可以消除或減少伴隨的 XML 配置。基於 @Configurable 註解的 DI 也因此受益。例如,你可以如下所示將 mailSender 屬性標記為 @Autowired

 
@Configurable
public class Order {
    private transient MailSender mailSender;
    
    @Autowired
    public void setMailSender(MailSender mailSender) {
        this.mailSender = mailSender;
    }
    
    public void process() {
        ... 
        mailSender.send(...);
        ...
    }
}

你甚至可以透過直接標註欄位本身來去掉 setter 方法,將上面的程式碼簡化為

 
@Configurable
public class Order {
    @Autowired private transient MailSender mailSender;
    
    public void process() {
        ... 
        mailSender.send(...);
        ...
    }
}

無論哪種情況,伴隨的 XML 配置都可以簡化為如下所示(注意使用了 <context:annotation-config/>)


<context:spring-configured/>
    
<context:annotation-config/>
    
<bean id="externalMailSender" ...>
    ...
</bean>

有關此領域物件 DI 選項的更多詳細資訊,請參閱使用 AspectJ 為 Spring 領域物件注入依賴項

讓複雜的事情成為可能:AbstractInterfaceDrivenDependencyInjectionAspect

AnnotationBeanConfigurerAspect 的基礎切面 AbstractInterfaceDrivenDependencyInjectionAspect 使用介面而不是註解來標記可配置的類。雖然這看起來是一個相當表面的變化,但它提供了一些有趣的選項,例如使用領域介面和註解指定依賴注入、透過繞過反射提高注入效能以及利用多個切面配置一個物件。

在設計層面,此切面配置任何型別實現了 ConfigurableObject 介面的領域物件。雖然讓型別直接實現 ConfigurableObject 介面無疑是有效的選擇,但一個更優雅的替代方案是在另一個切面(AbstractInterfaceDrivenDependencyInjectionAspect 的子切面將是合乎邏輯的選擇)中使用 declare parents 語句。該語句將宣告一個可配置的類實現了 ConfigurableObject 介面。這使得你的領域類不受 Spring 特定構件的影響,同時又能從 DI 機制中受益。讓我們來看一個這種用法的例子。

考慮前面章節中的 Order 類。與其使用 @Configurable,不如讓它實現一個領域特定的 MailSenderClient 介面,這表明它使用了 MailSender

 
public class Order implements MailSenderClient {
    private transient MailSender mailSender;
            
    public void setMailSender(MailSender mailSender) {
        this.mailSender = mailSender;
    }
            
    public void process() {
        ... 
        mailSender.send(...);
        ...
    }
}

接下來,你編寫 AbstractInterfaceDrivenDependencyInjectionAspect 的一個子切面,將依賴項注入到任何 MailSenderClient 物件中。

 
public aspect MailClientDependencyInjectionAspect extends 
    AbstractInterfaceDrivenDependencyInjectionAspect {
    private MailSender mailSender;
    
    declare parents: MailSenderClient implements ConfigurableObject;
            
    public pointcut inConfigurableBean() : within(MailSenderClient+);
    
    public void configureBean(Object bean) {
        ((MailSenderClient)bean).setMailSender(this.mailSender);
    }
            
    public void setMailSender(MailSender mailSender) {
        this.mailSender = mailSender;
    }
}

切面中使用了兩個 AspectJ 構造

  1. declare parents 語句使得 MailSenderClient 實現了 ConfigurableObject 介面,使其有資格透過 AbstractInterfaceDrivenDependencyInjectionAspect 進行 DI。
  2. The inConfigurableBean() 僅選擇 MailSenderClient 的子型別中的連線點,從而將切面的適用範圍限制在僅匹配的型別上。

The configureBean() 方法透過直接呼叫適當的 setter 方法來執行向 bean 的注入。當然,任何其他適合 bean 配置的邏輯,例如呼叫多引數方法或呼叫任何初始化方法,也都完全可行。請注意,以這種方式使用的直接呼叫避免了反射,並且如果領域物件建立速率很高,可以帶來顯著的效能提升。

你需要配置 MailClientDependencyInjectionAspect 切面例項本身,以注入其依賴項——mailSender 屬性。Spring 的做法是為該切面建立一個 bean 並在應用上下文中配置它


<bean class="example.MailClientDependencyInjectionAspect" 
        factory-method="aspectOf">
    <property name="mailSender" ref="externalMailSender"/>
</bean>
    
<bean id="externalMailSender" ...>
    ...
</bean>

圍繞這個切面還有一些額外的模式

  • 使用多個切面配置一個物件(例如,每個“客戶端”介面一個切面)。
  • 使用領域註解而不是領域介面或 @Configurable 註解來指定可配置的型別。
  • 使用基於 hasmethod() 的型別模式(目前是 AspectJ 5 中的實驗性功能,將成為 AspectJ 6 中的正式功能),以避免使用與 DI 相關的型別或註解。
  • 使用基於 AspectJ 的 mixins 為客戶端介面提供預設實現,並避免重複的 setter 方法。

然而,這些想法就留待另一篇部落格文章來討論吧。

當你需要時提供靈活性:AbstractDependencyInjectionAspect

最後,這是最靈活的基礎切面。此切面要求你對 AspectJ 切入點語言有紮實的理解。然而,除非在極端定製場景(例如自定義反序列化事件)中,否則你不會直接建立此基礎切面的子切面。相反,你將使用我們之前討論過的子切面之一。

該切面聲明瞭子切面可以定義的六個切入點

  1. beanConstruction(Object bean):選擇 bean 的構造。典型實現將選擇物件初始化連線點。
  2. beanDeserialization(Object bean):選擇 bean 的反序列化。典型實現將選擇注入物件中必須存在的 readResolve() 方法。如果你使用了非標準的反序列化(不呼叫 readResolve()),你可以使用此切入點選擇適當的替代方法。
  3. inConfigurableBean():選擇定義切面可配置的 bean 中的連線點。典型實現將使用帶有適當型別模式的 within() 切入點。
  4. preConstructionConfiguration():選擇需要在構造之前注入依賴項的 bean 的連線點。此切入點的預設實現不選擇任何連線點(bean 將在建構函式執行後注入依賴項)。
  5. mostSpecificSubTypeConstruction():選擇與最具體子型別對應的連線點。預設實現使用連線點簽名來確定建構函式是否代表被注入 bean 型別層次結構中最具體的建構函式。然後將此資訊與 preConstructionConfiguration() 切入點結合使用,以利用 before 或 after 通知來注入依賴項。
  6. leastSpecificSuperTypeConstruction():選擇與最不具體的超型別對應的連線點。

此切面還定義了一個抽象方法 configureBean(Object bean),其實現應指定與依賴注入對應的邏輯。

所以,你現在擁有了在應用中啟用領域物件 DI 的所有選項。如果你正在進行 DDD 或需要將 DI 擴充套件到你的領域物件,你應該關注這組新的切面。根據你的具體需求和 AspectJ 知識,你會發現其中一種有助於建立一個優雅的解決方案。

訂閱 Spring 新聞通訊

透過 Spring 新聞通訊保持聯絡

訂閱

保持領先

VMware 提供培訓和認證,助你加速發展。

瞭解更多

獲取支援

Tanzu Spring 透過一項簡單的訂閱即可為 OpenJDK™、Spring 和 Apache Tomcat® 提供支援和二進位制檔案。

瞭解更多

近期活動

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

檢視全部