EJB3.0先睹为快(程序员0408期文章)

EJB 3.0先睹为快

撰文/透明

在过去的两个月里,我们不断地从各种渠道听说关于EJB 3.0的种种流言。在TheServerSide年会和JavaOne大会上,围绕着EJB 3.0展开的讨论总是热烈中的最热烈者。而在仅仅看到只鳞片爪的J2EE开发者们这里,看待EJB 3.0的态度也是各有不同——轻量级方案的推崇者们将它视为J2EE旧势力的颠覆、轻量级技术的全面胜利;EJB的拥趸们则轻描淡写地声称这里只有一些换汤不换药的表面文章。就在上周(准确地说, 6 24 ),Sun发布了EJB 3.0规范的第一个预览版本,我们终于有机会一窥神秘面纱背后的庐山真面目。尽管最终的规范或许还要过一年才能确定,尽管规范的内容还有可能发生变化,但作为一个负责的J2EE开发者、一个好奇的程序员,我们还是有必要尽早了解这个未来的主流技术。现在,就让笔者陪你一道进入EJB 3.0的新世界吧。

对读者的要求

原则上这只是一篇介绍性文章,不涉及过分细节的内容。但为了更好地理解EJB 3.0的概念,读者最好拥有EJB 2.1的基础知识。另外,如果能够预先准备一些关于Java 1.5的知识(例如JSR 175 metadata annotationJSR 14 generic),对于阅读本文将大有帮助——读者可以在2003年第6期《程序员》的《了解“猛虎”六大利器》一文中获得这些信息。

总览

相比EJB 2.1而言,EJB 3.0最大的改进在于简化了EJB的开发工作。出于兼容性的考虑,实现EJB 3.0规范的应用服务器将完全支持EJB 2.1,新的规范也没有替换或废弃任何旧有的API。也就是说,如果你愿意的话,你可以完全忽略EJB 3.0带来的新特性,仍然沿着2.1的方式去开发EJB。相信这对于很多应用开发商来说是个好消息,毕竟技术的更迭总是会带来一些阵痛。

EJB 3.0最大的目标是从应用开发者的角度最大限度地降低EJB体系结构的复杂性。直到现在,开发EJB仍然意味着继承一些与业务无关的接口(例如javax.ejb.EJBObject)、操作一些与业务无关的对象(例如JNDI查询、EJBHome)、编写复杂的配置文件。一个良好的IDE(例如Weblogic Workshop)可以简化这些工作,但使用IDE往往又意味着厂商绑定和应用服务器绑定。EJB 3.0希望从根本上化简这些复杂的工作,毕竟这里的大部分工作是相当机械性的。

为了达到这个目标,EJB 3.0采取了下列措施:

?         Java元数据标注(metadata annotation)记录EJB元数据(例如EJB的类型、配置信息等)。使用元数据标注可以减少开发者所需提供的工件(artifact)数量,例如配置描述符(deployment descriptor)就将完全由应用服务器自动生成,EJB接口也可以从一个POJOPlain-Old Java Object,普通Java对象)类自动生成。

?         采用“只配置特殊情况”的策略:在大多数情况下使用默认配置,只有在特殊情况下才由开发者编写元数据配置,从而避免了开发者针对大量常见、预料之中的行为编写配置。

?         借助元数据标注、依赖注入机制[1]和简单的查找定位机制,将对应用服务器环境的依赖和JNDI访问封装起来,降低开发和使用的难度,并且提升EJB组件的可移植性和可复用性。

?         简化EJB的类型。新的EJB组件更类似于POJO(或者说,普通的Java Bean),运行时所需的接口类型将由应用服务器根据元数据标注自动生成。EJB的业务接口无须再继承session bean或者entity bean的特定接口,而是一个普通Java接口(实际上,entity bean无须实现任何接口)。并且ejbCreate()ejbRemove()之类的回调方法不再是必须实现的,从而大大提升了EJB组件的可测试性:你可以在EJB容器之外测试它们。

?         客户端无须使用Home接口。

?         简化了容器管理持久化(Container-Managed PersistenceCMP)的使用,提供了一组用于指定O/R mapping属性的元数据标注;同时,提供了更强大的O/R mapping能力:支持轻量级的领域建模,支持entity bean的继承和多态操作,极大地增强了EJB QL的功能。

?         减少了对checked exception的使用。

