[weld] CDI中的bean

wuhaixing 2010-03-22
虽然把用在web和企业应用中的java 类称为bean已经有年头了,但在Java EE 6之前,并没有对"bean"进行过明确的定义。即使在EE的规范中,被称为“bean”的东西也有不同的含义,EJB bean,JSF的managed bean。此外,还有第三方的框架给"bean"赋予了各自的含义,比如Spring和Seam。虽然bean这个名词大家都很熟悉,但一直没有给他一个通用的、明确的定义。
最终Java EE6在Managed Beans规范中给出了一个通用的定义。Managed Beans被用来指代那些由容器管理(container-managed)的对象,他们和普通的POJO在编程上的区别很小。主要用来提供一些基本服务,比如资源注入,lifecycle回调和关注点切入等。一些辅助规范,比如EJB和CDI,是建立在这个基础模型之上的。这些规范一起为Java EE平台建立了统一的bean概念和轻量组件模型。
通常情况下,任何定义了默认构造方法(无参数)的Java类(或者在构造方法前加了@Inject注解)都是bean。任何JavaBean和EJB session bean都是,你不需要给他们添加任何特殊的代码或配置,只需要把他们打包在含有META-INF/beans.xml的包里,就可以把这些bean注入到他们的调用者之中了(打包是指jar,war或者EJB jar等)。你或许想要了解Why Is Beans xml Required In CDI
你过去编写的那些JavaBean和EJB目前不能利用CDI规范中定义的那些新服务,但你可以把他们和CDI一起用,也就是说,让container来创建和销毁他们的实例;把他们放在特定的context中;把他们注入到其他bean中;把他们用在EL表达式中;用qualifier annotation来区分他们;甚至给他们加上interceptors和decorators。完成这些并不需要你修改代码,好吧,最多需要你加上一些annotations。

原文中的Getting our feet wet
wuhaixing 2010-03-23
bean通常就是应用程序中实现业务逻辑的那些类。你可以在代码中直接调用,也可以通过Unified EL调用,bean也可以访问事务中的资源。bean之间的依赖关系是通过container自动管理的。大多数的bean是放置在特定的上下文环境中,并携带状态的。bean的生命周期也是由container来管理的。
一直在强调的上下文环境(contextual)究竟意味着什么?
既然bean是有状态的,那么所拥有的bean实例具有什么状态就是个值得考虑的问题。不像无状态组件模型(比如无状态会话bean)或singleton组件模型(servlets或者singleton bean),bean的不同使用者所见到的bean状态可能是不同的,使用者所见的bean状态是由他所引用的bean实例来决定的。
然而,与无状态或singleton模型相似,使用者不用关心bean的生命周期,也就是说不用自己创建(new Class())和销毁bean。而是由bean的作用域来决定:
  • bean实例的生命周期
  • 那些使用者引用的是同一个bean实例

在CDI应用程序中的线程里,可能会有一个处于活动状态的context与bean的作用域(scope)相关联。这个context可能是这个线程独享的(比如当bean是request scoped的时候),也可能是与其他特定的线程共享的(比如当bean是session scoped的时候),甚至是与所有线程共享的(如果是application scoped)。
在同一个context中执行的使用者(其他的bean)会看到bean的同一个实例。但在不同context中使用者可能会看到不同的bean实例(由context之间的关系决定)。
contextual模型最大的优势在于能让stateful bean成为服务!使用者无需考虑所使用的bean的生命周期,也无需了解bean的生命周期。Bean之间通过传递消息进行交互,bean的实现决定了他们自身状态的生命周期。
这种松散的耦合关系得益于:
  • bean之间的交互是通过精心定义的public APIs实现的
  • 彼此之间的生命周期互不影响

也就是说,只要实现了同一接口,其他bean就完全可以替代原有的bean,不用考虑生命周期,这种替换不会影响其他bean的实现。实际上,为适应不同的部署要求,CDI提供了一个简单的机制-“Alternatives”。让你可以根据需要选择不同的bean实现部署要求。
还有一点需要注意的是,并不要求bean的使用者也是bean。有些对象,比如servlets或MDB(message-driven beans),他们天生就是不可注入的contextual 对象,但他们还是可以通过injection引用bean。
wuhaixing 2010-03-24
依照规范,bean具有如下属性:
  • 一组bean类型
  • 一组qualifiers (修饰符)
  • Scope(作用域)
  • EL name(这个可以没有)
  • 一组拦截器绑定(interceptor bindings)
  • bean的实现

