SeamFramework.orgCommunity Documentation

章 4. 依賴注入(Dependency injection)

4.1. 綁定標記
4.1.1. member 和綁定標記
4.1.2. 綁定標記的組合
4.1.3. 綁定標記和 producer method
4.1.4. 預設綁定類型
4.2. 建置類型
4.2.1. 啟用 deployment type
4.2.2. Deployment type 優先權
4.2.3. 範例 deployment type
4.3. 修正相依性不足(unsatisfied dependencies)的問題
4.4. 客戶端代理伺服器(Client proxies)
4.5. 透過程式性的搜尋來取得一個 Web Bean
4.6. Lifecycle callback、@Resource@EJB@PersistenceContext
4.7. InjectionPoint 物件

Web Bean 支援了三個主要的依賴注入機制:

Constructor parameter injection:

public class Checkout {

        
    private final ShoppingCart cart;
    
    @Initializer
    public Checkout(ShoppingCart cart) {
        this.cart = cart;
    }
}

Initializer method parameter injection:

public class Checkout {

        
    private ShoppingCart cart;
    @Initializer 
    void setShoppingCart(ShoppingCart cart) {
        this.cart = cart;
    }
    
}

以及 direct field injection:

public class Checkout {


    private @Current ShoppingCart cart;
    
}

每當 Web Bean 的 instance 第一次被例示時,依賴注入就會發生。

EJB Bean 不支援 Constructor parameter injection,因為 EJB 是由 EJB container 來例示(instantiate)的,而不是以 Web Bean 管理員。

當套用了預設的 @Current 綁定類型時,Constructor 和 initializer method 的參數不需要被明確地標記。不過,儘管套用了預設的綁定類型,被注入的欄位還是一定要指定綁定類型。若該欄位不指定綁定類型的話,它將不會被注入。

Producer method 也支援 parameter injection:

@Produces Checkout createCheckout(ShoppingCart cart) {

    return new Checkout(cart);
}

最後,observer method(我們會在 章 9, 事件(Events) 中談到)、disposal method 以及 destructor method 全都支援 parameter injection。

Web Bean 規格定義了一個流程,該流程稱為 typesafe resolution algorithm(typesafe 解析演算法),當 Web Bean 要注入至一個注入點時,Web Bean 管理員便會遵照該流程來進行。這個演算法乍看之下非常地複雜,不過當您理解它之後,您會發現它實際上並不複雜。Typesafe 解析會在系統初始化時進行,這也代表了若 Web Bean 的相依性無法被滿足的話,管理員會即刻透過回傳一個 UnsatisfiedDependencyException 或是 AmbiguousDependencyException 來通知用戶。

這個演算法就是為了要讓多重 Web Bean 實做相同的 API 類型以及:

讓我們來探討 Web Bean 管理員如何判斷某個 Web Bean 要如何被注入。

若我們擁有超過一個實做特定 API 類型的 Web Bean,注入點可藉由使用綁定標記來確切地指定哪個 Web Bean 應該被注入。比方說,PaymentProcessor 的實做可能有兩個:

@PayByCheque

public class ChequePaymentProcessor implements PaymentProcessor {
    public void process(Payment payment) { ... }
}
@PayByCreditCard

public class CreditCardPaymentProcessor implements PaymentProcessor {
    public void process(Payment payment) { ... }
}

@PayByCheque@PayByCreditCard 為綁定標記:

@Retention(RUNTIME)

@Target({TYPE, METHOD, FIELD, PARAMETER})
@BindingType
public @interface PayByCheque {}
@Retention(RUNTIME)

@Target({TYPE, METHOD, FIELD, PARAMETER})
@BindingType
public @interface PayByCreditCard {}

客戶端 Web Bean 開發人員會使用綁定標記來確切指定哪個 Web Bean 應被注入。

使用 field injection:

@PayByCheque PaymentProcessor chequePaymentProcessor;

@PayByCreditCard PaymentProcessor creditCardPaymentProcessor;

使用 initializer method injection:

@Initializer

public void setPaymentProcessors(@PayByCheque PaymentProcessor chequePaymentProcessor, 
                                 @PayByCreditCard PaymentProcessor creditCardPaymentProcessor) {
   this.chequePaymentProcessor = chequePaymentProcessor;
   this.creditCardPaymentProcessor = creditCardPaymentProcessor;
}

或使用 constructor injection:

@Initializer

public Checkout(@PayByCheque PaymentProcessor chequePaymentProcessor, 
                @PayByCreditCard PaymentProcessor creditCardPaymentProcessor) {
   this.chequePaymentProcessor = chequePaymentProcessor;
   this.creditCardPaymentProcessor = creditCardPaymentProcessor;
}