下面,我们来亲身感受一下EJB 3.0的开发体验,我想你一定会喜欢这种体验。

EJB类和业务接口

在使用EJB 3.0 API编程时,EJB类是开发者需要提交的主要(通常也是唯一)工件。这个类是一个典型的POJO bean,开发者需要编写它的全部代码,并可以用EJB 3.0规范提供的Java元数据标注对EJB类及其元素进行标注,以便生成其他工件——当然,按照“只配置特殊情况”策略,如果不书写大多数标注,EJB容器也会生成默认的工件。

EJB类必须用元数据标注声明自己的类别(session beanentity bean或者MDB),或者实现javax.ejb.EnterpriseBean接口的某个子接口(SessionBeanEntityBean或者MessageDrivenBean)。通常的做法是采用元数据标注,可用的标注包括@Stateless@Stateful@MessageDriven@Entity。如果通过元数据标注声明类别,EJB类就无须实现任何由EnterpriseBean子接口定义的回调方法;而如果实现了这些回调方法,EJB容器也会按照EJB 2.1规范指定的方式调用它们。

下面是一个有状态session beanEJB类示例代码:

@Stateful public class CartBean implements ShoppingCart {

    private float total;

    private Vector productCodes;

    public int someShoppingMethod(){...};

    ...

    ejbRemove() {...};

}

可以看到,这个类与普通Java Bean唯一的区别只在于开始处的“@Stateful”标注。而在EJB容器之外,这个标注将被直接忽视,不会造成任何影响。另外,CartBean类实现了SessionBean接口声明的ejbRemove()方法,EJB容器会在该组件被移除时调用该方法;如果不实现这个方法,组件被移除时就只进行默认的操作。

按照“针对接口编程”的原则,session beanMDB应该拥有一个业务接口(entity bean则不必,因为它多半只是简单的、由一组getter/setter组成的Java Bean)。MDB的业务接口通常已经由消息类型确定了(譬如使用JMS时,MDB的业务接口就应该是javax.jms.MessageListener),最值得关注的就是session bean的业务接口。由于session bean通常代表业务逻辑的实现,讨论它的业务接口也最有意义。

EJB类的业务接口无须继承任何来自EJB规范的接口。换句话说,业务接口完全是一个POJIPlain-Old Java Interface,普通Java接口)。业务接口可以先于EJB类设计、并由EJB类实现,也可以从EJB类中自动生成。一个EJB类可以实现多个业务接口。

业务接口将被默认为本地接口(等效于使用@Local标注),开发者可以用@Remote标注将其声明为远程接口。如果EJB类实现了多个业务接口,那么每个业务接口都必须显式使用@Local@Remote@WebService标注声明自己的访问方式。下面是一个无状态session bean和它的业务接口示例代码:

/*

* The bean class implements the Calculator business interface:

*/

@ Stateless public class CalculatorBean implements Calculator {

    public float add(int a, int b) {

       return a + b;

    }

    public float subtract(int a, int b) {

       return a - b;

    }

}

public interface Calculator {

    public float add(int a, int b);

    public float subtract(int a, int b);

}

如果EJB类没有实现任何接口,容器就会自动为它生成一个业务接口。在这种情况下,EJB类的名称必须以“Bean”、“Impl”或“EJB”字样作为结尾,生成的业务接口名称则将去掉这些后缀(例如CartBean类生成的业务接口名称为Cart)。如果EJB类没有用@BusinessMethod标注将任何方法标注为业务方法,自动生成的业务接口将包含EJB类中所有的public方法。下面的示例代码等效于前一段代码,但业务接口将由EJB容器自动生成:

/*

* This class definition causes the Calculator

* interface to be generated as a local business interface.

* This interface will have the methods add and subtract.

*/

@ Stateless public class CalculatorBean {

    public float add(int a, int b) {

       return a + b;

    }

    public float subtract(int a, int b) {

       return a - b;

    }

}

业务接口可以声明抛出任意类型的异常,但业务方法的实现不应抛出java.rmi.RemoteException异常,即便业务接口被标注为@Remote@WebService也不行。如果确实出现了网络协议级的异常,容器将抛出一个EJBException,并将RemoteException包装在其中。

此外,尽管获得的是一个业务接口类型的组件,但客户端仍然可以将session bean对象强制转型成传统的EJB接口(EJBObjectEJBLocalObject),因为容器会对业务接口进行扩展。例如下列代码就是可行的:

