作者/Rod Johnson 改写/透明
曾几何时,EJB被人们当做J2EE的核心而顶礼膜拜。可惜,过去七年的经验褪去了EJB的光环。我现在更多地把EJB当作一种过渡性的技术:它普及了很多有价值的思想;但对于大多数新的应用来说,它并不是最佳的选择。在本文中,我们将审视EJB教给我们的东西,以及——更重要的是——如何享受那些有价值的思想,同时避开EJB的重大缺陷。
EJB的原意是简化企业应用的开发,它希望让应用开发人员将能够将注意力集中于他们的问题领域和编写业务逻辑,而不是去关注系统级的问题。同时,EJB规范也承诺EJB(以及基于EJB的应用)在不同应用服务器之间的可移植性。那么,这些期望达到了吗?
一个过时的组件模型
没有什么可惊奇的,EJB现在看起来已经锈迹斑斑了。1998年3月,当EJB第一次出现时,企业软件的世界完全是另一番风景。自那以后,发生了如此之多的变化,以至于我们很难确切地回忆起当时的行业状况。当EJB规范1.0版问世时,在企业软件领域根本没有标准。微软事务服务器(Microsoft Transaction Server,MTS)可能是最接近一个企业组件框架的东西,但它是厂商专有的,而且依赖于微软对C++和其它语言并不优雅的扩展,并且紧密绑定在COM/DCOM上。
CORBA是COM/DCOM之外唯一开放的替代品。然而CORBA尽管功能强大,用起来实在太复杂,对C++之类语言的依赖也令人望而生畏。而且CORBA本质上是用于分布式组件通信的,而不是管理应用对象。CORBA的ORB也不是真正的应用服务器。
在这样的环境中,EJB看起来既简单又开放。然而,随后几年的技术发展,使得EJB不再像当初那样具有独到的优势。下面我将介绍这些重要的技术发展。
与今天大不相同的不仅是技术背景:当时的商业环境也要温和得多。电子商务(.com)领域能够向基础架构投入大把的钞票,即便它们暂且无法证明自己的效益也在所不惜。尽管这个良性的经济气候催生了很多有价值的技术(例如J2EE),但它也造成了负面的影响:人们对于新技术的质疑不够。而EJB可能正是这种情况最大的受益者:这样一种涉及应用服务器许可证、硬件性能、开发时间、培训的成本高昂的技术,原本不应该在短时间内获得广泛接受的。 |
Java语言的进步
EJB 1.0规范是在Java 1.2对API进行重大改进之前6个月发布的。想想那时候的Java吧:新的集合框架还未出现;Swing还不是Java核心的一部分,还被放在com.sun.java.swing这个包里;Javadoc还不能使用frame;类加载器(class loader)还不是自然分为多个层次的。你还记得那是怎样的Java吗?
J2SE 1.3又对Java进行了更为重大的改进,例如动态代理(dynamic proxy),它使得任何接口可以被一个运行时生成的代理所实现。有了这样的能力,EJB所采用的那种“不完全匹配组件接口”的实现方式看起来越发的笨拙。要求一个专门的部署阶段也同样值得怀疑:实际上,很多EJB容器厂商很快利用动态代理的先进技术取消了代码生成和编译的步骤,而这正是EJB部署过程中的重要一环。看起来,大多数厂商都同意:session bean不再需要代码生成的支持,只有entity bean才有必要。[1][1]
.NET的挑战
EJB最初的灵感部分来自于MTS,后者则借鉴了更老的“事务监视器”的概念。不过,EJB的吸引力比MTS可要大得多。在EJB的大部分时间里,微软缺乏一个可信任的企业框架。COM/DCOM和MTS都无法令人信服,EJB 在这个领域基本上无人竞争。
2002年初,随着雄心勃勃的微软.NET企业平台的发布,情况发生了改变。.NET受J2EE的影响颇深,微软从J2EE专家这里汲取了大量知识。同时,.NET又与J2EE有一些显著的差异,特别值得一提的是与EJB的差异。(我在这里只关心体系架构方面的差异。很显然,与J2EE不同,.NET实际上是一种专有的系统,但这并不是我们现在要讨论的。)
很多Java开发人员错误地认为:EJB核心服务在.NET中没有等价物。实际上,尽管.NET没有EJB的直接等价物,但它却提供了类似于EJB的关键服务。
.NET模糊了web容器与EJB容器之间的区别,而区分这两种容器恰好是“经典”J2EE架构的根本。任何一种.NET语言中的对象都可以通过继承System.EnterpriseServices.ServicedComponent来使用企业级服务,这里没有独立于受控环境的特殊“容器”。与其说这是一个缺陷,倒不如说是一种改进:这意味着任何对象都可以享受企业级服务,而不需要特殊的部署阶段,也无须在开发时承担那么沉重的包袱。说实话,我并不欣赏“强制继承ServicedComponent”的做法,但那也好过实现一个EJB。
部署这样一个“享受企业级服务的组件”比部署EJB简单多了,因为.NET也抛弃了J2EE中的很多外部部署描述文件。譬如说,对于那些用于声明性事务管理的元数据,.NET不是将它们放到独立的XML部署描述文件(例如ejb-jar.xml文件)中,而是“元数据属性”的形式存放在实际的组件源程序中。下面的例子定义了一个事务性的对象,它的方法将访问一个隐含的事务上下文,后者会提供类似于EJBContext的事务管理方法。请注意,这个类必须扩展ServicedComponent:
[Transaction(TransactionOption.Required)]
public class MyComponent :ServicedComponent, MyInterface {
public void myMethod(long objectIdentifier) {
...
}
...
}
这个源码级元数据的用法很简捷。因为与EJB规范中的假设正相反,事务性行为通常是业务模型最基本的部分。“应用程序装配者”角色与“bean开发者”角色之间的划分仅仅存在于EJB规范委员会的头脑中,我从未在真实的项目中看到过这样的角色划分。甚至可以说,这种划分是非常危险的,因为改变事务语义就从根本上改变了行为,而这是任何单元测试都无法测出的。另外,代码重构也不会破坏源码级元数据,这正是外部XML部署描述文件的一个重大缺陷。
所有这些都值得认真对待。可悲的是,J2EE社区没有对.NET投入应有的关注。证据?如果有人傻到在TheServerside.com或是其它J2EE门户网站中发表与J2EE相关的.NET新闻,立即就会骂声四起——而且全都是毫无价值的老调重弹。幸运的是,J2EE社区中一些重要的推动者们显示了出一种开明的态度,他们愿意采用一些.NET所开创的、有价值的特性,例如:
Ø Java 1.5将为Java添加源码级元数据,以及C#风格的“自动装箱”(autoboxing,简单数据类型和对象之间的转换)功能。
Ø JBoss 4采用了.NET风格的元数据来驱动企业服务。
Ø Spring和其它AOP框架(除了JBoss AOP之外)也使用了源码级元数据。
Ø 甚至EJB 3.0也有可能采用源码级元数据来描述目前保存在XML EJB部署描述文件中的信息。
Web Service
在EJB 1.0和EJB 1.1的年代,除了Java自己的RMI/RMP(那还是一种相当底层的技术)之外,重要的分布式对象技术就只有CORBA和COM/DCOM了——这两种技术都很复杂,都没有被业界大量采用。相比之下,EJB更加简单、概念更加一致,因此它成功地在很多系统中引入了分布式对象——甚至是那些根本不需要对象分布的地方。
如今,情况已经大不相同了。基于XML的web service提供了比过去大得多的开放性。这是真正的互操作,不仅仅是在微软技术和基于Java的解决方案之间,还可以在Perl、Tuxedo、以及其他很多语言和系统之间实现互操作。
同时,越来越多的人认识到:分布式对象的使用仅仅在少数应用中才是适合的。因此,快速增长的领域是在异构平台之间的互操作性,而不是J2EE应用服务器之间的远程调用。
EJB的另一个大问题在于:它总是试图把组件模型和远程支持放到同一个标准中,这样做有两个大问题:同时处理组件模型和远端支持增加了复杂性;而且远程协议还在不断发展。EJB规范最早3个版本希望将Java RMI(或者说,至少是RMI over IIOP)作为一个标准的远程协议,这种想法现在看起来非常可疑。随着更多远程协议的发展,EJB容器不可能支持所有的协议,而EJB又没有提供“可插入”的远程机制。
我喜欢的是一个更加模块化的方法:将“远程调用”看作一种服务,可以在对象模型之上通过façade将其暴露出来,就好像web界面就是业务对象之上的一个façade。
敏捷方法学的兴起
最后,自从EJB在1998年诞生之后,开发过程和最佳实践的思想发生了重大变化。可能最重要的事件就是“敏捷”方法学的兴起,例如极限编程(eXtreme Programming,XP)。
尤其值得一提的是,测试先行的开发(test first development),或者至少是严格的单元测试,其价值在在很多项目中得到了证实。而EJB使得有效的单元测试非常困难,因为EJB严重依赖容器的服务。因此这就意味着EJB使得敏捷开发过程难以被应用。
从未出现的组件市场
我们对EJB的失望还在于它没能成功地创造一个第三方组件市场,而这是其承诺的主要愿景之一——还记得EJB规范所设想的、将多个厂商提供的EJB“组装”起来构成应用程序的情况吗?
这个组件市场并没有出现,它是否真的会出现也很值得怀疑。之所以这样,一部分是因为EJB的模型(它的可移植性有缺陷,部署也过于复杂),然而更重要的是:企业组件太过复杂,有太多的依赖关系需要包装。在实际工作中,那种所谓“可复用”的第三方EJB组件其实很少,而且我看到的几个例子也具有非常大的依赖性——例如,依赖一个精心设计的数据库schema和专有的数据库特性,或是依赖某种特定J2EE应用服务器的专有特性。这种依赖性使构想中的“J2EE组件市场”很成问题。过去也曾经有过成功的组件市场,例如ActiveX控件和(在一个较小范围内)JavaBean组件。然而这些组件所面对的问题要比企业级应用简单得多。
方兴未艾的新范式:AOP
最后不得不提的是,一种新的编程模型正在浮出水面。那是一种更为通用的解决方案,仅仅其中的一部分就能够提供EJB大多数有价值的能力,那就是面向方面的编程(Aspect Oriented Programming,AOP)。在J2EE应用开发中,我们主要用到AOP的拦截(interception)能力,它为我们提供了“在任何对象的方法调用前/后加入自定义行为”的能力,这使得我们可以处理企业应用中的横切(crosscutting)关注点(即:同时作用于多个对象的关注点),并且仍然保持强类型(不需要改变方法签名)。例如,我们可以在一个应该具有事务的方法调用前开始一个事务,在方法返回时提交或者回滚。使用AOP让我们可以把“与事务管理相关的重复劳动”放进一个框架内。另外一个很适合使用AOP的场合则是自定义安全检查。
我把AOP看作是OOP的补充,而不是竞争对手。OOP在通常的场合下工作得很好,但在特定的领域里却有所欠缺:举例来说,如果我们必须为多个对象和方法应用相同的事务行为,我们需要将同样的代码剪切/粘贴到每一个方法里。AOP让我们可以把这类问题封装到方面(aspect)中,从而更好地实现模块化。AOP定义了“切入点”(pointcut)的概念,让开发者可以从另一个角度来思考程序的结构,从而弥补了OOP的某些缺陷:如果需要对一组方法施加横切的行为,就应该拦截这些方法。
在这里,你可以看到与EJB的相似之处:EJB服务说到底就是拦截。客户端执行EJB上的一个方法,EJB容器则使用拦截来提供安全检查、线程管理、声明性事务管理等服务,这与AOP拦截是相同的概念。这是一个强大的概念, EJB最重要的价值都是由它提供的。
为了获得这些服务,AOP比EJB更有吸引力,因为它对业务对象的要求更少。例如,它们通常不需要依赖一个特定的API(譬如EJB API);它们可以是POJO,从而使得开发更加容易,并且可以获得对象模型的所有利益。AOP的另一个优点是:它比EJB允许更大的开放性。譬如说,只要愿意,我们就可以自定义应用方面;而使用 EJB,我们就只能使用那些系统级的方面——这些方面是EJB规范规定、由EJB容器实现的。
EJB为我们提供了什么?
有经验的系统架构师倾向于只使用EJB的一小部分。毫无争议,无状态session bean(Stateless Session Bean,SLSB)是EJB中最有用的,然后是message-driven bean(用于异步操作)。几乎同样毫无争议的,有状态session bean的价值是可疑的。EJB规范使得容器很难让有状态session bean具有与HTTP session对象同样的稳定性,这意味着在大多数场合HTTP session对象是更好的选择。entity bean则可能是EJB规范中最虚弱的部分,它的性能相当差,并且没能解决好O/R映射中最重要的问题,而这恰好是人们希望它完成的工作。
为什么SLSB如此流行?因为它们是简单的组件(至少按照EJB标准是这样),并且对一些企业系统常见的问题提供了一个相当好的解决方案。下面,让我们逐一审视SLSB提供的主要服务:
声明性事务管理
SLSB最有价值的服务可能就是容器管理的事务(Container-Managed Transaction,CMT)了。尽管最终通过JTS/JTA协调事务的是J2EE服务器而不是EJB容器,但SLSB让我们可以使用声明性、而非编程性的事务管理。我们可以在Java代码之外告诉EJB容器如何划定事务边界。当然,如果想要回滚事务,我们需要使用EJB API,但是在理想的状态下中,我们不必为事务管理写哪怕一行代码,也不需要使用复杂的JTA API。
尽管EJB CMT对于大多数应用来说很有价值,但要处理一些复杂的事务管理问题就显得力不从心了。譬如说,有时一个业务操作需要多个事务,如果每次事务都通过一个EJB入口点调用一个EJB方法,这就比通过编程方式界定事务更复杂了。乐观的锁定策略也会成为一个问题。如果我们使用声明性事务来驱动一种持久技术(例如TopLink)执行乐观事务,乐观并发异常将在容器提交事务之后才出现,而此时时应用代码已经将控制权交给了EJB容器,很难重新控制程序的运转。EJB没有为这类更复杂的场景提供良好的支持。如果使用Bean管理事务(Bean-Managed Transaction,BMT)的EJB,则需要从JNDI得到JTA UserTranscation对象,然后就像在应用服务器中运行的其他对象一样直接使用JTA API。在同一个EJB中混合BMT和CMT也是不可能的,所以如果只有一个方法需要复杂的事务行为,所有的其它方法都必须承担BMT和JTA的复杂性。
然而,尽管CMT不能解决所有的问题,对于大多数应用场景,它仍然不失为一种出色的技术,理应成为人们使用session bean的一个重要理由。
不过EJB CMT其实可以做得更好。EJB事务支持的另一个局限是:它与JTA驱动的全局容器事务绑在一起。尽管表面上看起来完全恰当,实际上这对于很多应用来说是杀鸡用牛刀。依赖全局JTA事务不仅带来了对EJB容器的依赖,同时还造成了对应用服务器所提供的分布式事务协调器的依赖,而后者仅仅在需要多个事务性资源(例如数据库)的时候才有意义。实际上,很多J2EE应用——甚至是那些非常复杂的应用——只需要使用一个数据库(可能是一个分布在集群上的数据库,例如Oracle 9i RAC),这些应用需要的仅仅是一个本地的、特定于资源的事务,当然声明性事务管理的好处总是存在的。
因此,可以同时适应高端和低端的轻量级、更少侵入性的事务基础架构才是真正的价值所在。例如Spring框架提供的基于AOP的声明性事务管理可以通过配置,在支持多个数据库的JTA、或者JDBC、或者其它的特定于资源的事务管理API(例如JDO事务API)之间切换,而不需要对业务代码做任何修改。这意味着你可以根据应用的需求选择最适合的事务管理策略——毕竟,如果你要做的只是一个单数据库应用,你就不需要具有JTA支持的高端的应用服务器。
而且,EJB CMT还可以做得更加灵活。只有当业务方法抛出一个非受控(unchecked)异常时,EJB容器才会回滚事务。非受控异常被看作是一个严重的问题,EJB容器会把它记入日志,然后销毁出错的EJB实例,并抛出一个EJBException(对本地客户端)或者RemoteException(对远程客户端)。如果抛出的是EJB规范定义的“应用”异常(RemoteException之外的受控异常),EJB开发者就必须调用EJBContext.setRollbackonly()方法指定回滚策略。这样的处理方式不是我们通常希望的,如果能够指定“哪些受控异常可以直接触发事务回滚”就更好了——这样的自动回滚将不被视为程序错误。Spring的声明性事务管理通过“回滚规则”提供了这样的能力,这样做的好处在于:应用开发者几乎不再需要调用一个特定于框架的setRollbackOnly()方法。
远程调用
远程调用也是EJB——尤其是SLSB——证实了自身价值的领域。直到EJB 2.0为止,SLSB只能提供RMI远程调用;EJB 2.1增加了web service远程调用。EJB容器支持远程请求的能力,就像支持EJB实例的生命周期一样,是很有价值的,任何一个还记得如何管理自定义的RMI服务器的人都会同意这一点。
然而,正如我曾经说过的,很多人认为EJB规范混淆了远程和组件模型。这里最重要的问题是:很多时候,我们根本不需要远程调用。EJB极大地简化了远程调用,也使它成为了一种危险的诱惑——诱惑人们采用一种并不适用、而且可能非常昂贵的体系结构(昂贵在复杂性、工作量和性能上)。
在web应用中,将web组件和业务组件放在同一个JVM中几乎总是一个更好的主意。在这类应用中使用带远程接口的EJB无法增加任何价值,通常对于设计还是有害的。
集群
EJB常常被鼓吹为获得J2EE应用最大可伸缩性的不二法门。这种说法的理由之一是这样一种信念:为了获得比普通应用更高的可伸缩性,就必须采用分布式应用。另外一个信念是:EJB容器具有神奇的集群能力——商业EJB容器通常是昂贵的,所以期待其中存在某种魔力也是很合理的。事实上,EJB容器的集群功能并不是那么神奇,对于entity bean和有状态session bean的集群服务是相当有限的。
entity bean在集群中往往没有O/R映射解决方案(例如一个好的JDO实现)表现得那么好。在一个集群环境中,因为EJB容器的数据复制能力的局限,通常必须在每一个事务开始时从数据库加载entity状态。对比之下, Tangosol Coherence之类专门的集群缓存技术比应用服务器厂商所提供的复制功能更加强大。
有状态session bean的集群很容易引起问题,因为EJB规范本身就决定了它无法像Servlet API那样轻松地实现集群环境下的优化、减少节点间状态复制的次数。事实上,薄弱的集群能力已经被广泛认为是有状态session bean的致命弱点。
于是,关于EJB集群的争论最终归结到了远程无状态session bean。对于SLSB来说,它的所有实例都是相同的,并且可以互换。EJB容器只需要为远程客户端提供一个EJBObject存根,在其中用循环算法、基于负载的算法或者随机算法来提供负载均衡。但借助Cisco负载均衡管理器这样的设备,也可以在硬件级别很容易地实现负载均衡(而且很可能效率更高)。
问题的关键在于,集群的主要用途并不在于J2EE web应用的业务对象层。在下列位置的集群服务通常更为重要:
Ø web 层。通常必须在这里管理session状态,所以有必要设计一个路由方案。如果不需要保存session状态,一个web farm[2][2]风格的解决办法通常是最有效的。
Ø 数据访问层。
Ø 数据库。
水平可伸缩性的真正限制通常在于数据访问。大多数entity bean实现在集群环境要求每次对持久对象的访问都必须访问数据库,这会对性能造成影响,并且数据库负载也会升高(因为限制了可伸缩性)。如果采用一个专门的持久化解决方案(例如Hibernate或者一个JDO实现),再加上一个支持集群的缓存,我们通常可以获得更高的吞吐量。通常我们没必要在业务层管理状态,也就是说专门用于业务层的集群服务并不是很重要。
线程管理
EJB的线程管理所能提供的价值往往不像它所吹嘘的那么确切。EJB让开发者们能够以编写单线程程序的方式编写业务对象,同时又使这些业务对象能够适用于多线程环境,这当然是大家都愿意看到的。然而,实际情况是:EJB的线程支持无法彻底解决“对EJB facade之后的对象的并发访问”造成的问题,而且它也不是解决并发问题的唯一选择。另外,不论调用entity bean的任何方法,整个entity bean实例都会被加锁,这是一种相当幼稚的做法,而且并不总是很合理。
对于类似于SLSB的服务对象,存在比EJB线程管理更好的替代方案,包括:
Ø 实现不读/写实例变量的多线程服务对象。这种方法在servlet、Struts Action之类的对象中工作得非常完美。大多数无状态服务完全不需要复杂的线程管理。对于这类服务对象,只维护一个单独的实例(而不是维护一个实例池)在很多场合下都是有好处的。举例来说,如果我们因为效率的原因需要在一个无状态session bean中维护一个缓存(例如缓存一个开销庞大的计算得到的结果),EJB缓冲池将导致很多缓存同时存在。单独一个实例也许是更合理的办法。
Ø 使用Command模式:可以在收到每个请求时为每个服务对象创建一个新的实例,在对象内部消除并发的问题。WebWork/XWork很成功地采用了这个办法,在Spring中也同样可行——只要将bean定义为“prototype”类型即可。现代JVM很好地处理了对象的创建和销毁,服务对象通常可以很廉价地被实例化。
Ø 使用一个多线程服务对象,在其中使用普通Java语言的同步或并发类库以便保护任何读/写状态。如果需要控制的方法不多,同步实现起来很简单,工作起来也会很完美。当然同步不能阻止集群中另一台服务器上的同一段代码并发执行,但是EJB线程管理也同样做不到。
EJB实例池
与线程管理关系最为紧密的便是EJB的实例池。在各种EJB实例池中,最有价值的是无状态session bean缓冲池(这也是最简单的EJB实例池),因为无状态对象是缓冲池技术的理想用户。
在我看来,SLSB缓冲池的价值被高估了,尽管某些时候它确实有其价值。就像我曾经说过的,比起构思EJB规范时的Java 1.1 JVM,现代的JVM执行通常的垃圾收集要敏捷的多。如果时间回到1998年,我们的确需要实例池以避免垃圾收集器死掉,但现在真的还有这个必要吗?
某些应用服务器特有的EJB部署描述文件允许为每一个SLSB部署配置缓冲池大小,然而这种配置的价值是很可争议的。如果创建一个典型的SLSB只有相对较低的代价,那么配置一个比较小的缓冲池会产生不必要的竞争,而过大的线程池也不能改善吞吐量,因为吞吐量的决定因素是应用服务器所允许并发执行的线程的最大数量(线程池是一个应用服务器至关重要的服务,我将简短地加以讨论)。如果SLSB缓冲池的大小小于可以使用某个特定SLSB的最大线程数量,后来的调用将被阻塞,直到有一个EJB实例可以使用为止;如果SLSB缓冲池的大小大于线程的最大数量,那么吞吐量不会有任何提升。有人认为:不论任何场合,理想的缓冲池大小就是“希望访问一个特定SLSB的最大线程数量”,这种观点也同样是很可争议的。
设计EJB线程池的主要动机是避免垃圾收集和节省内存。比起Java 1.1 VM,现代JVM的垃圾收集效率要高得多;如今的内存也便宜得多。也就是说,比起当初,这两个动机如今都不是很重要了。 |
资源池
在J2EE(或是其它三层应用)的缓冲池中,真正有价值的是资源池,而不是业务对象实例池。例如,将数据库连接放入缓冲池中,以避免在加载时耗尽连接,这是企业应用的基本策略。应用服务器也必须将执行线程放入缓冲池中,以防备拒绝服务攻击或是大负载时的失败。
然而,这样的资源池可以由J2EE应用服务器(而非EJB)提供,因此不必与EJB容器绑在一起。这样一来,任何一个运行于J2EE应用服务器中的对象(例如一个Servlet或者普通的Java类)都可以获得数据库连接池。在web应用中,web容器会在应用服务器的入口处强制使用线程池。并不需要用更多的线程池来保护业务对象,除非它在分布式应用中运行到了进程之外。
安全
另外一个EJB宣称提供的服务是安全管理。开发者可以在EJB部署描述文件中进行方法级的、基于角色的安全性配置(和Servlet API一样,EJB也支持编程性安全管理,可以在代码中检查一个通过了身份识别的用户是否具有某个特定的角色)。
单是捧着J2EE规范看看,这的确是很好,但实际用起来就不是那么好了。我见过不少J2EE应用有复杂的安全需求,它们大多发现J2EE安全基础架构根本不够用。这些应用没有一个能够依赖EJB的声明性、基于角色的安全:原因大抵是基于角色的安全不够用,或是需要一个动态安全凭证检查的级别、而EJB没有提供。这很大程度上是J2EE安全基础架构的问题,而不是EJB的,但仍然抵消了“安全性”为EJB挣来的分数。
声明性安全性确实有其价值,唯一的问题是:EJB的安全性服务必须绑在标准的J2EE安全基础架构上,而这些基础架构仍然无法满足复杂的需求。更好的办法是使用AOP来实现声明性自定义安全性:一般的做法是将安全凭证保存在一个ThreadLocal对象(而不是J2EE安全上下文)中,再用一个自定义的安全拦截器包裹在需要安全性管理的业务方法周围。
对于安全需求比较简单的web应用,通常在web层检查用户身份就足够了,例如借助Servlet API的声明性(基于web.xml)或者编程性基于角色的安全性。在web层实现声明性安全管理的其他方法还有:向需要保护的内容周围添加Servlet filter;借助WebWork2和Spring之类web框架中的拦截器;或者在web控制器中使用AOP拦截器。Spring等框架不仅提供了web MVC功能,还提供了AOP功能。
业务对象管理
最后,EJB容器也是一种工厂:它借助JNDI为服务对象(通常是SLSB)提供目录服务。EJB部署描述文件提供了一个从服务名称(JNDI 名称)到实现对象的映射,从而消除了业务接口(EJB的组件接口)与实现类(EJB实现类)之间的耦合。
这种解耦是一个好的编程实践。EJB的一大优点就是强制调用者使用业务接口,而不是直接使用实现类。随着EJB在实际工作中被广泛采用,它也在无意中普及了“针对接口编程、而非针对类编程”的原则。不过,EJB并不是达到“分离接口与实现”的唯一途径。
Java语言把接口作为头等公民来对待[3][3],从而提供了完美的解耦能力。不过,如果在Java代码中明确指定“使用接口的哪个实现类”,那么针对接口编程带来的好处也就被抵消殆尽了,因此我们需要一些基础设施提供帮助,从而可以在Java代码外部选择接口的实现类。EJB容器就是这样的基础设施——但不是唯一的,更不是最好的:有很多更加轻量的容器可供选择。Spring框架以一种非常轻量的方法帮助开发者实践“针对接口编程、而非针对类编程”的原则:它所提供的通用工厂可以根据配置(而不是Java代码)来按名字获得对象实例。如果想要切换某个接口的实现类,你只需要编辑一个简单的XML文件。另外,还有别的轻量级容器也可以达到类似的效果,而不需要EJB的复杂性,例如PicoContainer(www.picocontainer.org)。
由于Spring和PicoContainer是如此轻量级,因此它们比起EJB来更适合管理细粒度的业务对象——而EJB则只适合那些粗粒度的组件。对于细粒度的对象来说,实现、部署EJB和使用JNDI访问的开销太高了,即便这样可以使它们拥有“在不影响调用者的前提下切换实现”的能力也是得不偿失。换句话说, Spring和PicoContainer这样的轻量级解决方案能够提供一步到位的工厂功能;而EJB只适合一部分应用场景,常常需要由组件facade之后的某种轻量级解决方案来补充。
总结:EJB的服务
上面所讨论的服务大多已经被加诸所有类型的EJB,然而除了SLSB和MDB之外,别的EJB(尤其是entity bean)并不经常使用它们。例如,处于性能考虑,几乎任何场合都不会使用entity bean的远程接口。为entity bean提供事务管理也是毫无意义的:经验告诉我们,最好把entity bean设计成不包含业务逻辑的哑数据访问对象,而这样的对象不需要任何事务管理。
EJB帮助证实和普及了很多有价值的想法,但这并不意味着EJB的实现就是完美的。我们需要的解决方案原本可以不必这么复杂,这正是EJB的轻量级替代品出现的原由。有了过去七年的经验,以及从.NET中学到的东西,我们现在有机会既吸取EJB的优秀之处又避免其缺陷。我们可以拥有SLSB的一切长处,同时又避免所有不必要的复杂性。
EJB的现状
EJB仍然有其位置,但比很多J2EE开发人员所认为的位置要少得多。我可以确信,就在不长的几年之后,EJB将被归入“遗产系统”之列。然而,如果符合下列条件之一的话,现在使用EJB也是恰当的:
Ø 你在开发以中间件为核心的应用,其中web界面只扮演很少的角色,并且需要一个分布式(而不是简单的集群)体系架构。很多大型金融应用符合这个描述。然而,分布式体系结构常常被滥用,导致性能的损失和复杂性的提升,而没有带来任何真正的收获。
Ø 你想要开发一个基于RMI的分布式应用。EJB是达到这个目标的最简单的技术。
Ø 你确实需要有状态session bean,不能借助HTTP session对象或者厚客户端来满足这个需求:譬如说,你必须同时保存多种客户端的状态。在真实项目中,这种情况是相当罕见的。
你不需要使用EJB来满足下列需求——它们常常被误认为是需要使用EJB的理由。
Ø 事务需求。事务协调器是由应用服务器(而非EJB容器)提供的。EJB CMT确实很好(特别是当与SLSB同时使用时),但我们还可以做得更好。
Ø 多线程。编写适用于多线程环境的无状态服务对象并不困难。如果需要缓冲池,Spring可以提供,并不需要EJB。
Ø 资源池。这也是由J2EE(而非EJB)提供的。
即便不使用EJB,你也可以享受这些服务,而且不需要开发你自己的基础架构代码。
如果恰好有非常熟悉EJB的开发人员,你或许想要使用EJB。然而,由于EJB的复杂性,很多有EJB开发经验的开发人员实际上缺乏关于EJB的深度知识,而一知半解很可能是一件危险的事情。
我希望你从本文中得到的信息不止是“EJB是一种糟糕的技术”,更重要的是判断是否使用、何时使用EJB所需的基础知识。你应该能够为自己准备一个核对清单,用来检验EJB是否能为应用提供价值。如果所有条件都满足,就放心地使用EJB吧。