[weld] bean的生存空间

wuhaixing 2010-04-05
bean的生存空间决定了他的寿命。生存空间还决定了使用者引用的是哪个bean实例。依据CDI规范的定义,scope能决定的是:
  • 创建及销毁其辖内bean实例的时间
  • 给注入点提供其辖内的哪个bean供其引用


(在bean 的世界里,房子不仅决定了bean能活多久,还决定了要嫁给谁。我猜,在那个世界的房价肯定会更高吧!)

比如说,有一个叫CurrentUser的bean住在session scope中,那在相同HttpSession的context中的bean都只认识一个CurrentUser的实例。当这个session中第一次有人需要CurrentUser的时候,container就会创建一个实例放在这个session对应的scope中;当session结束的时候,这个实例也随之消亡。

JPA 实例在这个模型中会觉得不太舒服,因为Entities有自己的生命周期和识别方式,而这些方式和CDI中的方式不是很配。因此,别像对待CDI bean那么对待entities,如果你把entity放在@Dependent之外的scope中,你肯定会遇到麻烦的。如果你要把一个注入的实例交给JPA EntityManager,client proxy会跳起来反对你的。

wuhaixing 2010-04-05
户型

CDI中的context 模型是可扩展的,创建自己的scope type不是不可能的。首先先创建一个新的scope type annotation:
@ScopeType
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface ClusterScoped {}


当然,这是最容易的一步。要让ClusterScoped不是个空架子,还需要定义一个Context对象来实现这个scope所需的功能。这可是个技术活,非常考验功力,要是你还不是框架开发级别的牛人,请勿轻易尝试。在seam以后的版本中,你可能会见到一个叫做business scope的这么个玩意。

可以把scope type annotation放到类前面表明他是住在哪种scope中的bean:

@ClusterScoped
public class SecondLevelCache { ... }
wuhaixing 2010-04-05
上天安排的scope

CDI 规定了container必须提供的四种scope:
  • @RequestScoped
  • @SessionScoped
  • @ApplicationScoped
  • @ConversationScoped

对于使用了CDI的web应用程序:
  • 任何servlet请求都能够访问处于活动状态的request,session和 application scopes
  • 任何JSF请求都能访问活动的conversation scope


要在其他的web框架中支持conversation scope,可以创建一个 CDI extension。

request和application scope在下面各种情况下也会处于活动状态:

  • 调用EJB远程方法时,
  • 调用EJB的异步方法时,
  • EJB超时的时候,
  • 当有消息送达message-driven bean时
  • 当有消息送达MessageListener时
  • 调用web服务时


当应用程序试图访问一个bean时,如果该bean所在的scope没有处于非活动状态的context,container会抛出一个 ContextNotActiveException。

标记为@SessionScoped 或 @ConversationScoped的Managed beans必须是 serializable的,因为container 会一次又一次的 挂起 HTTP session。


wuhaixing 2010-04-05
conversation scope
其他的三种scope应该是广大的web开发者很容易理解的概念,接下来介绍新出场的conversation scope。
conversation scope 有点像session scope,跨越多个请求保存用户与系统之间的交互状态。与session scope不同的是,conversation scope:

  • 由应用程序开始,由应用程序结束;
  • 在JSF程序中保存web浏览器特定tab中的状态(浏览器的所有tab都会共享domain cookies,session cookies)

conversation代表一个任务--从用户角度来看是一项完整的工作。conversation context保存用户当前的工作信息。如果用户同时做很多事情,就会有很多conversation。

conversation context在任何JSF 请求中都处于活动状态。大多数conversation在请求结束的时候终止。如果希望conversation能够继续,必须将其提升为long-running conversation。
wuhaixing 2010-04-05
conversation的界定
在JSF程序中,可以用CDI提供的bean来控制conversation的生命周期。这个bean可以通过注入引用:
@Inject Conversation conversation;


要将与当前request关联的conversation提升为long-running conversation,在代码中调用begin()方法,要在当前的request结束时将当前的long-running conversation销毁,调用end()方法。

下面这个例子演示了conversation-scoped的bean如何控制其所在的conversation:

@ConversationScoped @Stateful
public class OrderBuilder {
   private Order order;
   private @Inject Conversation conversation;
   private @PersistenceContext(type = EXTENDED) EntityManager em;
   
   @Produces public Order getOrder() {
      return order;
   }

  public Order createOrder() {
      order = new Order();
      conversation.begin();
      return order;
   }
   
   public void addLineItem(Product product, int quantity) {
      order.add(new LineItem(product, quantity));
   }

  public void saveOrder(Order order) {
      em.persist(order);
      conversation.end();
   }
   
   @Remove
   public void destroy() {}
}


