Java EE 6:EJB 3.1 的变化引人注目

  • Josh Long
  • 张龙

2010 年 2 月 17 日

话题:Java语言 & 开发

Enterprise Java Bean 3.0(EJB 3)规范为 Java 企业级开发画上了浓墨重彩的一笔,规范的制订是非常透明的,很多想法都来源于社区。它代表了更加一致的服务模式,大量使用 POJO 的同时降低了复杂性。Java 5 的注解间接地成就了这种模式,在使其变得更加强大的同时也减少了开发者的工作量。对于各种新的解决方案来说,EJB 3 抛弃了那些差劲的决策,因此会对那些曾经回避 EJB 的开发者产生极大的吸引力。EJB Entity Beans 不见了,取而代之的是 JPA Entity。EJB 2.1 及更早版本所需的大量 Java 类和接口不见了。默认的“约定优于配置”得到了广泛的应用,这样人们上手就会非常快。EJB 3.0 是一场真正的革命。

如果说 EJB 3.0 是一场革命,那么 EJB 3.1 则是一种进化。EJB 3.1 的一些新特性似乎应该放到 EJB 3.0 中,大家应该原谅规范制定者所持有的谨慎态度——让 80% 的内容能够运转良好要比废弃掉规范中某个差劲的模式要好很多,宁缺毋滥。当然了,其发布时机还是不错的。虽然提供了大量的新特性,但 EJB 3.1 规范及所有的向后兼容和补充说明加起来一共才 626 页,比 10 年前的 EJB 2.1 规范少了 14 页。

我们将在本文介绍这些新特性及其使用方式。

EJB 3.1 的新特性

EJB 3.1 最大的变化并不是向平台增加了很多新特性,而是简化了平台的使用。其中一些扩大了用户所用 API 的范围以提升灵活性;另一些只是带来了更多的灵活性而已。

Singleton

Singleton 是一种新型的 Session Bean,提供了一个额外的保证:每个 JVM 上的 Bean 只会被创建一次。该特性在很多地方都有用武之地,比如说缓存。此外还保证每个资源只有一个共享视图存在,而应用服务器并没有提供该特性。无需持久化的有价值的数据可以通过简单存储实现,因为重新创建的代价可能会很大。来看一个具体的示例吧。假设其他地方已经创建好了 User 实体(或许使用了 JPA 2.0)。

@javax.ejb.Singleton 

public class ChatRoom { 

   private java.util.Map<User,Collection<String>> userComments; 

   @PostConstruct 
   public void setup (){ 

     userComments = new java.util.concurrent.ConcurrentHashMap<User,Collection<String>>(); 
     /* ...*/
   }
   public void join(User usr){ /* ...*/ }
   public void disconnect(User usr){  /* ...*/ }

   public void say(String msg){ /* ...*/  }
}

所有的客户端都可以通过 ChatRoom 实例的引用来更新同一个可变的状态。可以保证的是客户端所获得的实例是一样的。由于这是一个 Session Bean,因此与其他 Session Bean 一样都是线程安全的。

@javax.ejb.EJB 
private  ChatRoom  chatRoom ; 

 /* ... */

chatRoom.join(myUser) ;
chatRoom.say( "Hello, world!");
chatRoom.disconnect();

设计 Singleton 的目的就是用于并发访问。规范赋予了开发者复杂的控制手段来解决并发访问问题。我们可以通过容器以声明的方式指定某种访问类型,该行为是默认的;可以通过注解 @javax.ejb.ConcurrencyManagement(CONTAINER) 显式指定使用容器管理的并发手段。如果想对 Bean 进行更多的控制,请使用 @javax.ejb.ConcurrencyManagement(BEAN)。凭借容器管理的并发手段,我们可以在方法或类层次上指定访问类型。可以在默认情况下于类层次上使用 @javax.ejb.Lock(WRITE) 注解以保证所有的业务方法都是可序列化的,然后针对特定的“只读”方法再进行优化,这么做不会产生任何的副作用。对于只读方法需要使用注解 @Lock(READ)。对于 @Lock(WRITE) 注解所修饰方法的所有访问都是可序列化的,同时会阻塞客户端的访问直到前一个访问完成,或是出现超时的情况。可以通过 @AccessTimeout 注解指定超时的时间,该注解需要一个 java.util.concurrent.TimeUnit 值。现在我们可以使用这种并发控制了,先删除之前的 ChatRoom 实现代码。

