EJB设计模式概述
周警伟
一. 设计模式重要性
采用EJB技术的J2EE项目中,EJB架构的设计好坏将直接影响系统的性能、可扩展性、可维护性、组件可重用性及开发效率。项目越复杂,项目队伍越庞大则越能体现良好设计的重要性。
二. 常见EJB设计模式
Session Facade Pattern
通常项目中,客户端往往需要频繁的对服务器端数据进行操作。当采用实体EJB作为数据的抽象层时,如果直接让客户端程序与实体EJB交互,会产生实现一个业务需求便需要大量的EJB属性操作(如下图1)。这直接导致如下问题:网络负载大(远程客户端时)、并发性能低、客户端与服务器端关联度大、可重用性和可维护性差、性能
因此有必要在客户端与实体EJB层间加入Session EJB层,在Sessino EJB中实现商业逻辑并封装对实体EJB的操作。(如下图2)
图1:客户端直接与实体EJB交互
图2:通过SessionEJB层实现
Session Fa?ade模式的好处是:降低了网络负载,SessionEjb可以调用实体EJB的本地接口;将商业逻辑与商业数据隔离;维护与开发方便;显著提高性能。
Session Fa?ade模式因其简单使用,是目前使用很广的模式。但具体应用过程中应注意:避免将所有的操作封装到一个很大的SessionEJB内;服务器端数据结构应由实体EJB实现,除非特例否则避免直接的数据库操作;SessionEjb内某些系统通用操作的代码容易重复(比如权限检查等,解决办法是将系统通用服务封装在Java Class内)。
Message Facade Pattern
很多时候,一次Request需要操作多个EJB又不需要得到即时返回。对这种异步调用,通常应用Message Fa?ade Pattern.
这种时候,如采用Session Fa?ade Pattern存在如下问题:
1. 客户端等待返回的时间过长。一个SessionEjb的实例在完成客户请求过程中中涉及到的每一次对其他实体Ejb的调用过程中都会被锁定直到得到实体EJB返回信息后才能进行下一步操作。这样造成客户不必要的等待,并很容易因时间导致整个事务失败。
2. 系统可靠性和容错性低。如果需要调用不同系统或服务器上或多个异构数据源的多个EJB时,任何一个环节出错,均导致客户请求失败。
以Message-Driven Bean为基础的Message Facade Pattern则可以解决上述异步请求需求。具体架构见下图3
图3:使用Message Facade Pattern
Message Facade Pattern的不足之处在于:
1. Message-Driven Bean没有返回值。这样通知客户执行结果只能依赖于Email或人工等其他手段。
2. Message-Driven Bean执行过程中无法将捕获的异常直接返回给客户端,即无法使客户端直接直到错误信息。
3. Message-Driven Bean通过接收Message响应客户请求,对Message内容的合法性(比如对象的类型等)依赖与客户端.容易产生运行时错误。
Message Facade Pattern经常与Session Facade Pattern在同一个项目里共同使用。
EJB Command Pattern
Session Facade Pattern中将商业逻辑实现封装在Session EJB中,这种做法带来诸多益处之外也带来如下问题:
1. 由于业务经常的变化,导致经常需要更新Session EJB代码。
2. 客户端代码不得不包含大量EJB相关的API,不利于后期项目维护。
3. 项目开发测试需要经常的EJB重部署过程。
引起上述问题的重要根结就是Session EJB本身重量级组件,其开发测试部署工作量较大,开发周期较长。以上不足可以通过EJB Command Pattern克服。
EJB Command Pattern中将商业逻辑实现封装在普通的Java Class(称之为Command Bean)中。该模式的具体实现有很多种,通常的框架都包括三部分:
1. Command Bean.由应用开发者写的具体实现某商业操作的Java Class.主要包含getXXX(),setXXX(),execute()方法。
2. Client-Side Routing Logic.由多个Class组成,用于将请求转发至Command Sever,这个过程对客户是透明的。这部分代码可以跨项目使用。路由规则中可以考虑用XML技术。
3. Remote Command Server.实际执行商业操作请求。通常可以用Session EJB层实现。
整个框架见下图4:
图4:Command的基本框架
EJB Command Pattern具有如下好处:
1. 适应与需要快速开发环境。因Command Bean是轻量级的Java Class,其编译和调试比较方便。
2. 将表现层与商业实现层隔离,同时将客户端代码与EJB层隔离。
3. 将客户端代码开发与服务器端代码开发相对清晰。早期可以创建空的Command Bean方便客户端代码调试。
EJB Command Pattern的弱处在于:
1. Command Bean中对事务的控制不如Session EJB中。
2. Command Bean是无状态的。
3. 无法将异常直接返回给客户。
4. 在大项目中,由于商业逻辑复杂,常导致大数量的Command Bean存在.
5. 作为Command Server的Session EJB打包时必须包含Command Bean以致存在维护上的不便。
EJB Command Pattern的一个实际实现可以参考IBM's Command Framework.
Data Transfer Object Factory
基于EJB的J2EE项目,经常需要在客户端与服务器端传输大量数据。数据的组织形式常用的是DTO(Data Transfer Object,服务器端数据对象的抽象)。但因为客户端表现层经常是变化的,所需要服务器端数据也变动频繁,换句话说,DTO的数量和属性经常要更改。因此如何以及在何处生成和维护DTO便是需要考虑的问题。
一种解决方案是直接在Entity EJB中直接处理,即在Entity EJB的Bean类中加入getXXXDTO()、setXXXDTO()等。但这样做导致EJB与DTO层紧紧绑定。一旦DTO更改,与该DTO相关的EJB即需要重编译打包。EJB层与客户端层相关联不仅使维护困难而且导致EJB的重用性大大降低。
更好的解决方案是利用Data Transfer Object Factory封装对DTO的操作逻辑(如下图6)。
图6:DTO Factory示例
DTO Factory具体实现方式通常有两种:
1. 普通Java Class实现,用于Session Facade Pattern使用DTO环境下。
2. Stateless Session EJB实现,用于非EJB客户端使用DTO环境下(见图7)。
图7:SessionEJB实现DTOFactory
DTO Factory带来如下好处:
1. 使Entity EJB的重用成为可能。由于不含DTO处理逻辑,Entity EJB功能单一化,只作为数据源。不通客户端通过各自的DTO Factory可以从同一个Entity EJB得到各自所需的个性化数据(自定义DTO)。
2. 提高可维护性和性能。
3. 可以根据在DTO Factory层生成很复杂的DTO结构,诸如继承、关联关系等,而对客户端提供一个透明、细化的数据接口。
使用DTO Factory时需要注意的是:不需为每个Entity EJB定义一个Factory。可以为一系列相关的Entity EJB创建一个Factory,或者只创建一个Factory。
Generic Attribute Access
使用Entity EJB作为商业数据层时,我们首先需要从数据库加载数据,创建对应的Entity EJB实例,之后对内存中Entity EJB实例的属性进行相应操作。对属性的操作比较直接的做法是:直接调用Entity EJB的getXXX()/setXXX(),通常利用EJB2.0的本地接口;通过DTO Factory生成DTO。但这两种做法都存在如下问题:
1. 当Entity EJB的属性特别多时候,以上做法会带来复杂罗嗦的代码,使EJB变的庞大无比。
2. 使Entity EJB的客户端(比如Session EJB)和Entity EJB的接口紧密关联。Entity EJB属性的增删都需要更改客户端代码,给项目开发和维护带来不便。
事实上可以利用更通用的方式访问Entity EJB的属性,即定义Generic Attribute Access Interface。见下图8:
图8:Generic Attribute Access Interface示例
Generic Attribute Access Interface由Entity EJB的本地或远程接口实现,并利用Hash Maps传输数据。实现方式常见如下:
1. BMP类型实体EJB可以在Bean类中定义包含所有属性的私有成员变量HashMap。
2. CMP类型实体EJB可以在Bean类中可以适用Java Reflection API实现。
3. 建立一个父类,在不同的情况下定义子类重载父类方法。
使用Generic Attribute Access Interface需要在客户端与服务器端对属性以及对应的关键字建立统一的命名习惯。常见的做法如下:
1. 建立并保持良好的文档记录和命名约定。
2. 在实体EJB的实现类中定义静态成员映射属性。
3. 创建共享静态类,通过成员变量映射实体EJB属性。
4. 通过JNDI在服务器端保存属性映射关系。
Generic Attribute Access Interface的运用带来一下益处:
1. 接口实现后对不通实体EJB都适用。
2. 对属性较多实体EJB能精简代码,并更具维护性。
3. 使运行中动态增删实体EJB属性成为可能。
Generic Attribute Access Interface的缺点在于:
1. 访问EJB属性时增加了额外的操作。需要通过关键字映射属性,最后还需进行类型转换。
2. 需要建立客户端与服务器端的命名约定。
3. 因为通过HashMap操作时候需要进行类型转换,容易产生运行时类型不匹配异常。
Business Interface
EJB规范要求Bean实现类必须实现所有在远程(或本地)接口中定义的所有方法,同时不允许Bean实现类直接继承远程(或本地)接口。这就导致编译时候很容易产生两者不一致的问题,即远程(或本地)接口中定义的某方法为在Bean实现类中被实现等错误。为避免上诉错误,可以利用应用服务器厂商所提供的工具。但也可以应用EJB的设计架构来实现:定义商业接口。
Business Interface即创建自定义商业接口,在接口中定义所有EJB提供的商业方法,并让Bean实现类和远程(或本地)接口都实现该商业接口。其继承关系见下图9:
图9:商业接口的使用
Business Interface是个普通的Java Class。依赖于使用本地接口与远程接口的不通,Business Interface的定义略有不同:应用与远程接口时,在接口中的方法需要抛出java.rmi.RemoteException;而应用与本地接口时候则不需要作任何特别处理。
应用Business Interface时候必须注意一点:EJB规范不允许直接EJB的实例将对自己的引用(this对象)返回给客户端,否则编译时候即报错。但使用Business Interface后,编译时候无法检查出有无将this对象返回给客户端。这一点需要程序员自己保证。
三. 内部数据转换策略
Data Transfer Object
基于EJB的J2EE多层架构应用中,经常涉及的一个问题就是如何在各层之间传递批量数据,比如客户端对服务器端数据的批量读写操作等。比如需要得到实体EJB的属性,直接的方法是多次调用不通的属性,如下图10:
图10:低效的数据传递方式
但这种方法容易导致许多问题,比如性能以及代码的复杂度等,更有效的办法是在一个调用中得到所有需要的属性。所以可以引入Data Transfer Object来封装所需要的属性,并在客户与服务器端通过传递该对象一次实现对数据的操作。如下图11:
图11:通过DTO传递数据
DTO为普通的Java Class,通常是服务器端数据的快照。由于网络传输的需要,DTO应该实现java.io.Serializable接口。
DTO的设计有两种模型:Domain DTO以及Custom DTO。
Domain DTO仅仅实现对服务器数据的拷贝,通常与实体EJB为一对一的关系(也存在为多个相关联的实体EJB对应一个Domain DTO)。Domain DTO通常除用于读取更改实体EJB属性外也可用于创建实体EJB时候。实体EJB与Domain DTO对应关系如下图12:
图12:Account EJB 与 Account DomainDTO
Domain DTO的应用除了DTO所具有的一般优点外,还有别的益处:
1. 开发迅速。因为一旦实体EJB设计好后,很容易转换得到Domain DTO。
2. 可以利用Domain DTO的setXXX()方法在客户端进行属性有效性效验。
Domain DTO的缺点有:
1. 客户端绑定了服务器端数据模型,不利于维护。
2. 不够灵活,无法处理客户端的多样化数据要求。对一个数百个属性的实体EJB请求一个属性时候却返回一个包含所有属性值的Domain DTO明显是笨重的实现。
3. 导致代码的重复。
4. Domain DTO中如果嵌套包含了别的Domain DTO时,一旦需服务器端数据的更改而需要重定义Domain DTO模型时候异常困难。
Custom DTO则可以克服上述的一些缺点。Customer DTO仅仅封装用户感兴趣的服务器数据集即可以根据客户端需求创建Customer DTO。这样作的优点是灵活高效;缺点是大项目中可能导致大量的Customer DTO存在。
通常Domain DTO可以用于数据的更新与创建;Customer DTO可以用于客户用于表现层的数据读取。两者可以相辅相成。而且使用DTO一般与DTO Factory同时使用。
Domain Transfer Hash Map
DTO的使用往往缺乏通用性。不通的用户案例需要创建不同的DTO。当项目很复杂时,从维护性考虑需要更好的数据传输的实现方式。
Domain Transfer Hash Map即利用HashMap作为客户所需数据集的封装。好处是:
1. 良好的维护性。
2. 较大的通用性。不同的客户端可以使用相同的数据传递方式。
缺点是:
1. 需要维护客户端与服务器端在属性及其对应关键字的映射关系。
2. 当需要使用基本类型的数据时候,因为Hash Map的限制必须将基本类型先转换成对象。
3. 使用得到的数据时,需要进行类型强制转换。
Data Transfer RowSet
当需要处理直接的JDBC调用得到的结果集时,显然用DTO/Hash Map已经不合适,因为需要对大量数据进行类型转换等额外操作是很费资源和不必要的,而且最终用户常需要以表格式样显示数据。
所以对二维表式数据,更好的处理方式是利用Data Transfer RowSet。Data Transfer RowSet通过将ResultSet直接转换为RowSet传递给客户端。
在Session EJB中使用RowSet的一段示例代码如下图13:
图13:使用RowSet
使用RowSet的好处很多:
1. 接口通用于各样的数据库查询操作。
2. 当需要表格式数据显示时,因为直接从ResultSet得到,所以不需要额外的数据类型转换。
缺点是:
1. 数据库结构暴露给客户端。
2. 不符合面向对象设计思想。
3. 依赖于SQL。
Data Transfer RowSet通常用于只读式数据的显示操作,经常和JDBC for Reading Pattern连用。
四.事务和数据持久机制
JDBC for Reading Pattern
基于EJB的J2EE应用中,通过EJB对数据库的操作可以有两种方式:实体EJB或者Session EJB中直接利用JDBC访问。
客户很多时候取出数据库中数据并以表格方式显示。这种情形如果使用实体EJB会导致如下问题:
1. 引用服务器端频繁的数据库查询和加载操作。因为加载N个实体EJB总需要进行一次find()操作 N次数据加载。
2. 如果使用Remote接口,引起频繁的额外网络操作。
3. 对关联关系比较复杂的数据库表结构,很难直接通过Entity EJB表现。
因此建议在只需对数据库表数据进行只读访问时候,应该采用JDBC for Reading Pattern,即通过JDBC直接访问数据库。除了避免上述使用实体EJB的缺点还带来一下好处:
1. 充分利用数据库能力,比如数据库的缓存机制。
2. 减少了对事务控制的资源。
3. 利用自定义SQL可以按需要比较灵活的读取数据。
4. 只需要一次数据查询,减少了数据库操作。
缺点是:
1. 于J2EE应用的面向对象设计相违背。
2. 因为Session EJB代码中包含了自定义SQL,维护性差。
3. Session EJB中不得不包含JDBC的API,并且需要了解数据库结构。