EJB(2.X-3.0)、Hibernate、Spring:剖析、批判和展望

一、前言
  我强调EJB、Hibernate、Spring的恩怨情仇,同时也必须说明,我一向反感你说我怎么侵入、你说我怎么依赖式的EJB VS Hibernate、EJB VS Spring的讨论,因为这种行为本身就是没有意义的、错误的。我提倡从正确的技术对比和理性的技术批判中受益。对比,我们需要找准对比点;批判,我们需要从source、spec、application context中分析、批判。

二、从EJB说起

2.1 EJB几种Bean类型的引入顺序

  EJB1.0,有两种Bean类型:SessionBean、EntityBean。

  EJB2.0,引入CMP EntityBean、引入Message-Driven Bean、引入Local接口。

2.2 Entity Bean和O/R Mapping的微妙关系

  我想对O/R Mapping、O/R Mapping Engine做一个简要的说明。

  O/R Mapping,以对象视图(Object View)来看待DB Record,对象操作能够通明地映射成DB Record操作。

  O/R Mapping Engine,就是使得O/R Mapping成为可能的具体实现手法。

从我们的定义来看,使用BMP EntityBean意味着你自己在实施一个非常简单的O/R Mapping ,你自己在为能够以对象视图和DB交互做出努力。而为了支持CMP EntityBean,EJB Server提供商会为你提供O/R Mapping 能力。而且,事实的确是这样,任何支持CMP EntityBean的EJB Server都需要提供一个Persistence(O/R Mapping) Engine,譬如JBOSS的JAWS(Just Another Web Store)。
至于,Hibernate、IBATIS等,虽然,也叫做O/R Mapping Tool,但是它们的意义已经远远超过了CMP EntityBean O/R Mapping Engine这样的Tool。下面会有详细的分析。

2.3 EJB-1.0
  EJB1.0是分布式组件架构,包括SessionBean和EntityBean。它引入了很多非常前卫的技术、概念。主要包括分布式组件、容器、DB操作的对象试图。

  EJB1.0,可能Expert Group设想的EJB的应用场景是大规模分布式的系统。所以,SessionBean、EntityBean尚没有引入Local接口。 client->SessionBean、client->EntityBean、SessionBean->SessionBean、SessionBean->EntityBean等等都是remote的。

  EJB1.0,将容器这个概念引入EJB,意义重大。这里我想顺便澄清一个问题:容器是什么?我的观点:容器只是一个概念、一种架构。就拿EJB Server来说,Server试图为Bean提供分布式、事务、安全等基础设施,那么就必须有一个凌驾于Bean之上的Layer或者说warp,这样才能够从高层拦截Bean调用,进行一些额外操作。这样的架构就叫做容器架构,这个概念当然不是自EJB才有的。至于怎样实现,方法各异。

  事实上,以个人的观点,容器架构的核心,不在于从高层“掌握”Beans(Objects),而在于采用怎样的技术来实现Bean(Object)调用的拦截。无疑,EJB Servers和Spring的实现手法是不同的。下面会详细讨论这个问题。

  EJB1.0为DB操作提供了对象试图。Expert Group当初是怎样定位EntityBean的?无疑,1.0中的EntityBean,也就是2.0以后的BMP EntityBean,定位是Domain Object(我不知道当时有没有这个概念,只是它们的思想是非常一致)。它的fields直接映射DB Table Schema,member functions就是对Table Record的操作。Client->EntityBean、SessionBean->EntityBean等就可以直接和数据库交互了。

  有人跟我说EJB1.0基于Catalysis方法学,SessionBean对应Role,EntityBean对应Domain Object。到目前为止,我对这种说法,持保留态度,因为EJB Spec中,我丝毫没有这种说法的痕迹。