通过使用Conversation API,这个bean能控制自己的生命周期。但有些bean的生命周期完全取决于其他对象。

Conversation传播

任何JSF faces请求(JSF form提交)或redirect 都会自动传播Conversation context。而non-faces请求则不会自动传播,比如,通过链接导航。

将conversation的唯一标识作为request参数可以强制non-faces请求传播conversation。CDI规范将cid作为传播conversation唯一指定request参数名称。conversation的标识可以通过Conversation对象获取,其EL bean名称为conversation。

因此下面的链接会传播conversation:
<a href="/addProduct.jsp?cid=#{conversation.id}">Add Product</a>


用JSF 2的link组件可能会更好:
<h:link outcome="/addProduct.xhtml value="Add Product">
   <f:param name="cid" value="#{conversation.id}"/>
</h:link>


conversation context能够在跨越redirect传播,因此很容易实现常用的 POST-then-redirect模式,而无需用那些脆弱的方式,比如“flash”对象。container会自动将conversation id作为request参数加到redirect的URL中。


conversation超时
为了节约资源,允许container随时销毁conversation及其保存的状态信息。虽然规范没有要求,CDI的实现通常会引入某种超时机制。当conversation超过一定的时间没有活动时,container就会销毁他。

Conversation对象提供了设置超时时间的方法,这个设置是对container的提示,可以忽略。
conversation.setTimeout(timeoutInMillis);
wuhaixing 2010-04-06
伪空间Singleton

除了4个内置的scope,CDI还提供了两个伪空间。首先出场的是singleton,用@Singleton标明。

@Singleton在 javax.inject 包内,而不像其他scope一样在 javax.enterprise.context包内。

你应该能猜到“singleton”在这里的含义,被标记的bean只会被初始化一次。可这会有个小问题,带有@Singleton标记的bean没有代理,使用者持有的是这个单例实例的直接引用。所以我们得考虑使用者被串行化的情况,比如标记了@SessionScoped 或 @ConversationScoped的bean,或者引用了 @SessionScoped 或@ConversationScoped的bean,或者statuful session bean。

当然,如果这个单例实例是个简单的,不可变的,可串行化的对象,比如string、数字或日期,我们可能还不会那么在意,因为他被串行化仅仅是导致出现重复的实例。如果会使得他不再是真正的Singleton,还不如将他放到默认的scope中。

下面有几种方法可以确保Singleton的bean跟着其使用者一起被串行化后还能保持Singleton的方法:
  • 让Singleton bean实现writeResolve() 和 readReplace() (在java serialization规范里定义)
  • 确保client对Singleton bean的引用是transient
  • 让client引用Instance<X>,而不是直接引用单例实例bean X

最后一种办法,也是最好的办法就是不用他,用@ApplicationScoped,container就会让你通过proxy访问bean,并为你解决串行化的问题。
wuhaixing 2010-04-06
伪空间dependent

终于轮到CDI提供的伪空间dependent了,他是没有显式声明所属scope的bean的默认scope。

比如下面这个bean的scope类型就是 @Dependent:
public class Calculator { ... }


dependent bean的实例不会由不同的使用者或注入点共享。他的对于绝对依赖于其他对象。随其所属对象的创建而创建,随其所属对象的销毁而销毁。

如果dependent bean在EL表达式中引用,则每次计算这个表达式时都会创建这个bean的实例。这个实例不会在计算任何其他表达式时重用。

如果你需要在JSF页面中通过EL 名称直接访问bean,通常不能将其标记为@Dependent。否则任何通过JSF输入而赋给他的值会立即丢失。因此CDI提供了@Model,可以给bean一个名称,并且相当于同时标记为@RequestScoped。如果你确实需要在JSF页面中访问一个@Dependent的bean,把他注入到另外一个bean中,然后通过getter方法提供给EL。

标记为@Dependent的 Beans 不需要代理对象,其使用者是直接引用他的。
wuhaixing 2010-04-06
修饰符@New

内置的修饰符@New允许我们获取bean的dependent对象,即使这个bean被标记为其他的scope。这也是CDI给我们提供的又一个便利。
@Inject @New Calculator calculator;


这个类必须是一个有效的managed bean或session bean,但不需要是被启用的bean。

Calculator可以被标记为不同的空间类型,比如:
@ConversationScoped
public class Calculator { ... }


因此下面这个注入的属性每次都会得到一个不同的Calculator实例:

public class PaymentCalc {
   @Inject Calculator calculator;
   @Inject @New Calculator newCalculator;
}


calculator属性注入的是一个conversation-scoped的Calculator实例,而newCalculator属性注入的则是一个新的Calculator实例,其生命周期绑定到其所属的PaymentCalc上。
这个特性对于producer方法尤其有用。
Global site tag (gtag.js) - Google Analytics