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

工程 | Ramnivas Laddad | 2008年1月24日 | ...

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

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

目前,大多數Spring使用者使用@Configurable註解來指定可配置類。隨著即將釋出的Spring 2.5.2中的最新改進,從 nightly build 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會將建立物件的 mailSender 屬性設定為 externalMailSender bean。

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 類。您可以使用一個域特定的 MailSenderClient 介面,而不是使用 @Configurable,它表示它使用 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. inConfigurableBean() 只選擇 MailSenderClient 子型別中的連線點,從而將切面的適用性限制為僅匹配型別。

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() 切入點結合使用,以便在構造前或構造後建議注入依賴項。
  6. leastSpecificSuperTypeConstruction():選擇與最不具體超型別對應的連線點。

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

所以,您的應用程式中啟用了域物件DI的所有選項都在這裡。如果您從事DDD或者需要將DI擴充套件到您的域物件,您必須檢視這組新的切面。根據您的具體需求和AspectJ知識,您會發現其中之一有助於建立優雅的解決方案。

獲取 Spring 新聞通訊

透過 Spring 新聞通訊保持聯絡

訂閱

領先一步

VMware 提供培訓和認證,助您加速進步。

瞭解更多

獲得支援

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位制檔案,只需一份簡單的訂閱。

瞭解更多

即將舉行的活動

檢視 Spring 社群所有即將舉行的活動。

檢視所有