2.4 EJB-2.X

  无疑,EJB1.X的设计存在重大的缺陷。2.0增加的特性包括Local接口、CMP EntityBean,它们是对1.x缺陷的重大更正。

  首先,事实上没有多少Expert Group想象中的大规模分布式应用。我们从两个方面来说:(1)通过Remote方式使用 EntityBean引起严重的性能问题,很有必要提供Local接口。(2)访问SessionBean的WebApplication和SessionBean部署在同一服务器上的情况非常普遍,所以提供SessionBean的Local接口,也是必然的事情。2.X之后,最常用的一个Pattern就是使用SessionBean Fa?ade通过Local的形式访问EntityBean。而且,即使应用规模大到连SessionBean和EntityBean都需要部署到不同的Server,也没有关系,因为EJB2.X同样支持Remote接口。

  其次,EJB2.0引入CMP EntityBean。CMP EntityBean解决了EntityBean持久化表示和JDBC分离的问题,同时大大简化了EntityBean的开发、提高了性能。但是,我不得不说,CMP EntityBean将EJB1.X的Domain Model理念完全冲掉了,因为CMP EntityBean是不能包含任何Domain Logic的。BMP EntityBean似乎就是Matrin所说的DomainObject,而CMP EntityBean在典型的SessionBean->EntityBean这样的应用场景下,似乎就是Martin所说的Transaction  Script中的AnaemicDomainObject。

  顺便说一下,我个人是不同意Martin的RichDomainObject的说法。因为,在数据应用系统中,Martin提到的相对于Transacton Script中AnaemicDomainObject的RichDomainModel往往没有反映现实世界。一个Bank Account反映到现实世界,就是账本中的一条记录,它没有自发的动作,譬如withdraw。它和Person不同,Person可以有say(String words)这样的自发动作。Account的withdraw应该放到AccountManager中,由AccountManger来操作Account。不是说OO中的Object都需要有动作,现实世界中,本来就有静态的、没有自发动作的事物,譬如一个账本、一个帐号、一个资料库。虽然,不可否认,Rich Domain Model(对比AnaemicDomainObject说的)能够带来不少的好处(什么样的好处,你看看Martin的《Domain Logic and SQL》,就知道了)。

三、    我理解的Hibernate

  本来,本文的题目叫做《EJB、Spring:剖析、批判和展望》,因为我觉的Hibernate和EJB、Spring不是一个层次的东西,虽然,这个道理很浅显,但是为什么那么多人还拿Hibernate来攻击EJB,来攻击EntityBean?EntityBean是值得狠狠攻击的,但是你用错了枪。


  我上面提到,支持CMP EntityBean的EJB Implements都有一个Persistence Engine,也就是O/R Mapping Engine。CMP O/R Mapping Engine用来做什么的?它通过分析CMP Abastract Schema、分析EJBQL、分析Bean状态等行为,生成SQL,然后和DB 进行交互。

  而在我眼里,Hibernate不是”O/R Mapping Tool”这几个字能概括的了。我说Hibernate是一款独当一面的轻量级翻译中间件,是Layer,和CMP EntityBean O/R Mapping Engine不是一个层次的东西了。

Application------->CMP EntityBean Operation-------->DB
                                      &line;
                                   (O/R Mapping Engine)
 &line;---HQL、Criteria Query

Application------> Hibernate ------> &line;---POJO/PO Operation---------> DB &line;---and so on

  通过上面的两个图,你看出区别来了吗?

  EntityBean应用,不知道O/R Mapping Engine的存在,只需要使用EntityBean来完成交互。

  而在Hibernate应用中,Application是直接使用Hibernate的。也就是说,它是直接使用O/R Mapping Engine的。

  在这里,我建议你停下来,想想EntityBean是不是应该对应Hibernate中的PO/POJO?举个例子,你修改PO后,是不是需要sessionObj.update(po)来更新,这个sessionObj.update(po)是不是表示你直接使用Hibernate的Persitence Engine?是的。而在EntityBean中,你修改EntityBean后,你需要其它的行为来使得EntityBean的变化同步到DB吗?不需要。因为,EJB Container拦截你的调用,在你更改Bean的field之前、之后,container会调用load/store方法的(当然,在BMP/CMP EntityBean中,情况是不同的,BMP EntityBean调用programmer自己用JDBC编写的load/store等方法,而CMP EntityBean,使用CMP Peristence Engine来做这个工作)。这样,就隐式的持久化数据了。不需要,你像Hibernate那样调用session.update这样的语句。EntityBean这种同步方式是对它性能差的重要原因之一。值得注意的是,EJB Implements对于EntityBean同步并不完全是我上面描述的那样,同步的频率和事务、特定的implements是紧密相关的。

  总的来说,CMP EntityBean O/R Mapping Engine是为静态的、功能固定的EntityBean的O/R Mapping提供支持而开发的。而Hibernate担任的是一个Layer的作用。