Calculator calc;  // Calculator是一个session bean业务接口

EJBLocalObject ejbO = (EJBLocalObject)calc;

上下文环境和依赖注入

任何一个组件都不能脱离上下文环境而存在,EJB也不例外。在2.1版本中,EJB容器并不对组件的上下文环境做更多的管理。如果一个EJB需要使用另一个EJB,它就必须通过JNDI找到并创建后者的实例。在EJB 3.0中,容器对组件上下文管理投入了更多的关注。EJB可以借助元数据标注要求EJB容器对其进行依赖注入,将自己需要访问的上下文环境通过成员变量或setter方法注入进来;也可以通过javax.ejb.EJBContext接口新增的lookup()方法来获得容器中的任何一个EJB或资源。熟悉Spring框架的读者也许会对这样的设计记忆犹新——没错,就像BeanFactory的设计。从概念上来说,EJB 3.0容器的组件管理部分功能与Spring毫无二致,下面我们来看看其中的一些细节。

EJB 3.0允许两种形式的依赖注入:成员变量注入和setter方法注入。setter方法注入(也就是我们常说的type 2 IoC[2])我们已经很熟悉了;如果把一个成员变量直接声明为public,并加上适当的标注,EJB 3.0容器也会对其进行依赖注入。也就是说,下列两种依赖注入方式是等效的:

    // 成员变量注入

    @EJB public ShoppingCart myShoppingCartBean;

   

    // setter注入

    @EJB public void setMyShoppingCartBean(ShoppingCart aCart) {

       myShoppingCartBean =  aCart;

    }

如果需要注入的是一个EJB组件(更准确地说,一个session bean),则应该使用@EJB标注;如果需要注入的是位于EJB环境中的外部资源(例如数据库连接、事务对象等),则应该使用@Resource标注。使用这两个标注时,还需要指定待注入资源(或EJB)的名称(name属性)、JNDI全名(jndiName属性)、待注入EJB的业务接口(businessInterface属性)等信息,不过这些信息大多可以根据程序代码自动推算出一个合理的默认值。如果所有信息都采用默认值,那么连标注名称都可以不必区分,统统采用@Inject标注即可。例如:

    @Inject public DataSource myDB;

    @Inject public ShoppingCart myShoppingCart;

除了EJBContext之外,EJB 3.0还规定了一个全局的上下文环境入口类:javax.ejb.Context。它的声明及用法如下:

package javax.ejb;

 

public class Context {

    public Object getObject(String objectName) {...}

    public String[] getObjectNames() {...}

    public boolean isObject(String objectName) {...}

}

 

Context context = new Context();

通过这个入口对象,可以访问EJB环境中所有的资源及组件。

entity beanO/R mapping

EJB 3.0规范中,entity bean是一种轻量级的领域对象。在这里,“轻量级”的意思是:使用EJB 3.0 API时,entity bean不需要Home接口或任何组件接口,最重要(通常也是唯一)的工件就是entity bean类本身——这是一个具体类,而不是(像EJB 2.1 CMP所要求的那样)一个抽象类。

CMP在持久化entity bean时有两种策略:如果@Entity标注的access属性被指定为PROPERTY(这也是默认的取值),则按照Java Bean命名规则,持久化所有的bean属性(被标注为@Transient的属性除外);如果access属性被指定为FIELD,容器就直接访问entity bean的成员变量,将本身不是transient、又没有被标注为@Transient的成员变量视为持久化字段。需要注意的是,如果采用第二种策略,这些被作为持久化字段的成员变量必须是protected或者public的;如果采用第一种策略,需要持久化的bean属性对应的settergetter则必须是protected或者public的。在我看来,第一种策略(根据Java Bean属性进行持久化)更加合理,也更容易理解。

每个entity bean必须有一个唯一标识(或者叫主键,primary key),这个主键必须是下列类型之一:Java原生类型(例如charint)、原生类型的包装类型(例如CharInteger)、String类型、或者由多个符合上述类型约束的字段组成的主键类。如果使用自定义的主键类,那么必须覆盖该类型的equals()hashCode()方法。