此外,还可以有一个alternative注解.
wuhaixing 2010-03-25
Bean 类型、修饰符及依赖注入
Bean 对其他beans的引用通常是靠依赖注入来实现的。注入属性向容器说明了被注入的bean必须满足的特性。这些特性包括:
  • bean的类型,以及
  • 一组qualifier(修饰符)

bean类型就是你定义的类或接口;当然,这个类型必须是使用者可见的。比如将EJB Session bean作为bean的时候,bean类型就是@Local接口,或者bean类的本地view。一个bean可以有多个bean类型。比如下面的bean,他有四个bean类型:
public class BookShop 
      extends Business 
      implements Shop<Book> {
   ...
}

他的bean类型是 BookShop, Business and Shop<Book>,还有隐含的类型 java.lang.Object. (注意,参数化类型也能作为bean类型).
而下面这个Session bean的bean类型只有本地接口  BookShop, Auditable 和java.lang.Object ,因为 bean 的类声明, BookShopBean不是使用者可见的类型。
@Stateful
public class BookShopBean 
      extends Business 
      implements BookShop, Auditable {
   ...
}

注意
session bean的bean 类型包括local interface和bean class的local view。EJB remote interfaces不能作为session bean的bean类型。因此,除非将EJB的remote interface定义为资源,不能在使用者中用他的remote interface注入一个EJB。

如果想限定bean对客户可见的类型(也就是bean 类型),可以使用@Typed annotation,在其参数中列出作为bean类型的类。比如下面这个bean的类型被限定为Shop<Book>,当然还有 java.lang.Object:
@Typed(Shop.class)
public class BookShop 
      extends Business 
      implements Shop<Book> {
   ...
}

某些情况下,容器无法单靠bean类型来决定给使用者提供哪个bean。比如,有两个类都实现了PaymentProcessor接口:CreditCardPaymentProcessor 和 DebitPaymentProcessor。注入一个类型为PaymentProcessor的属性(field)会导致歧义的发生。当出现这类情况时,使用者必须给出一些额外的说明,weld为此提供了qualifier annotation来对注入点进行修饰。
qualifier是由用户定义的annotation,其本身由@Qualifer注解。qualifier annotation是类型系统的扩展。有了他,就没必要再用一个字符串方式名字的方式来区分类型(一直强调的typesafe)。下面就是一个qualifier:
@Qualifier
@Target({TYPE, METHOD, PARAMETER, FIELD})
@Retention(RUNTIME)
public @interface CreditCard {}

以前你可能很少见到annotation的定义,甚至有可能是第一次见到。在CDI中,你将越来越熟悉这个小玩意儿,因为你会一次又一次的创建他们。
现在我们就能用刚才定义的那个qualifier annotation来消除歧义了。下面段代码告诉容器我们需要一个类型为PaymentProcessor,并带有修饰符@CreditCard的bean:
@Inject @CreditCard PaymentProcessor paymentProcessor


注意
如果一个注入点没有显式的声明qualifier,那么他会带有一个默认的@Default修饰符。


对于代码中每个使用了依赖注入的位置,容器都会为其搜索符合条件的bean,也就是带有全部qualifier的,并且类型一致的bean。如果找到了,他就把这个bean注入到使用者中,否则,他会向用户报告错误。
那怎么让bean带有qualifier呢?当然是注解bean类(因为qualifier是一个annotation)!下面这个类带有qualifier @CreditCard,并且实现了bean类型PaymentProcessor,所以他就符合注入点的要求:
@CreditCard
public class CreditCardPaymentProcessor 
    implements PaymentProcessor { ... }


注意
与上面保持一致,没有显式的声明qualifier的bean会带有一个默认的@Default修饰符。


故事到这里并没有结束,CDI还定义了一个简单的resolution rule来帮助容器决定发现多个满足要求的bean时该怎么办,这是后话了,暂且按下不表。

wuhaixing 2010-03-26
作用域(Scope)
bean的作用域决定了他的生命周期及其实例的可见性。CDI的context模型可扩展,能够根据需要创建作用域。规范中规定了常用的作用域,由容器提供。每个scope都有对应的annotation类型。
例如,任何web应用程序都需要的session scoped bean:
public @SessionScoped
class ShoppingCart implements Serializable { ... }