四、    Spring不是神话

  Rd Johnson聪明在哪里?聪明在,他坚持了自己的实践,而不是随大流。Rd Johnson认识到90%的应用不需要分布式、不需要J2EE中那些重量级的技术,譬如JNDI,他就动手为EJB脱去Remote这层皮、将大多数应用中不必要的技术隔离、改造。从适用范围上来说,Spring对EJB做了90%的补充。

  个人看法:Spring的哲学在于,framework针对最常见、最简单的应用场景而设计,等到需要特殊技术的时候,再想办法解决问题。这样,在绝大多数没有特殊要求的应用中,Spring就显示出优势来了。下面,我们会做详细的讲解。

4.1 Spring“无侵入性“是谎言,但是有资格笑”百步之外的EJB”

 “无侵入性”是Spring标榜的特性。但是,我想说,Spring的“无侵入”是谎言,随着应用的深入,“无侵入”对什么framework来说,都是个神化。

  什么就叫“无侵入性”?部署到Spring中的Object不需要强制任何实现接口就可以说Spring是“无侵入性”的?我觉的,这是大错特错。如果你非要说,Spring的确不需要像EJB那样强制实现一些接口,那么我只能告诉你:

(1)Spring设想的Object的应用场景是从最简单的出发。所以,它没有,为了一些预料中要使用的特性而强制Object实现一些特定的接口。但是,事实上,在Spring中,如果你的应用场景稍微深入一点,那么你就和和Spring绑定了。

(2)Spring管理的Object,从某种意义上说是没有状态的。

针对第一点,我举两个个例子。(1)EJB内部方法的调用,会导致基础设施不会起作用。但是Bean接口(SessionBean、EntityBean、MessageDrivenBean)中都有可以使Bean获得自己Context的支持,譬如:SessionBean的setSessionContext(SessionContext ctx) 等等,容器部署Bean的时候会通过它给每个Bean设置一个上下文。而EJBContext中,有EJBObject getEJBObject这样的函数,可以使得Bean获得自身的EJBObject,这样通过EJBObject来调用Bean自己的函数,基础设施就会起作用了。而Spring中,如果,一个Object的函数需要调用自己的其它函数,而又希望譬如安全检查、事务等等Aspect起作用?那么Spring,怎么做?你需要设置Bean的exposeProxy属性。