作为轻量级的领域对象,EJB 3.0 entity bean的创建并不需要像从前那样大费周章,使用者可以直接new出一个entity bean,并对其赋值。此时的entity bean仅仅是一个普通Java Bean,还没有持久化唯一标识,也没有与持久化上下文关联。随后,使用者可以借助EntityManagerentity bean与持久化上下文关联,并赋予它唯一标识。EntityManager是所有entity bean的持久化管理接口,任何entity beanCRUD和查询操作都必须通过它来进行。熟悉Hibernate[3]的读者会对这个接口倍感亲切,因为它与HibernateSession接口非常相似。下面就是EntityManager接口中的主要方法:

package javax.ejb;

 

public interface EntityManager {

    public void create(Object entity);

    public < T > T merge(T entity);

    public void remove(Object entity);

    public Object find(String entityName, Object primaryKey);

    public < T > T find(Class < T > entityClass, Object primaryKey);

    public void flush();

    public Query createQuery(String ejbqlString);

    public Query createNamedQuery(String name);

    public Query createNativeQuery(String sqlString);

    public void refresh(Object entity);

    public void evict(Object entity);

    public boolean contains(Object entity);

}

相比EJB 2.1EJB 3.0 entity bean在结构上最大的改进或许就是对继承的支持。一个entity bean类可以继承另一个entity bean类,而且EJB 3.0还支持entity bean的多态关联和多态查询。在使用继承时,子类和父类的ID字段必须是同一个。

将一个有继承关系的类体系映射成数据库表结构时,可选的映射方案有以下三种:

?         “一个类体系一张表”策略:将类体系中所有的字段(不论来自父类还是子类)映射到同一张表中。这样做的优点是访问快捷、多态查询方便,缺点则是有较多的冗余字段,空间浪费较大。

?         “每个类一张表”策略:将每个类的所有字段(不论来自父来还是本身)映射到一张独立的表中。这样做在空间方面没有浪费,读写/更新操作也很快速,但多态查询的能力非常差,需要使用UNION查询或者子查询才能实现多态查询。

?         “连接子类”策略:将每个类(不论是抽象类还是具体类)本身的字段映射到一张独立的表中。这样做很方便进行多态查询,也没有什么空间浪费,但创建和查询的操作性能较低,一次普通的查询就需要一次甚至几次(视继承的层数而定)连接操作才能完成。

EJB 3.0仅仅要求应用服务器支持“一个类体系一张表”的映射策略。另外两种映射策略在3.0版本中是可选的,但在3.1版本中可能成为必备特性。

查询接口和EJB QL

EJB 2.1版本中,entity bean的查询能力是它饱受诟病的特性之一。Floyd Marinescu的《EJB设计模式》一书中竟然有“用JDBC读取数据”(JDBC for Reading)这么一个模式,entity bean的查询能力之弱可见一斑。

EJB 3.0 entity bean的查询功能主要是通过Query接口来实现的。从前面提到的EntityManager接口可以看到,有三种方式可以创建一个Query对象:

?         调用createQuery方法,用一个EJB QL语句作为参数创建查询;

?         调用createNamedQuery方法,创建一个命名查询(named query);

?         调用createNativeQuery方法,用一个原生SQL语句作为参数创建查询。

也就是说,EJB 3.0 entity bean不仅能执行EJB QL查询,还能执行原生SQL语句。Query接口支持分页等必要功能,出于篇幅限制,我们不列出它的完整定义,下面是使用它的一个例子:

    public List findWithName(String name) {

       return em.createQuery(

"SELECT c FROM Customer c WHERE c.name LIKE :custName")

           .setParameter("custName", name)

           .setMaxResults(10)

           .listResults();

    }

