SeamFramework.orgCommunity Documentation

第 4 章 依赖注入

4.1. 绑定注释
4.1.1. 成员绑定注释
4.1.2. 绑定注释的组合
4.1.3. 绑定注释和生产者方法
4.1.4. 默认的绑定类型
4.2. 部署类型
4.2.1. 激活部署类型
4.2.2. 部署类型优先级
4.2.3. 部署类型样例
4.3. 修正没有满足条件的依赖
4.4. 客户代理
4.5. 通过编程查找获得一个Web Bean
4.6. 生命周期回调方法,@Resource@EJB@PersistenceContext
4.7. InjectionPoint 对象

Web Beans支持三种主要的依赖注入机制:

构造器参数注入:

public class Checkout {

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

初始化 方法参数注入:

public class Checkout {

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

和直接的域注入:

public class Checkout {


    private @Current ShoppingCart cart;
    
}

当Web Bean实例被首次初始化时,依赖注入总是随之发生。

EJB Beans不支持构造器参数注入,因为EJB是由EJB容器负责实例化,而不是Web Bean管理器负责。

当应用默认绑定类型@Current时,构造器的参数和初始化方法需要显式地注释。然而,即使应用了默认的绑定类型,注入域 也必须 指定一个绑定类型。如果一个域没有指定绑定类型,这个域将不会有任何注入发生。

生产者方法也支持参数注入:

@Produces Checkout createCheckout(ShoppingCart cart) {

    return new Checkout(cart);
}

最后,观察者方法(我们将在 第 9 章 事件一章中讨论),清除(disposal)方法和解构(destructor)方法都只支持参数注入。

Web Beans规范定义了一个称为 类型安全解析算法的过程,当在注入点识别所注入的Web Bean时,Web Bean管理器会遵循这个过程。这个算法初看起来非常复杂,然而你一旦理解了它,它就相当直观。类型安全解析在系统初始化的时候执行,这意味着如果Web Bean的依赖无法被满足的时候,管理器将立刻通知用户,抛出一个 UnsatisfiedDependencyException 异常或者 AmbiguousDependencyException 异常。

这个算法的目的是允许多个Web Beans实现相同的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应该被注入。

使用域注入:

@PayByCheque PaymentProcessor chequePaymentProcessor;

@PayByCreditCard PaymentProcessor creditCardPaymentProcessor;

使用初始化方法注入:

@Initializer

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

或者使用构造器注入:

@Initializer

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

所有的Web Beans都有一个 部署类型 。每个部署类型标识一套Web Beans,这套Web Beans会有条件地在某些系统部署下面被安装。

例如,我们可以定义一套为名为 @Mock 的部署类型,这种部署类型用来标识只应该在整合测试环境下才在系统中安装的Web Beans:

@Retention(RUNTIME)

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

假定我们有一些和外部系统交互的Web Beans来处理付费:

public class ExternalPaymentProcessor {

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

因为这个Web Bean并不显式地指定一个部署类型,因此它有一个默认的 @Production 部署类型。

在整合测试或者单元测试期间,外部系统可能会很慢或者无法获得,所以我们可能会创建一个模拟对象:

@Mock 

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

但是Web Bean管理器如何决定在一个特定部署中使用哪一个实现?

如果你注意的话,你可能想知道Web Bean如何决定使用哪个实现— ExternalPaymentProcessor 还是 MockPaymentProcessor —。思考一下当管理器遇到这个注入点时会发生什么:

@Current PaymentProcessor paymentProcessor

现在有两个Web Bean满足 PaymentProcessor 合约。当然,我们无法使用一个绑定注释来消除这个歧义,因为绑定注释在注入点被硬编码到了源代码中,并且我们希望管理器在部署时能够决定注入哪一个Web Bean!

这个问题的解决方案是每个不同的部署类型都拥有不同的 优先级 。部署类型的优先级由它们在 web-beans.xml 中出现的顺序决定。在我们这个例子中, @Mock@Production 出现的晚,所以它拥有更高的优先级。

无论何时管理器发现有多个Web Bean能够满足一个注入点指定的合约(API类型加上绑定注释),它都会考虑Web Beans的相对优先级。它将注入拥有更高优先级的Web Bean。在我们这个例子中,当系统运行在整合测试环境中(这是我们想要的),Web Bean管理器将注入 MockPaymentProcessor 对象。

将其和当今流行的管理器体系比较是很有意思的。各种 "轻量级"的容器也许也可以支持条件化部署在类路径中的类,但是这些需要部署的类必须显式地,逐个地列在配置代码或某个XML配置文件中。Web Bean当然支持通过XML来定义和配置Web Bean,但是多数情况下,只要不需要复杂的配置,部署类型可以使用一行XML就能配置一整套Web Beans。同时,浏览代码的开发者可以很容易识别这些Web Bean应该部署在哪些场景中。

考虑到所有实现一个注入点API类型的Web Bean的绑定注释和部署类型类型,如果解析算法失败,那么Web Bean管理器无法识别究竟应该注入哪个Web Bean。

通常我们很容易修正一个UnsatisfiedDependencyException 或者 AmbiguousDependencyException

要修正一个 UnsatisfiedDependencyException,只须简单的提供一个实现API类型的Web Bean并且拥有注入点的绑定类型 — 或者激活一个已经实现API类型并且拥有绑定类型的Web Bean的部署类型。

要修正一个 AmbiguousDependencyException,我们需要引入一个绑定类型来区分API类型的两个不同的实现,或者改变其中一个实现的部署类型以便Web Bean管理器可以使用部署类型优先级来决定究竟部署哪一个实现。只有两个Web Bean共享一个绑定类型并且拥有相同部署类型的时候才会抛出 AmbiguousDependencyException

使用Web Bean依赖注入的时候还需要注意一个问题。

注入的Web Bean的客户通常不会直接拥有这个Web bean实例的引用。

想象一下如果一个应用范围的Web Bean能够拥有一个请求范围的Web Bean的直接引用。应用范围的Web Bean是被很多不同的请求共享的。但是不同的请求应该看到不同的请求范围的Web Bean实例!

现在再想象一个会话范围的Web Bean拥有一个应用范围的Web Bean的直接引用,会话上下文常常被序列化到硬盘以便更高效的使用内存。但是,应用范围的Web Bean实例不能和会话范围的Web Bean一起被序列化!

因此,除非Web Bean使用默认的 @Dependent 的范围,Web Bean管理器必须通过一个代理对象来间接地拥有所有注入的Web Bean引用。这个 客户代理 负责确保收到方法调用的Web Bean实例就是当前上下文相关联的实例。客户代理也允许诸如绑定到会话上下文的Web Bean可以序列化到硬盘中,而无需递归地序列化注入到这个Web Bean中的其他的Web Bean。

不幸的是,由于Java语言的限制,一些Java类型无法被Web Bean管理器代理。因此,如果注入点的类型无法被代理的话,Web Bean管理器会抛出一个 UnproxyableDependencyException 异常。

下面的Java类型无法被Web Bean管理器代理:

修正 UnproxyableDependencyException 很容易。只需简单的想注入类添加一个无参构造器,引入一个接口或者将注入的Web Bean的范围 @Dependent 即可。

应用可以通过注入获得一个 Manager 接口实例:

@Current Manager manager;

Manager 对象提供一套通过编程获得一个Web Bean实例的方法。

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

绑定注释可以通过编写一个帮助类 AnnotationLiteral 的子类来指定,否则很难在Java中实例化一个注释类型。

PaymentProcessor p = manager.getInstanceByType(PaymentProcessor.class, 

                                               new AnnotationLiteral<CreditCard
>(){});

如果绑定类型拥有一个注释成员,我们无法使用 AnnotationLiteral 的匿名子类— 相反,我们需要创建一个具名子类:

abstract class CreditCardBinding 

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

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

企业级Web Beans支持所有EJB规范定义的生命周期回调方法:@PostConstruct, @PreDestroy, @PrePassivate@PostActivate

简单的Web Bean只支持 @PostConstruct@PreDestroy 回调。

企业级和简单的Web Bean都支持使用f @Resource, @EJB@PersistenceContext 来分别注入Java EE资源,EJB和JPA持久化上下文。简单的Web Bean不支持使用 @PersistenceContext(type=EXTENDED)

@PostConstruct 调用总是在所有依赖注入之后发生。

我们有一些依赖对象— @Dependent 范围的Web Bean — 需要知道它们所注入的对象或者注入点的信息,以便能够实现其功能。例如:

一个 @Dependent 范围的Web Bean可以注入一个 InjectionPoint 实例并且访问这个注入点相关的元数据。

我们看一个例子。下面的代码很冗长脆弱,有重构问题:

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

这个生产者方法允许你注入一个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;

下面的生产方法能够完成这个工作:

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(); 
}