ExposeProxy: whether the current proxy should be exposed in a ThreadLocal so that it can be accessed by the target. (It's available via the MethodInvocation without the need for a ThreadLocal.) If a target needs to obtain the proxy and exposeProxy is true, the target can use the AopContext.currentProxy() method.

所以,当你需要上面说的内部调用需要基础设施起作用的特性,不管在EJB还是Spring肯定需要和特定框架绑定的。为什么说,Spring五十步笑百步?因为,我上面提到,Spring在Object很简单的情况下,是可以任意部署的、复用的。而EJB却不管你需不需要,开始就设想你需要的。同样,Spring中的BeanFactoryAware、BeanNameAware等等接口也都说明了一点:Spring将特性从Object剥离,从而,尽量降低它的依赖性。只有当你的Object复杂的时候,framework才会侵入你的Object。

针对,第二点,我想着重谈一下。为什么说,从某种意义上说Spring中部署的对象是没有状态的?我们知道,Spring支持两种Object:Singleton和Prototype。Spring Spec中,认为,Singleton可以称为stateless的,Prototype可以称为是statefule的。而在EJB的世界中,StatefuleSessionBean和EntityBean也称作是stateful的。那么,它们的stateful分别意味着什么?它们为什么在依赖性方面有那么大的区别?为什么Spring中的Object不需要实现特定接口,而EJB需要?先来,看看EJB的SessionBean接口:

void

 ejbActivate()
          The activate method is called when the instance is activated from its
          "passive" state. 
 void
 ejbPassivate()
          The passivate method is called before the instance enters the
          "passive" state.
 void
 ejbRemove()
          A container invokes this method before it ends the life of the
          session object.
 void

 setSessionContext(SessionContext ctx)
          Set the associated session context.

  其中的setSessionContext我上面说过。看ejbActivate()、ejbPassive(),为什么会有这两个函数?而Spring不需要实现有同样函数的接口?这是EJB和Spring的对象管理机制的不同造成。EJB implements一般来说,为了复用Bean,会采用一级Cache加上一级InstancePool(StatelessSessionBean是不需要Cache的),从而支持将StatefulSessionBean持久化到磁盘,支持EntityBean的Bean Instance(注意这个Bean Instance和client得到的EntityBean是不同的,它没有和任何的DB Record关联)的复用,这就导致了ejbAcrivate、ejbPassivate等的引入。但是,Spring没有采用这样管理机制,它只有Singleton/Prototype。而Prototype虽然也可以说成是Statefule的,但是它不会在不同的client中复用Object Instance,而是每一个client一个对象,哪怕一万个client,那么就产生一万个Instance,而在EJB中,可能使用100 Instance来服务,将not active的Bean持久化到磁盘,复用Bean Instance。还请注意,这里我不是说EJB中的StatefuleSessionBean好,事实上我发现,一般来说,当并发量很大时,采用节约内存而持久化Bean到磁盘这种策略,I/O瓶颈引起的问题更为严重。

  再看,ejbRemove,这个没什么多说的,Spring中你可以选择实现InitializingBean、DisposableBean接口,但是Spring推荐不要这样做,可以写普通的init成员函数,然后在配置文件中指明init-method、destroy-method属性,这样避免和Spring框架的绑定。
  总的来说,Spring从对象最基本的引用场景出发,当需要复杂特性的时候,才会采用特殊机制来解决问题,也就是在这时,才会使应用绑定到Spring中。所以,它的侵入性比较低,但是不是“无侵入性”,不是你想的那么美好,当然,也没有“绝对无侵入“的framework。

4.2 我觉的Spring IOC的设计思路不够完美

  Spring的IOC被一些人当作多么神奇的东西。

EJB具有Spring中所说的那种IOC的能力吗?答案是肯定的。EJB中的EJB引用、资源引用、环境属性都可以说是IOC,不是吗?然而,Spring和EJB的IOC不同在哪里?

Spring注入的特色:主要考虑Local Object的查找,这个时候不需要任何的协议(譬如JNDI),当你需要注入Remote Object的时候,采用RMI协议或者使用第三方Tool(譬如Hessian)。

EJB的特色:无论你的Bean-Bean是否部署在同一台机器上、Client->Bean是否在同一台机器上,肯定需要通过JNDI来查询Bean,只是,如果是它们在同一台机器上的时候,你使用Local接口,这样使得调用变为Local调用,从而提升性能。EJB它从出生时起,就定位为分布式组件架构,一头栽进“distributed”不容易出来了。这个可能就叫“尾大不掉”吧。

这一切的不同,只能说,它们的定位不同。一个更关注Local、一个更关注Remote。Spring仍然坚持它的哲学,从最基本的、大多数的场景考虑起,到特殊需要的时候,再想办法来解决问题。它试图找到J2EE开发和系统能力的均衡点。

  可以说,Spring的做法,更加合情合理。但是,我也相信,Spring在”只是为Remote注入提供简单的支持“这一点上有点矫枉过正。我觉的,它可以做的更好,譬如通过作为J2EE标准的JNDI来封装Local、Remote查找。

目前,Spring不怎么关心Remote Object注入,对于需要Remote注入的情况,只提供简单的支持,而且还需要针对expert单独写配置信息。在这里,EJB3.0的做法,我觉的,是目前,最方便、最理智、也是最有前途的。EJB3.0通过 at remote、@local就可以让EJB Server做不同的部署。

Spring导出远程对象。
<bean class="org dot springframework.remoting.rmi.RmiServiceExporter">
     <!-- does not necessarily have to be the same name as the bean to be exported -->
            <property name="serviceName"><value>AccountService</value></property>
               <property name="service"><ref bean="accountService"/></property>
               <property name="serviceInterface"><value>example.AccountService</value></property>
               <!-- defaults to 1099 -->
               <property name="registryPort"><value>1199</value></property>
</bean>
 
Spring中注入Remote是怎样做的?

<bean class="example.SimpleObject">
               <property name="accountService"><ref bean="accountService"/></bean>
</bean>
 
<bean id="accountService" class="org.springframework.remoting.rmi.RmiProxyFactoryBean">
               <property name="serviceUrl"><value>rmi://HOST:1199/AccountService</value></property>
               <property name="serviceInterface"><value>example.AccountService</value></property>
</bean>
看了,这段代码,你就知道了。

这种方法非常的轻量级,从本质上来说,这种方法和JNDI没有任何的不同,这种方法,也需要一个NamingService,还记得RMI编程中,运行服务端的时候,首先运行rmiregistry,这个实际上就是非常简单的NamingService。看来,Rd Johnson对JNDI真是深恶痛绝啊。怎么也不愿意用JNDI。Spring这种方法,也许没有JNDI那样重量级,但是很显然它不能支持工业级分布系统,J2EE发展到今天,JNDI已经成为最核心的技术,它不是简单的Object Naming Service,它提供标准接口来定位用户、微机、网络、对象、服务器等等。它已经广泛而深入的进入J2EE技术的各个领域、成为J2EE系统的纽带。

举个很简单的例子:我需要在Spring应用中动态获取Remote Object,我该怎么做?我需要无缝使用LDAP,我该怎么做?答案是,Spring不能帮到你什么。

那么我就想,Spring为什么不使用JNDI来封装Local、Remote查找两种协议?从而,使用J2EE标准来实现无缝“注入”。这样带来的优点就是:(1)适应JNDI已经广泛而深入渗透的领域(2)JNDI是J2EE标准(3)支持动态Remote Service获取(4)支持工业级分布系统。

Properties props = new Properties();

pros.put(“searchServiceFactory”,”org.net.spring.bean.BeanFactory”);

Context ctx = new Context(env);

ctx.lookup(“servicename”);

Properties props = new Properties();

pros.put(“searchServiceFactory”,”com.sun.j2ee.jndi”);

Context ctx = new Context(env);

ctx.lookup(“servicename”);

  从下面,你可以看出,我个人认为Spring如果仍能OS,free,也许还会有很多人选择它,但是如果,它试图商业化,那么可以肯定它必须向EJB3.0靠拢。向标准靠拢。

4.3 独立的、完整的、面向Programmers的AOP框架才是Spring真正的亮点

  EJB具有Spring中所宣扬的AOP能力吗?答案是肯定的。EJB的基础设施就是使用AOP技术实现的。然而,我要说,EJB的AOP并没有面向Programmers。为什么这么说?EJB Implements为你提供一组基础设施(例如,JBOSS中那些可配置的interceptors)。你可以根据系统需要配置。但是,你不可以编写自己的interceptor,然后配置到系统中。因为EJB的interceptors往往和EJB Implements内部愈合很紧,你想编写自己的interceptor,意味着你必须阅读EJB Implements的source,了解它的实现。换句话说,EJB Implements中的AOP技术是为了EJB Server能够提供基础设施而使用的,它没有想到为programmer提供更多的AOP能力。而Spring的AOP开始就是作为一个框架来发展的,是独立的、完整的。造成这种情况,是历史的原因。EJB Implements作者也不是神人,他们不可能,N年前,就想到将AOP框架设计的足够独立,从而面向programmes。

个人观点:EJB3.0在基础设施方面的说明,基本沿袭EJB2.X的。只是,可以提到EJB3.0支持通过annotations来使用基础设施,没有说,EJB3.0需要完善的AOP框架,但是,我想,EJB3.0 Implments应该都会提供一个独立的、完整的、面向programmer的AOP框架。事实上,JBOSS不早就有了J

  当然,在Spring中使用AOP也不是那么的轻松,譬如,让你自己写TransactionProxy,你还是需要了解Spring AOP内部运作机制的。

4.4 Spring对其它框架的集成

  这个问题,就不谈了。

五、EJB将走向何方

5.1 EJB-3.0

  我们没必要说EJB2.X本身有多少的缺陷,毕竟,它是前一个J2EE时代的产物,只能说EJB2.X已经不能反映大多数J2EE应用的实际需要。过时了。那么EJB3.0打算带我们走向何方?

  EJB3.0 Spec除了针对简化开发、方便测试、方便部署等目标做了不少的修改,更重要的是EJB3.0对SessionBean,特别是EntityBean模型做了一个全面的整容手术。这种修改是革命性的。

在我的《如果我来改进EJB2.X模型》中,我谈到,如果,让我对EJB2.X的EntityBean模型做修改,那么首先需要为新的模型定好位。就拿EntityBean来说好了。 

第一条路:继续EntityBean设计的初试理念:Remote Domain Model(包括BMP EntityBean代表的Domain Model和CMP EntityBean代表的AnaemicDomainObject),并且保留Local接口,力图改经持久模型的设计,提高性能(即使CMP EntityBean的性能也是难以令人接受的,这种情况,我个人认为,主要是因为EntityBean模型设计的不好,在我的另一篇《如果我来改进EJB2.X模型》中有深入的分析)、增强功能(EJBQL实在太弱),让那些连SessionBean、EntityBean都需要部署在不同Server上的应用来为EJB2.X的EntityBean留口气。

但是,显然,EJB Server提供商是不可能甘心这一点羹的,因为那样的应用实在太少了。事实已经证明,如果EntityBean的Remote不是必须的,那么RemoteEntityBean性能上是不可行的,它只能工作在SessionBean后端,然而,即使EntityBean工作在SessionBean后端,但是EntityBean本身的局限性也太多,粒度要么太粗要么太细,性能、功能太弱,等等,开发数据应用非常地蹩脚,那么如果,在Remote EntityBean不是必须的情况下,我为什么不完全放弃EntityBean,在SessionBean后端使用其它的O/R Mapping Tool来开发数据应用,譬如Hibernate。这就是,EntityBean可以走第二条路。当然,从某种意义上来说,也是它必须走的路。

第二条路:完全抛弃EntityBean,采用Hibernate这样的O/R Mapping Engine作为Session Bean、Message-Driven Bean的后端数据持久化工具。而从EJB3.0可以看出,EJB3.0的确完全抛弃了传统的EntityBean模型。个人意见:可以这样说吧,EntityBean已经不复存在,Expert Group在SessionBean下给你换上了一个非常sharp的Persistence engine,你拿着engine,想干什么就干什么好了(上面讲过,EntityBean中,PersitenceEngine对client是通明的,这是由这两种引擎的本质作用决定的。有人说,EntityBean Application中不可以使用Dynamic Query,只能在配置文件中申明EJBQL,这些都是两种Persistence Engine的本质所决定的)。蹩脚的、强制模型的EntityBean不复存在!另外,EntityBean Remote特性在EJB3.0中根本没有提到,或许只是作为一个可选特性了吧(我还没有想到,EJB3.0中,如何来支持Remote PO,这个问题很诡异)。看来,Expert Group已经彻底否定了EntityBean的设计,或者说EntityBean的确是不符合实际需求的,Remote EntityBean、Remote Domain Object在绝大多数情况下是不切实际的。

话外题:Hibernate和JDO的关系,很微妙。EJB3.0和JDO的合并、Gavin进入EJB3.0 ExpertGroup令人很迷惑。EJB3.0的持久化模型采用JDO,应该是理所当然的。但是,目前,EJB3.0的Persitence Engine部分似乎被Hibernate左右,那么JDO的位置应该在哪里?

六、Spring将走向何方

  无疑,“听起来很美妙的”IOC、实力、实用派Spring AOP、集成大量framework的Spring是目前、对分布式、高级J2EE特性要求不强的系统的最合理选择。但是,你可以看到,Spring能做到的,除了集成大量framework这个特性外(当然这个永远不会被写进EJB Spec,但是如果EJB Server供应商想这样做,也是非常简单的事),EJB3.0也能做到,而且很多地方做的比Spring好很多,最重要的,EJB是标准,所以,很肯定的说,如果Spring OS、free,保持目前的姿态发展,仍然会成为开发人员不错的选择,然而,如果Spring试图商业化,我是Rd Johnson的话,我会向EJB3.0靠拢,摇身成为EJB3.0 Server提供商。

七、EJB3.0是J2EE商用framework的未来

  大肆革新过的EJB3.0,是J2EE商用framework的将来。

你可能感兴趣的:(EJB(2.X-3.0)、Hibernate、Spring:剖析、批判和展望)