session-scoped bean的实例会绑定到用户会话中,并且由那个会话的context中执行的请求所共享。

注意
一旦bean绑定到context,他就会一直存在直到context被销毁,没有办法手工把他移除。所以,如果你不想在整个session中都碰见他,就用一个生命周期比较短的scope,比如request或者conversation。


如果没有显式的说明bean的scope,那bean的作用域是默认的dependent pseudo-scope。这个作用域比较特殊,表明bean的生命周期是由他所注入的对象来决定的,也就是和使用者生命周期一致。

wuhaixing 2010-03-26
EL 名
如果你想在支持Unified EL表达式的地方(非java代码)引用bean,比如说在JSP或JSF页面中,那你必须给bean起一个EL名。
bean的EL名是用注解@Named指定的,如下所示:
public @SessionScoped @Named("cart")
class ShoppingCart implements Serializable { ... }

这样你就可以很容易的喊他回家吃饭了
<h:dataTable value="#{cart.lineItems}" var="item">
   ...
</h:dataTable>


用惯了Seam的弟兄们请注意
并不是因为@Named让class成为了bean。在bean 包里的大多数类都已经被认为是bean了。@Named注解只是让他可以在EL中出现而已,通常是在JSF的页面里。


如果想省事儿,也可以让CDI来帮你给bean起个名字,只要留下@Named就行了,不用非在括号里起个名字。
public @SessionScoped @Named
class ShoppingCart implements Serializable { ... }

这样CDI会用首字母非大写的不完全类名作为bean的名字,在这就是shoppingCart。
wuhaixing 2010-03-26
可选方案
qualifier可以让我们在开发时在一个接口的多个实现中做出选择。但如果想在部署的时候再决定用哪个bean,比如测试环境中的mock实现,那就需要用alternative。还是老办法,用annotation,这次是@Alternative。
public @Alternative
class MockPaymentProcessor extends PaymentProcessorImpl { ... }

通常当一个interface有多个实现时我们才需要这么搞,当把bean加入可选方案之后,在其所在包里的META-INF/beans.xml说明一下用那个就ok了。至于怎么说明,那是后话。反正一般也用不着,别急。
wuhaixing 2010-03-26
拦截器绑定类型(Interceptor binding type)
如果你熟悉EJB 3.0,那么怎么用拦截器应该也很熟悉了。从Java EE 6开始,拦截器的应用范围更广了。没必要为了拦截bean的方法把他打扮成EJB了,爽吧!那么CDI究竟为interceptor做了些什么呢?先来点背景知识
Java EE 5中对拦截器的定义是比较难以理解的。你需要在EJB的实现中直接声明interceptor的实现,在@Interceptors注解或XML配置中。其次,应用interceptor的顺序也是按声明的顺序来的。如果你只是对一个bean应用interceptor,这样也没事,但如果你一次又一次的使用他们,那你有很多机会把应用顺序搞乱。这是个问题。
CDI为将interceptors绑定到bean提供了新的方法,引入了间接绑定的概念。我们必须定义一个interceptor binding type来描述interceptor所执行的操作。
interceptor binding type是由用户定义的annotation,其本身带有@InterceptorBinding注解。这样,拦截器类与bean类之间的关系就不是联系的那么紧密了,绑定关系是间接的。
@InterceptorBinding
@Inherited
@Target( { TYPE, METHOD })
@Retention(RUNTIME)
public @interface Transactional {}

然后用拦截器绑定类型对进行事务管理的拦截器进行注解:
public @Transactional @Interceptor
class TransactionInterceptor { ... }

接下来,我们拦截器绑定类型把interceptor绑定到bean上,还是采用注解的方式:
public @SessionScoped @Transactional
class ShoppingCart implements Serializable { ... }

注意到了吗?TransactionInterceptor和ShoppingCart之间没有任何直接的关联,他们并不知道彼此的存在。
拦截器是在部署的时候说明的(在单元测试的时候是不需要TransactionInterceptor的)。默认情况下不会启用interceptor,需要在META-INF/beans.xml中启用他们,他们的执行顺序也是在此定义的。
Global site tag (gtag.js) - Google Analytics