命名查询(named query

EJB 2.1 CMPHibernateO/R mapping框架中,命名查询通常是以方法的形式出现的:findByLoginName()方法代表“按登录名查询”、findByRole()方法代表“按角色查询”,等等。而在EJB 3.0中,命名查询完全用元数据标注来声明,无须创建额外的方法。例如下面就是一个命名查询:

    @NamedQuery(

       name="findAllCustomersWithName",

       queryString="SELECT c FROM Customer c WHERE c.name LIKE :custName"

    )

使用这个命名查询的代码如下所示:

    @Inject public EntityManager em;

    customers = em.createNamedQuery("findAllCustomersWithName")

           .setParameter("custName", "Smith")

           .listResults();

EJB QLentity bean查询的重要工具。在EJB 3.0中,EJB QL有了很大的强化和提升,已经成为了一种真正可用的查询语言。下面,我们就来看看EJB 3.0 QL的主要特性。

?         批量操作。新的EJB QL加入了批量更新/删除的操作。需要注意的是,批量操作只能针对同一类型(及其子类型)的entity bean进行。批量更新/删除的QL示例如下:

DELETE

FROM Customer c

WHERE c.status = ‘inactive’

       UPDATE Customer c

SET c.status = ‘outstanding’

WHERE c.balance < 10000

?         连接(JoinEJB 3.0 QL支持内连接(inner Join)和外连接(outer Join)操作。连接可以根据对象本身的属性进行,也可以根据关联对象的属性进行。例如下列两个连接语句都是合法的:

SELECT c FROM Customer c, employee e WHERE c.hatsize = e.shoesize

SELECT c FROM Customer c JOIN c.orders WHERE c.status = 1

?         投影(ProjectionEJB 3.0 QL不仅能够查询得到entity bean。实际上,用EJB 3.0 QL进行查询时,你可以只取出自己关心的字段,甚至可以将多个entity bean的字段组合起来返回,这就是“投影”能力。例如下列查询:

SELECT c.id, c.status

FROM Customer c JOIN c.orders o

WHERE o.count > 100

将返回一个List对象,其中的每个元素是一个Object数组,后者的每个元素代表投影中的一个字段。如果你已经准备了一个Java对象用于保存投影查询的结果,也可以直接在QL语句中创建Java对象,例如:

SELECT new CustomerDetails(c.id, c.status, o.count)

FROM Customer c JOIN c.orders o

WHERE o.count > 100

请注意,此时要创建的Java对象必须有对应的构造子,否则会抛出NoSuchMethodException异常。

?         子查询(Subquery。在WHERE语句中可以使用子查询,用子查询的结果作为WHERE查询的条件,例如:

SELECT goodCustomer

FROM Customer goodCustomer

WHERE goodCustomer.balance < (

SELECT avg(c.balance) FROM Customer c)

?         多态查询(Polymorphism QueryEJB 3.0 QL自动支持多态查询。如果在FROM语句中指定的entity bean类型是一个基类,则该类型的所有子类都将在查询范围内。

?         命名参数(Named ParameterEJB 3.0 QL允许使用命名参数,命名参数用冒号(:)前缀表示,其值将在运行时动态绑定。例如:

SELECT c

FROM Customer c

WHERE c.status = :stat

在使用这个QL语句时,Query对象必须对stat参数赋值。

?         其他功能。其他可以在WHERE语句中用作查询条件的功能函数包括:UPPERLOWERTRIMPOSITION,CHARACTER_LENGTHCHAR_LENGTHBIT_LENGTHCURRENT_TIME,CURRENT_DATECURRENT_TIMESTAMP,等等。

熟悉Hibernate的读者不难看出,EJB 3.0 QLHibernate QL非常相似——在TheServerSide年会上演示时,EJB专家组的领导人Linda DeMichiel女士干脆就采用了Hibernate文档中的“Cat”示例。在EJB 3.0的参考实现问世之前,学习Hibernate QL应该对熟悉EJB 3.0 QL不无裨益。

小结

本文以较贴近的视角审视了EJB 3.0规范第一个预览版本揭示出的一些新特性。由于更多地关注有别于EJB 2.1的新特性详情,本文或许不足以帮助读者形成对于EJB 3.0的整体印象,有兴趣的读者可以参考《程序员》2004年第6期《剖析EJB》一文。另外,本文参考的仅仅是EJB 3.0规范第一个预览版本,在将来的一年中,这个规范的技术细节还可能发生变化,读者不可不察。

EJB 3.0将是EJB历史上最大的一次变迁,充分汲取Open Source社群——尤其是SpringHibernate等优秀项目——的经验使EJB重新具备了成为技术主流的潜力。有鉴于此,尽管EJB 3.0的规范要到明年才能定型、应用服务器支持还需要更长的时间,但现在开始深入了解它的方方面面,已经不算太早了。Java的一个新时代即将到来,你做好准备了吗?



[1] 关于依赖注入的深入讨论,参见《程序员》2004年第3期《IoC容器和Dependency Injection模式》一文。

[2] 详见《程序员》2003年第12期《反转的控制》一文。

[3] 关于Hibernate的深入讨论,参见《程序员》2003年第9至第11期连载的《冬眠的数据库》。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值