@javax.ejb.Singleton 
@javax.ejb.Lock(WRITE)
public class ChatRoom { 

   private java.util.Map<User,Collection<String>> userComments; 

   @PostConstruct 
   public void setup (){ 
     userComments = new java.util.concurrent.ConcurrentHashMap<User,Collection<String>>(); 
     /* ...*/
   }

   public void join(User usr){ /* ...*/ }
   public void disconnect(User usr){  /* ...*/ }
   public void say(String msg){ /* ...*/  }

   @javax.ejb.Lock(READ)
   public int getCountOfActiveUsers(){ /* ... run through the map and count ... */ } 
}

显然现在这个 ChatRoom 实现还是有问题的:一旦用户和帖子的数量超出了应用服务器所能承受的内存极限就会出现内存不足的问题。可以使用过期(expiration)机制来解决这个问题。为了能说的明白点,假设有一个垃圾收集器周期性地检查 ChatRoom 的帖子并根据 LRU 原则(或是使用 JPA 和 EntityManager.persists,然后再销毁)销毁过期的聊天数据。

EJB Timer

从 EJB 2.1 开始 EJB 就拥有一个 Timer 机制,然而遗憾的是该机制总是使用毫秒间隔数,用起来也十分不爽。EJB 3.0 对此进行了一些改进,但本质上却没有什么改变,还是基于时间间隔。因此,如果想在每周的开始执行一项活动就比较难办了。EJB 3.1 对此又进行了一些改进,提供了一种声明式、灵活的定时器服务并参考了其他调度器,如 Quartz 和 Flux。对于大多数简单的调度来说(包括 CRON 风格的调度),EJB 3.1 是一个完美的解决方案。再来看看 ChatRoom 吧,我们想周期性地回收旧数据。

@javax.ejb.Singleton 
public class ChatRoom {

   private java.util.Map<User,Collection<String>> userComments;
        
   @javax.ejb.Schedule(minute="1", hour="*") 

   public void cleanOutOldDataFromChatLogs() { 
      /** ... not reprinting all the existing code ... */
   }
}

我们编写了一个方法来快速检查数据,如果必要的话就会删除旧的聊天记录,这样一切就尽在掌握之中,不会再出现内存吃紧的问题了。这里使用了声明式模型以保证方法每隔一小时都会运行,当然了,我们仍然从 EJB 3.0 中注入了 TimerService。

无接口的视图

EJB 3.0 中的 Bean 至少要实现一个接口(Local 或是 Remote 的业务接口),接口用作 Bean 对于客户端的视图。虽然间接地使用接口是个很强大的技术,但有时会使事情变得复杂。在 EJB 3.1 中可以编写没有接口的 Bean。客户端所看到的视图就是类所公开的 public 方法。

@javax.ejb.Stateless 
public class Calculator { 

  public float add ( float a , float b) { 
    return a + b; 
  } 

  public float subtract (float a , float b){ 

    return a - b ; 
  }

} 

该 Bean 的客户端可以通过正常的注入来获取 Bean 并调用其方法:

@javax.ejb.EJB 
private Calculator calculator; 

...

float result = calculator.add( 10 , 10 ) ;
...

异步服务

处理可伸缩问题最简单的办法就是不去处理(直到容量不足时才考虑)!这种方式就是人尽皆知的 SEDA(staged event driven architecture)模式,可以通过排队来避免瓶颈的出现。通过这种方式为任务排队,同时客户端可以继续后面的处理。如果组件的处理需要花费很长时间,同时系统没有过载,那么这种模式可以保证处理时间较长的组件不会把系统搞糟。

处理可伸缩性问题的另一个方法就是不要在单向消息交换时阻塞客户端的调用。此外还可以进行异步的处理,让客户端继续后面的处理直到结果返回。所有这些方法都加到了 EJB 3.1 中新的异步服务支持当中。我们可以在 Bean 或是单个方法上使用注解 @javax.ejb.Asynchronous 以告诉容器在调用结果返回前不要阻塞客户端的执行,这样客户端就可以继续后续的处理,从理论上来说,这种方式可以让容器缓存待处理的任务直到其可以处理为止。