所有 Web Bean 都有一個 deployment type(建置類型)。各個 deployment type 都可標識一組應依照條件性地被安裝在系統的某些 deployment 中的 Web Bean。

比方說,我們可定義一個名為 @Mock 的 deployment type,它會找出只應在系統執行於一個整合測試環境中的時候才會被安裝的 Web Bean:

@Retention(RUNTIME)

  @Target({TYPE, METHOD})
  @DeploymentType
  public @interface Mock {}

假設我們有一些和外部系統進行互動以便處理款項的 Web Bean:

public class ExternalPaymentProcessor {

        
    public void process(Payment p) {
        ...
    }
    
}

因為這個 Web Bean 並未明確地指定一個 deployment type,因此它的 deployment type 會是預設的 @Production

假設要進行整合或是單元測試(unit testing),不過外部系統較慢或是無法使用。因此我們將建立一個 mock 物件:

@Mock 

public class MockPaymentProcessor implements PaymentProcessor {
    @Override
    public void process(Payment p) {
        p.setSuccessful(true);
    }
}

不過 Web Bean 管理員要如何判斷在特定建置中該使用哪個實做?

若您有在細心注意的話,您應該會疑惑 Web Bean 管理員如何決定哪個實做 — ExternalPaymentProcessorMockPaymentProcessor — 中選擇哪一個。請思考當管理員遇上了這個注入點時會如何:

@Current PaymentProcessor paymentProcessor

有兩個 Web Bean 可滿足 PaymentProcessor 合同。當然,我們無法使用綁定標記來消除語意上的含糊意義,因為綁定標記已寫死(hard-coded)在注入點的來源之中,而且我們希望管理員能夠在 deployment time 時作決定!

這項問題的解決方式就是利用各個 deployment type 的不同優先權。Deployment type 的優先權是透過它們出現在 web-beans.xml 中的順序來決定的。在我們的範例中,@Mock 出現的順序在 @Production 之後,因此它會有較高的優先權。

每當管理員發現了多於一個 Web Bean 可滿足由某個注入點所指定的合同(API 類型加上綁定標記)時,它會考量到 Web Bean 的相關優先權。若有一方的優先權較高,它便會選擇優先權較高的那個 Web Bean 來注入。因此,在我們的範例中,當 Web Bean 管理員執行於我們的整合測試環境中的時候(這正是我們想要的),它將會注入 MockPaymentProcessor

和現今多人使用的管理員架構相較之下,這個功能相當地有趣。各種「lightweight」的 container 都允許存在 classpath 中的 class 的條件性建置,不過要被建置的 class 必須要明確、各別地列在配置程式碼或是一些 XML 配置檔案中。Web Bean 不支援透過 XML 的 Web Bean 定義與配置,不過在一般情況下當不需要複雜的配置時,deployment type 允許一整組 Web Bean 能夠透過 XML 中的一個單獨行列來被啟用。其間,瀏覽程式碼的開發人員能夠輕易地分辨出 Web Bean 將會使用哪種建置方案(deployment scenario)。

就所有實做某個注入點的 API 類型的 Web Bean 綁定標記和 deployment type 來講,若 Web Bean 管理員無法辨識出正好一個需被注入的 Web Bean 為何,那麼 typesafe 解析演算法便會失敗。

要修正一個 UnsatisfiedDependencyExceptionAmbiguousDependencyException 通常相當容易。

若要修正一項 UnsatisfiedDependencyException,只要提供一個實做 API 類型並且擁有注入點的綁定類型的 Web Bean 即可 — 或是啟用一個已實做 API 類型並且擁有綁定類型的 Web Bean 的 deployment type 即可。

若要修正一項 AmbiguousDependencyException,您可加入一個綁定類型來在兩個 API 類型的實做之間進行分辨,或是更改其中一個實做的 deployment type,這樣一來 Web Bean 管理員便可透過使用 deployment type 優先權來在它們之間作選擇。AmbiguousDependencyException 只會在有兩個 Web Bean 共享一個綁定類型並擁有相同 deployment type 的情況下才會發生。

當您在 Web Bean 中使用依賴注入時,您還需要注意一個問題。

一個已注入的 Web Bean 的客戶端通常不會持有一個 Web Bean instance 的直接參照。

想像一個綁定至應用程式 scope 的 Web Bean 持有一個綁定至請求 scope 的 Web Bean 的直接參照。這個應用程式 scope 的 Web Bean 會在許多不同的請求之間被共享。不過,各個請求都應要看見一個不同的請求 scope Web Bean 的 instance!

現在,請想像一個綁定至 session scope 的 Web Bean 持有一個綁定至應用程式 scope 的 Web Bean 的直接參照。有時,session context 會被序列化至磁碟中以便更有效率地使用記憶體。不過,應用程式 scope 的 Web Bean instance 不該和 session scope 的 Web Bean 一起被序列化!

因此,除非有個 Web Bean 擁有預設的 @Dependent scope,否則 Web Bean 管理員便必須透過一個 proxy 物件來將所有注入的參照重新指向 Web Bean。這個 client proxy 負責確保收到 method 調用的 Web Bean instance 是個和目前 context 相聯的 instance。客戶端 proxy 亦可允許在不遞迴地序列化其它已注入的 Web Bean 的情況下也能讓綁定至 context(例如 session context)的 Web Bean 被序列化至磁碟。

不巧的是,礙於 Java 語言的限制,有些 Java 類型無法被 Web Bean 管理員代理(proxied)。因此,若某個注入點的 type 無法被代理的話,Web Bean 管理員便會回傳一個 UnproxyableDependencyException

下列 Java 類型無法被 Web Bean 管理員代理:

要修正 UnproxyableDependencyException 通常相當容易。只要將一個無參數的 constructor 附加至注入的 class、採用一個介面,或將已注入的 Web Bean 的 scope 更改為 @Dependent 即可。

應用程式可透過注入來取得 Manager 這個介面的一個 instance:

@Current Manager manager;

Manager 這個物件提供了一組用來程式性地取得 Web Bean instance 的 method。

PaymentProcessor p = manager.getInstanceByType(PaymentProcessor.class);

綁定標記能被透過建立 helper class 的 subclass AnnotationLiteral 來指定,否則在 Java 中很難例示一個標記類型。

PaymentProcessor p = manager.getInstanceByType(PaymentProcessor.class, 

                                               new AnnotationLiteral<CreditCard
>(){});

若綁定類型有個標記成員,我們便無法使用 AnnotationLiteral 的一個匿名 subclass — 我們需要建立一個有命名的 subclass:

abstract class CreditCardBinding 

    extends AnnotationLiteral<CreditCard
> 
    implements CreditCard {}
PaymentProcessor p = manager.getInstanceByType(PaymentProcessor.class, 

                                               new CreditCardBinding() { 
                                                   public void value() { return paymentType; } 
                                               } );

企業級的 Web Bean 支援 EJB 規格所定義的所有 lifecycle callback:@PostConstruct@PreDestroy@PrePassivate@PostActivate

基本的 Web Bean 只支援 @PostConstruct@PreDestroy callback。

企業級和基本的 Web Bean 皆支援使用 @Resource@EJB@PersistenceContext 來相應地注入 Java EE 資源、EJB 和 JPA 的 persistence context。基本的 Web Bean 不支援使用 @PersistenceContext(type=EXTENDED)

@PostConstruct callback 一定會在所有相依性都被注入後才會發生。

有幾種特定相依物件 — 含有 @Dependent 這個 scope 的 Web Bean — 需要知道有關於物件或是它們被注入的注入點相關資訊才能進行它們本應進行的工作。比方說:

含有 @Dependent 這個 scope 的 Web Bean 能夠注入一個 InjectionPoint instance 並存取和它所屬的注入點相關的 metadata。

讓我們來探討下列範例。下列程式碼較為冗長,並且有重構(refactoring)問題上的弱點:

Logger log = Logger.getLogger(MyClass.class.getName());

這個 producer method 能讓您在不明確指定 log category 的情況下注入一個 JDK Logger

class LogFactory {


   @Produces Logger createLogger(InjectionPoint injectionPoint) { 
      return Logger.getLogger(injectionPoint.getMember().getDeclaringClass().getName()); 
   }
}

現在我們可寫入:

@Current Logger log;

若您無法被說服的話,我們還有第二個範例。若要注入 HTTP 參數,我們需要定義一個綁定類型:

@BindingType

@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface HttpParam {
   @NonBinding public String value();
}

我們可如下在注入點使用此綁定類型:

@HttpParam("username") String username;

@HttpParam("password") String password;

下列 producer method 可完成此工作:

class HttpParams


   @Produces @HttpParam("")
   String getParamValue(ServletRequest request, InjectionPoint ip) {
      return request.getParameter(ip.getAnnotation(HttpParam.class).value());
   }
}

(請注意,HttpParam 標記的 value() 成員已被 Web Bean 管理員忽略掉,因為它已被標記為 @NonBinding.

Web Bean 管理員提供了實做 InjectionPoint 介面的內建 Web Bean:

public interface InjectionPoint { 

   public Object getInstance(); 
   public Bean<?> getBean(); 
   public Member getMember(): 
   public <extends Annotation
> T getAnnotation(Class<T
> annotation); 
   public Set<extends Annotation
> getAnnotations(); 
}