如果使用 @Asynchronous 来注解 Bean 类或是业务接口,那么该注解会应用到 Bean 上的所有方法;否则只有使用了 @Asynchronous 注解的方法才会变成异步的。异步方法可以返回 void 或是 java.util.concurrent.Future<V> 的实例。客户端可以通过 Future<V> 实例来查询返回的结果,但方法调用后客户端可以继续后续的处理而不会被阻塞。在这种方式下,即便 EJB 花费了 1、2 个小时的处理时间也无所谓,因为客户端并不会受到任何影响。从概念上来看,这与向 JMS 队列发送请求来调用服务的方式是一致的。

Future<V> 实例可用于取消任务或是等待结果。客户端的事务上下文不会被传播到异步方法中,这样对于异步方法来说,REQUIRED 要比 REQUIRES_NEW 更具效率。

看个具体的示例吧:要构建的服务需要与几个 Web Services 通信并综合使用调用的结果。我们需要结果,但却不能挂起客户端的请求(或许是个网页)。如下代码展示了该服务:p>

@javax.ejb.Stateless
public CarHotelAndAirLineBookingServiceBean implements CarHotelAndAirLineBookingService  {
  @javax.ejb.Asynchronous  
  public Future<BookingConfirmation> bookCarHotelAndAirLine( Car rental, Hotel hotel, AirLine airLine) { 
    /**  ...  */

   } 
 } 

我们在客户端(一个 JSF action)这样调用服务:

@Named 
public BookingAction { 

        @javax.ejb.EJB  private  CarHotelAndAirLineBookingServiceBean bookingService; 

        private Future<BookingConfirmation> confirmation;

        public String makeRequest(){ 

                Hotel hotelSelection = ... ;
                Car rentalSelection = ... ;

                AirLine airLineSelection = ... ; 

                confirmation = bookingService.bookCarHotelAndAirLine(
                    rentalSelection, hotelSelection, airLineSelection ) ; 
                return "showConfirmationPending";
        }

        public Future<BookingConfirmation> getConfirmation(){ /* ... */ }

        /* ... */
}

简化的部署

EJB 3.1 还首次提供了动态、更加简化的部署方式,支持.WAR 文件内的部署。在打包到 WEB-INF/classes 或是 WEB-INF/classes 下的.jar 文件中时,具有组件定义(component-defining)注解的类将成为 Enterprise Bean 组件。此外,还可以通过 WEB-INF/ejb-jar.xml 文件来定义 Enterprise Bean。打包到.WAR 中的 Bean 共享单独的命名空间并成为.WAR 环境的一部分。这样,将.jar 打包到 WEB-INF/lib 下就与将 class 文件放到 WEB-INF/classes 中是一样的了。

新规范另一个值得注意的特性就是 EJB Lite。对于很多应用来说,EJB 技术显得过于庞大了。EJB Lite 提供了 EJB 的一套子集,关注于 Session Bean 的使用。它提供了一种方式以嵌入式风格来使用 EJB 组件,这简化了单元测试。EJB Lite 支持无状态、有状态以及单例的 Session Bean。Bean 可以有 Local 接口,也可以没有接口。他们可以与拦截器协同工作并使用事务和安全等容器服务。

EJB 3.1 是个强大的开发者工具集,能满足应用 80% 的需要。规范的未来是光明的,同时也首次明确地提及了未来 Java SE 的裁剪机制将要移除的特性。未来可能会移除的特性包括老式的容器管理和 Bean 管理的持久化、Entity Bean 的 EJB 2.1 客户端视图、EJB QL(EJB 2.1 的查询语言)以及基于 JAX-RPC 的 Web Service 支持(包括 J2EE 1.4 引入的端点和客户端视图)。显然,EJB 3.1 是个引人注目、向后兼容的升级,它代表了 5 年前开始的 JSR 220(EJB 3.0)的新进展。

查看英文原文:Java EE6: EJB3.1 Is a Compelling Evolution

Java语言 & 开发