IBM®
跳转到主要内容
    中国 [选择]    使用条款
 
 
Select a scope: Search for:    
    首页    产品    服务与解决方案     支持与下载    个性化服务    
跳转到主要内容

developerWorks 中国  >  WebSphere  >

EJB 倡导者: 在面向服务的体系结构中,使用无 facade 的 EJB 组件是最佳选择吗?

来自 IBM WebSphere 开发者技术期刊

developerWorks
文档选项

未显示需要 JavaScript 的文档选项


级别: 中级

Geoff Hambrick (ghambric@us.ibm.com), 杰出工程师, IBM

2005 年 6 月 15 日

EJB 倡导者对使用各种形式的 facade 进行评价,包括 POJO、HttpServlet、会话 EJB 组件、消息驱动 Bean 和实体 EJB Home 方法,目的是试图了解使用 J2EE™ 组件实现良好的面向服务体系结构的核心。

在每个专栏中,EJB 倡导者提出要点,客户和开发者采用独特的前后衔接的对话框方式,在对某一感兴趣的设计问题推荐解决方案的过程中进行交流。其中忽略了任何确定性的细节,并且不提供“革新的”或专有的体系结构。详细信息请参阅 EJB 倡导者介绍

揭开 EJB 组件的面具

在所有以前的 EJB 倡导者文章中,EJB 组件都包在某种类型的facade下面。例如,在有关 EJB 交叉引用的文章中,该团队的开发人员使用纯 Java (POJO) 包装器来包装会话 Bean,以便隐藏查找 Home 接口和创建来自客户机引用的复杂性。会话 Bean 本身用 POJO helper 类包围起来,从而将实现服务提供商提供的 EJB 的复杂性隐藏起来。简而言之,该团队的目标是从根本上隐藏使用 EJB 这一事实。图 1 说明了作为封装层的 POJO 的用法。


图 1. 展示作为封装层的 POJO 用法的 UML 图
图 1. 展示作为封装层的 POJO 用法的 UML 图

在两篇讨论 CMP 实体执行的文章(第 1 部分第 2 部分)中,给出的建议是,在有 facade 展现本地 CMP 时,请始终使用会话 EJB 方法将数据传输对象(Data Transfer Object,DTO)传进传出——这样既可以确保存在全局事务,而且会最大限度减少层之间的通信。图 2 说明 session facade 如何在客户机 (HttpServlet) 和数据层(实体 EJB 组件)之间进行调节,从而提供面向服务的体系结构


图 2. 展示作为面向服务调节者的会话 EJB 的 UML 图
图 2. 展示作为面向服务调节者的会话 EJB 的 UML 图

在我们推出这一系列 EJB 倡导者文章后,可以用一条格言来作出结论:没有糟糕的模式,只有模式使用不当的糟糕应用程序。换句话说,这句格言实际上在告诫我们:一个人在说话时,绝不要用“从不”或“始终”之类的词语。请暂时忘掉我刚才所说的“从不”一词,下面的讨论将表明我的真正意图,即在某些情况下,最好直接使用 J2EE 组件,而在某些情况下,最好间接(通过某种类型的 facade)使用 J2EE 组件。在进行本讨论之前,我不清楚用户选择直接使用或间接使用的原因。





回页首


问题:组件太多

亲爱的 EJB 倡导者:

在上一篇文章中,我们的团队非常高兴地看到您最后展示了一个真实的面向对象的方法来使用实体 EJB 组件。我们从一个 Smalltalk 商店买了一本书,这本书讲述强制将 Smalltalk 切换到 Java™,我们有一点闷闷不乐的是,大多数 J2EE 开发人员已求助于过程编码风格。

但面向对象或许不是这样,如果将你们的所有文章都收集到一起,则可以获得用于构建的许多类。从客户机开始,这些类有:

  1. HttpServlet——接受来自浏览器的请求并调用关联的服务
  2. Java Server Page——呈现来自服务的结果
  3. Service Delegate——对是否使用会话 EJB 进行封装
  4. Service Session EJB Home——获得对会话 Bean 的远程引用
  5. Service Session EJB Interface——对委托隐藏存根实现
  6. Service Session EJB Bean——提供分布、事务和安全性
  7. Service POJO——确定要执行的适当任务的业务逻辑
  8. Task Delegate——对是否使用会话 EJB 进行封装
  9. Task Session EJB Home——获取对会话 Bean 的本地引用
  10. Task Session EJB Interface——对委托隐藏本地 EJB 实现
  11. Task Session EJB Bean——提供分布、事务和安全性
  12. Task POJO——任务的业务逻辑(假定这是 entity facade)
  13. Business Object Delegate——对是否使用实体 EJB 进行封装
  14. Business Object Key——给定数据对象的主键(或查询对象)
  15. Business Object View——最大程度减少层之间调用的数据传输对象
  16. Entity EJB Home——获得实体 EJB 的本地引用
  17. Entity EJB Interface——对委托隐藏本地 CMP 实现
  18. Entity EJB Implementation——与数据层关联的逻辑

该清单仅包含只有一个服务、任务和业务对象的应用程序的基本类。它甚至没有包含部署工具生成的部署描述符和所有代码!

现在,想像一个更复杂的应用程序,如同您在上篇文章中展示的数据模型那样。这个应用程序可能有许多“服务”、“任务”和“业务对象”,它们在每一层都有许多要处理转换的自定义数据传输对象。我们没有交叉引用 那样的代码生成器。因此我们倾向于尽可能直接跳过所有委托而使用“正确的”EJB 组件,但不想一失足成千古恨。

请给我们回信,
Too Many Notes





回页首


使用委托、facade 和 Helper 时所要考虑的折中处理

亲爱的 Too Many:

请留意有关使用基于 Eclipse 的工具构建和使用代码生成器的文章。然而,即使这些工具能帮助您从应用程序模型中“自由”生成许多这些组件,但在运行时获得这些不需要的组件对效率的提高也没有好处。代码生成器实际上使这个问题变得更糟!另一方面,如果这些组件容易修改,则也可以很容易修正这些问题。

因此,就必须手工构建所有这些组件而言,很奇怪您采用了您不太愿意使用的体系结构。请使用委托类来“完全隐藏使用 EJB 这一事实”,如第一篇文章 Cross with EJB References 所述,这里提到所要考虑的一些折中处理,但不太详细,现在正是详细讨论的时候。因此,虽然我最终同意您观点,但是由于回复很长,我在此表示歉意。

我希望本次讨论也将帮助您澄清您的观点。

不同的 EJB 组件类型具有不同的访问方法
您列出的上述清单展示了在端对端体系结构中使用的 3 种不同类型的组件:

  • 远程服务
  • 本地任务
  • 业务对象

上述每种组件都与一个不同类型的 EJB 组件或接口样式(分别为远程会话 EJB、本地会话 EJB 和本地实体 EJB)关联。检索每个组件的代码是不同的。例如,下面是一个 HttpServlet 代码片断,用于调用名为 OrderEntry 的远程会话 EJB facade 中的 getCustomerWithOpenOrder() 方法(我们已在上一篇 EJB 倡导者文章中介绍了此方法的详细实现)。它包括正常和错误路径处理,从而展示了真正的复杂性:


清单 1. 调用远程会话 Bean
try {
    // Assume a routine to get the id exists on the superclass servlet
    int cID = getCustomerId(req);
    // The five lines of code to invoke a remote session method
    InitialContext initCtx = new InitialContext();
    Object obj = initCtx.lookup("java:comp/env/ejb/OrderEntry");       
    OrderEntryHome home = (OrderEntryHome)PortableObjectRemote(
	obj, OrderEntryHome.class
    );       
    OrderEntry ref = home.create();
    CustomerData data = ref.getOpenOrderForCustomer(cID);
    // Assume an include JSP method exists on the superclass servlet
    include("CustomerWithOpenOrder", data, req, res);
}
catch (ParseException e) {
    // Occurs if parse routine fails to get a valid positive integer
    include("ParseException", e, req, res);
}
catch (NamingException e) {
    // Occurs if JNDI context cannot be initialized or name not found
    include("NamingException", e, req, res);		
}
catch (RemoteException e) {
    // Occurs if the object cannot be narrowed, created or executed
    include("RemoteException", e, req, res);		
}
catch (CustomerNotFoundException e) {
    // Occurs if the customer ID is valid integer but not found
    include("CustomerNotFoundException", e, req, res);
}
catch (OrderNotOpenException e) {
    // Occurs if the customer has no open order
    include("OrderNotOpenException", e, req, res);
}

下面是本地会话的类似代码片断:


清单 2. 调用本地会话 Bean
try {
    // Assume a routine to get the id exists on the superclass servlet
    int cID = getCustomerId(req);
    // The four lines of code to invoke a local session method
    InitialContext initCtx = new InitialContext();
    OrderEntryHome home = (OrderEntryHome)initCtx.lookup(   
        "java:comp/env/OrderEntry"
    );       
    OrderEntry ref = home.create();
    CustomerData data = ref.getOpenOrderForCustomer(cID);
    // Assume an include JSP method exists on the superclass servlet
    include("CustomerWithOpenOrder", data, req, res);
}
catch (ParseException e) {
    // Occurs if parse routine fails to get a valid positive integer
    include("ParseException", e, req, res);
}
catch (NamingException e) {
    // Occurs if JNDI context cannot be initialized or name not found
    include("NamingException", e, req, res);		
}
catch (ClassCastException e) {
    // Occurs if the object cannot be cast to the home class
    include("ClassCastException", e, req, res);		
}
catch (CustomerNotFoundException e) {
    // Occurs if the customer ID is valid integer but not found
    include("CustomerNotFoundException", e, req, res);
}
catch (OrderNotOpenException e) {
    // Occurs if the customer ID has no open order
    include("OrderNotOpenException", e, req, res);
}

这个清单与上个清单的区别在于,您不再需要检查远程异常,并且“收缩转换”由一个简单的类型转换运算符替换。

现在,我们比较一下本地会话代码与调用在(本地)Customer 实体 EJB 上实现的等同方法所需的代码:


清单 3. 调用本地实体 Bean
try {
    // Assume a routine to get the id exists on the superclass servlet
    int cID = getCustomerId(req);
    // The five lines of code to invoke a local entity method
    InitialContext initCtx = new InitialContext();
    CustomerHome home = (CustomerHome)initCtx.lookup(   
        "java:comp/env/Customer"
    );
    CustomerKey key = new CustomerKey(cID);
    Customer ref = home.findByPrimaryKey(key);
    CustomerData data = ref.getOpenOrder();
    // Assume an include JSP method exists on the superclass servlet
    include("CustomerWithOpenOrder", data, req, res);
}
catch (ParseException e) {
    // Occurs if parse routine fails to get a valid positive integer
    include("ParseException", e, req, res);
}
catch (NamingException e) {
    // Occurs if JNDI context cannot be initialized or name not found
    include("NamingException", e, req, res);		
}
catch (ClassCastException e) {
    // Occurs if the object cannot be cast to the home class
    include("ClassCastException", e, req, res);		
}
catch (FinderException e) {
    // Occurs if the customer is not found
    include("FinderException", e, req, res);	
}
catch (OrderNotOpenException e) {
    // Occurs if the customer ID is invalid
    include("OrderNotOpenException", e, req, res);
}

本地会话和本地实体之间的不同之处在于是否需要创建 key 实例、使用自定义的查找器和处理可能出现的 FinderException 异常。

作为折中处理,请注意没有必要将客户 ID 传递给 getOpenOrder() 方法(因此此组件代表与此 ID 关联的客户)。还要注意,它的名称不需要指定“ForCustomer”来将它与 session facade 中其他可能的方法区别开来。

EJB 组件从面向服务的签名中获益
不论这些方法之间有何差别,每种方法都返回 CustomerData 对象以及关联的 OrderData 对象。每个 OrderData 对象都与 0 个或多个 LineItemData 对象关联。从一组参数返回数据传输对象的完整“树”(也可能是 DTO 树)可最大程度地减少层间的通信,从而使此体系结构更具有面向服务的特点。换句话说,收集上述 HttpServlet 中所需的数据只需要一个粗粒度调用。对 DTO 传进传出(包括对异常)将使得客户机和服务器之间的绑定成为无状态的(也称为“断开连接”),即使使用实体 EJB 实现此绑定也是如此,这是因为下一个请求将使用客户 ID 来检索关联的实体。

DTO 是可序列化的,这一事实表明有可能使用其他服务(称为调节者)将这些 DTO 转换为其他形式。例如,上述代码中的 JSP 可被认为是一个调节者,以将 CustomerData 在与网页关联的 HTML 中呈现出来。如果使用该组件实现 Web 服务,则网关可能与调节者一起将 CustomerData 转换为 XML 文档,该文档是对非 J2EE 客户机进行 SOAP/HTTP 回复的一部分。

良好的面向服务体系结构使客户机代码之间具有松散耦合关系,这样可以根据当前状况进行实现,而无需更改客户机代码。松散耦合最简单的例子是对服务进行这样的编码:根据当前配置在远程或本地实现。较复杂的例子有,根据客户分为黄金客户、白银客户、黄铜客户还是未指定,对 submit() 服务进行不同的实现。支持相同 Bean 接口的 EJB 的任何版本的 Home 接口都可以被绑定到 JNDI 名称空间。此名称被设计为既包含 Bean 类型,又包含类别。在运行时,类别被附加到此名称,以透明检索对客户机代码的正确实现。

委托模式可以大大简化客户机代码
设计良好的面向服务签名的另一个好处是,(不考虑用于实现这些签名的 EJB 组件类型)可以使用委托(或服务定位器)模式。例如,在二月份的文章中讨论的模板继承模式可用于创建通用超类 HttpServlet 方法,该方法高速缓存 EJB Home 接口(或甚至高速缓存会话引用)并以常规方式处理错误。

但是,模板超类仅仅是委托(或服务定位器)模式的一种形式。在以前的文章中,我们讨论过其他委托模式,但没有展示代码。

例如,我们在一月份文章中讨论的常用的委托模式是一个通过新运算符访问的不同 Java 类,如下所示:


清单 4. 使用新运算符通过委托调用服务
try {
    // Assume a routine to get the id exists on the superclass servlet
    int cID = getCustomerId(req);
    // The two lines of code to invoke the service
    OrderEntryDelegate ref = new OrderEntryDelegate();
    CustomerData data = ref.getOpenOrderForCustomer(cID);
    // Assume an include JSP method exists on the superclass servlet
    include("CustomerWithOpenOrder", data, req, res);
}
catch (ParseException e) {
    // Occurs if parse routine fails to get a valid positive integer
    include("ParseException", e, req, res);
}
catch (ServiceException e) {
    // Occurs if the service is not able to be invoked 
    include("CustomerNotFoundException", e, req, res);
}
catch (CustomerNotFoundException e) {
    // Occurs if the customer ID is valid integer but not found
    include("CustomerNotFoundException", e, req, res);
}
catch (OrderNotOpenException e) {
    // Occurs if the customer ID has no open order
    include("OrderNotOpenException", e, req, res);
}

此代码片断表明,在您最大程度减小“移动部分”时,会相应减少必须在正常和错误路径中编写的代码行。只要使用委托类的潜在客户机数量大于 1,则似乎应进行折中考虑。

但是,使用委托的好处远不止使客户机更容易实现组件。客户机只需调用所需的服务即可,不论这个服务是组合服务、原始任务,还是对简单业务对象的访问,都不必在层之间进行转换和传送。换句话说,只有在服务方法提供客户机所需的某种转换时才实现此服务方法。选择要使用哪个 EJB 组件对客户机完全隐藏(如果使用了 EJB 的话)。

注意不要使用委托重新创建 J2EE 框架
对委托使用“新”方法所产生的一个问题是,每次调用委托时,都要创建和初始化委托的实例,除非进行高速缓存,否则将花费额外的时间,生成额外的垃圾。如果使用实际引用的高速缓存,则它必须是线程安全的,特别是在多线程客户机的上下文中使用时更是如此,这与 HttpServlet 类似。

自动处理实例高速缓存的替代方法是采用 Singleton 模式,该模式是使用类中的静态方法和成员变量实现的。该方法就像一个新运算符,只返回服务实现的单个副本:


清单 5. 使用 Singleton 模式实现委托
public class OrderEntryDelegate {
    private static OrderEntryDelegate instance = 
        new OrderEntryDelegate();
    public static singleton() { return instance; }
    // Insert rest of the delegate code here including cached variables
}

但是,对委托而言,使用新方法或 Singleton 方法所产生的另一个问题是,委托的类与客户机代码是紧密耦合的——即使可以通过在包中替换 JAR 文件来更改实现也是如此。有些团队解决此紧密耦合问题的方法是创建纯 Java 接口,该接口由充当 Factory 的类返回。该 Factory 类与 Singleton 类一样有一个静态方法,但却返回接口,如下所示:


清单 6. 使用 Factory 模式实现委托
public interface OrderEntryDelegate {
    // Insert service signatures here
}
public class OrderEntryDelegateImpl
implements OrderEntryDelegate {
    // Insert delegate implementations here
}
public class OrderEntryFactory {
    private OrderEntryDelegate instance = 
        new OrderEntryDelegateImpl();
    public static OrderEntryDelegate getDelegate() {
        return instance;        
    }
}

当然,此方法多加了两个要在体系结构中考虑的“注释”。为了删除其中一个类,某些团队通过使 Factory 实现委托接口将此 Factory 与默认实现结合起来(这一点与对 JNDI InitialContext 执行的操作非常类似)。而且,要获得某些重用,有些团队将委托接口用作 EJB 本地接口和 Bean 实现类的一部分,如下所示:


清单 7. 在本地会话 EJB 中重用委托接口
public interface OrderEntry 
extends OrderEntryDelegate, EJBLocalObject {
}
public class OrderEntryBean 
implements OrderEntryDelegate, SessionBean {
   // Insert service implementations here
}

无论如何,上面展示的 Factory 实现与 Singleton 基本等同,但它已构建成可以带环境变量或其他输入。如果选择该方法,则紧密耦合就转换为 Factory 本身。换句话说,Factory 类就与客户机代码紧密耦合。

我们了解到,有些团队尝试使用 FactoryFinder 模式解决此问题。在此模式下,上面的每个 Factory 都提供接口和实施。极端的情况是每个 Factory 都继承一个通用 Factory 接口——该接口仅仅是个标记(因此有些仅使用对象)。FactoryFinder 是只使用一次的类,用于将名称绑定到实现类:


清单 8. 使用 Factory Finder 模式实现委托
public interface Factory {
}
public interface OrderEntryFactory extends Factory {
    public OrderEntryDelegate getDelegate();
}
public class OrderEntryFactoryImpl implements OrderEntryFactory {
    private OrderEntryDelegate instance = 
        new OrderEntryDelegateImpl();
    public OrderEntryDelegate getDelegate() {
        return instance;        
    }
}
public class FactoryFinder {
    private HashMap factories = new HashMap();
    public FactoryFinder() {
       // Bind the implementations, possibly using environment vars
    }
    public Factory getFactory(String name) {
        return (Factory)factories.get(name);
    }
}

在实现 J2EE 之前,这是大多数团队的企业 Java 框架所能达到的程度。但显而易见,采用这种极端方法实现委托需要完全重新创建 EJB 框架:

  • FactoryFinder 等同于 JNDI Context。
  • Factory 等同于 Home 接口。
  • Delegate 等同于本地 EJB 接口。
  • DelegateImpl 等同于本地 EJB 实现。

我们似乎陷入困境,因为最简单的极端方法具有这样的缺点:最大程度降低面向服务体系结构中适应性的有效性。

Helper 类可以在容器之外测试并最大程度减少对 EJB 的需要
但是,在放弃委托方法之前,应考虑使用 Helper 类代替 EJB 的优点:对应用程序编程人员(客户机或服务器)完全隐藏对 J2EE 的使用,这正是交叉引用 所希望的。使用 Helper 模式的大多数团队都指出了这种方法的优点,即可以不使用 EJB 容器进行单元测试。当 Helper 类与基于委托模式的 Factory-(或 FactoryFinder-)结合使用时,Helper 类可以代替 DelegateImpl 进行功能验证测试(FVT,多个服务一起测试)。要进行替代,Helper 类只需扩展适当的 Delegate 接口即可:


清单 9. 在 Helper 类中重用 Delegate 接口
public class OrderEntryHelper
implements OrderEntryDelegate {
    // Insert service implementations here
}

业务对象层的 FVT Helper 可以根据委托类型只包含通过初始化器或构造函数加载的实例的散列表。

对于系统测试和生产,会话 Bean 实现是在任何情况下都通过 Helper 类的真正“facade”:


清单 10. 使用 Helper 模式实现会话 Bean
public class OrderEntryBean 
implements OrderEntryDelegate, SessionBean {
   private OrderEntryHelper helper = null;
   public void ejbCreate() {
       helper = new OrderEntryHelper();
   }
   // Insert service implementations here
   public CustomerData getOpenOrderForCustomer(int cID) {
       return helper.getOpenOrderForCustomer(cID);
   }
}

SVT 和生产业务对象层委托可以由这里展示的 CMP 实现所代替,甚至可以由 JDBC 实现(如果您没有阅读上两篇文章所讨论的 CMP)所代替。不管是哪种情况,都可以将体系结构设置为只使用“最外面”委托中的会话 Bean 实现,从而最大程度减少对 EJB 的使用,但仍可获得 EJB 提供的事务、安全性甚至分发功能。

因此,有多少“注释”留给我们?实际比以前要多,但次数不同:

  • 仅一次——FactoryFinder 和 Factory 接口
  • 整个阶段——DelegateFactory 接口、Delegate 接口、Helper 实现、Key、Query 和 View DTO
  • UT 和 FVT -- FVTFactory 实现(如果存在会话/任务服务,则返回 Helper 实现)和 CachingDelegate(用于数据服务)
  • SVT 和生产——SVTFactory 实现、Delegate 实现、EJB Home 接口、EJB 接口和 EJB 实现。

因此,如果我们认为这些优点均衡与委托、facade 和 Helper 没多大关系,则只能采用以前的方法。

J2EE 执行上下文可以简化签名和关联代码
是吗?在将 J2EE 框架隐藏在委托和 Helper 类之后,许多人忘记的一件事情是 J2EE 执行上下文是不可用的。事实上,许多 J2EE 编程人员都忘记使用它来简化服务签名。

例如,J2EE 客户程序可以从安全性上下文中对用户 ID 进行访问,这样就无须从输入参数中解析用户 ID,并将其传递到服务签名,如此 Servlet 客户机所示(说明代码可以达到的简单程度):


清单 11. 使用模板继承模式调用未知类型的 EJB 组件
public class GetOpenOrderServlet extends CustomerServlet
{
    public void doGet(
        OrderEntryDelegate ref,
        HttpServletRequest req,
        HttpServletResponse res
    ){
        try {
    	    // The ref is passed in from the template superclass    
    	    CustomerData data = ref.getOpenOrder();
	    // Assume an include JSP method exists on the superclass
	    include("CustomerWithOpenOrder", data, req, res);
        }
        catch (OrderNotOpenException e) {
            // Occurs if the customer ID has no open order
            include("OrderNotOpenException", e, req, res);
        }
    }
}

此代码说明从 J2EE 上下文派生客户 ID 这种能力将使服务签名变得相同,不管是在客户中心会话中实现还是在实体 EJB 中实现。

最后,大多数人容易忘记的有关 J2EE 规范的一点是,它只是一组接口,您的代码可以将这些接口实现为一个方法,以将各种对象标记为某些类型的组件。在实现并行框架(例如与基于委托模式的 Factory Finder 关联的框架)后,很容易以轻量级方式实现这些组件,以提供单元和功能测试环境。

但是,这对那些根本不愿意构建任何框架,而只将其注意力集中于支持其业务应用程序的团队不起多大作用。

这种分析的结果如何?总的来说,即使不符合 EJB 组件的轻量级单元测试环境的需要,但有一点我同意您的看法,即如果不使用各个委托类而只直接调用适当的 EJB 组件,则体系结构就变得更简单。

希望上述内容对您有所帮助,
您的 EJB 倡导者





回页首


有关不带 session facade 的 CMP 的未解答问题

亲爱的 EJB 倡导者:

您的分析很有帮助,但恐怕我们不能采用您的建议,我们真正感兴趣的是在使用面向对象方法时直接调用 Customer 实体。原因是 Customer 实体提供“持久性”会话,它与我们所需方法的所有数据都有关系。因为我们使用面向粗粒度无状态服务调用,就像您所建议的那样,所以 session facade 看起来是多余的。

在获得 Home 引用(在客户机启动时已作为一部分进行高速缓存)后,我们看看下面 3 行代码:

CustomerKey key = new CustomerKey(cID);
Customer ref = home.findByPrimaryKey(key);
CustomerData data = ref.getOpenOrder();

最初,我们喜欢的做法是从 J2EE 上下文派生客户 ID 来简化签名,但已意识到,必须在调用客户服务委托的情况下小心行事——其“持久性会话”CMP 仍必须具有传入的客户 ID,或您必须使用“当前客户”CMR。

总之,在通过跟踪表明存在两个 SQL 调用问题时,我们很惊讶——一个是查找问题,一个是 getOpenOrder 方法问题。还有两个事务。这一结果似乎在我们考虑这一结果并仔细读取规范后才有意义。因此,我们使用您展示的模板继承方法用 BMT 将实体包装起来,以减少必须编写和维护的组件的数量。这个方法解决了额外事务和 SQL 问题。但是,我们想知道是否还有更佳方法。

谢谢您,
Too Many Notes





回页首


自定义实体 Home 方法可以执行此功能

Too Many Notes:

您是否考虑了使用自定义实体 EJB Home 方法?这是我第一次建议使用它们,因为您的团队似乎是这样一个团队:

  1. 最大程度减少组件的数量且
  2. 熟练使用有 CMR 的 OO CMP。

实体 EJB 组件中的自定义 Home 方法实现起来相当容易:只需将代码从 session facade 移动到 EJB 实现类中名为 ejbHome<methodName> 的方法即可:


清单 12. 实现实体 EJB Home 方法
    public CustomerData  ejbHomeGetOpenOrderForCustomer(int id) 
    throws OrderNotOpenException
    {
	// Get the Customer DTO	
	CustomerKey key = CustomerKey new(id)
	Customer ref = findByPrimaryKey(key);
	return ref.getDataWithOpenOrder();
    }

然后将此方法放置于 EJB Home 接口中:

public CustomerData ejbHomeGetOpenOrderForCustomer(int id);

使用实体 Home 方法时,即使您不高速缓存 Home 引用,客户机代码也比会话 EJB 的代码简单:


清单 13. 调用实体 EJB Home 方法。
    InitialContext initCtx = new InitialContext();
    CustomerHome home = (CustomerHome)initCtx.lookup(   
        "java:comp/env/Customer"
    );
    CustomerData data = home.getOpenOrderForCustomer(cID);

此代码的优点是,无论是否使用 BMT 代码或 session facade 进行包装,都只存在一个事务。此代码作为方法工作良好,因为实体和用户之间通信流畅和存在作为“持久性会话”一部分进行访问和更新的数据的 CMR。

如果需要为工作单位编写的数据项之间的“关系”是临时的而非永久的(换句话说,数据聚合很特殊,而且没有真正得到底层数据模型的支持),则使用 session facade 更有意义。

感谢您的来信。这些内容有助于我考虑面向企业和服务的其他方面的问题。

就到这里吧,
您的 EJB 倡导者





回页首


结束语

对上述讨论进行总结,每种类型的 EJB 组件都在以下几方面存在差异:

  1. 如何从 JNDI 上下文检索 Home 接口(查找所用的名称以及是否需要进行收缩转换或类型转换)?
  2. 如何从 Home 访问组件引用(是否需要 key 或查询,是否使用创建或查找方法以及是否在 Home 接口本身可直接访问服务)?
  3. 调用服务方法时传递了哪些参数(是否传递 ID 参数)?
  4. 返回结果的性质是什么(断开连接,连接)?
  5. 抛出错误的性质是什么(已检查,未检查)?

最佳 EJB 组件是面向服务的组件,具有以下功能:

  1. 粗粒度——将函数设计为最大程度减少层之间的调用。
  2. 无状态——通过单个调用传入传出给定函数的所有相关数据,支持任何运行的实例以任何方式处理任何客户请求。
  3. 可调整——传入传出的数据是可序列化的,这样就可容易转换。
  4. 可适应——客户机和服务实现之间的链接是逻辑的(松散耦合)而非物理的(紧密耦合)。松散耦合支持不更改应用程序即可替代不同的实现,根据需要,客户机和服务器可以是分布式的,也可以是在同一位置。

通常,以通用的方式使用 POJO 委托类来简化对服务的调用。Helper 类用于简化构建服务。但是,为了提供上述适应性,委托和 Helper 常常从简单变为复杂:

  1. 超类方法——在运行时不需要其他实例。
  2. 新运算符——创建委托实现类的实例。
  3. Singleton——返回对委托对象单个实例的引用。
  4. Factory——返回委托接口实现的类。
  5. FactoryFinder——返回 Factory 的类,如上所述。

讨论到目前为止所出现的问题是,委托对 J2EE 框架是多余的。因此,下面是要进行折中考虑的基于委托和 Helper 的一些功能:

  1. 在客户机和服务器上启用纯 Java 编程模型。
  2. 不需要 EJB 容器启用 UT 和 FVT。
  3. 不触及服务实现而启用 SVT 和生产部署。
  4. 最大程度减少对 EJB 组件的需要。
  5. 基本上重新创建本地会话 EJB 方法。

同样,下面是要考虑的 EJB 组件的一些功能:

  1. 采用 J2EE 上下文可以简化服务签名。
  2. 服务质量对编程人员透明。
  3. 需要部署工具来生成部署类。
  4. 需要运行时平台进行实现。
  5. 可以实现 J2EE 框架的轻量级 UT/FVT 版本,这几乎与 FactoryFinder 委托模式一样容易。

考虑到我是 EJB 倡导者,您可以想像得到我倾向于使用不采用各个委托类的 J2EE 组件。现在正是企业 Java 编程人员“揭开”这些组件面具的时候——因此,在要直接公开 EJB 组件时,下面这几条经验值得考虑:

  1. 如果存在与调用功能的用户紧密关联的自然“网关”对象,并且如果存在对底层数据模型支持的关系且该模型提供对此功能所需的所有数据的访问权,则请考虑在利用 CMR 的实体上将该服务作为自定义 Home 方法公开。简而言之,Home 方法即等同于 session facade。
  2. 如果只从没有“持久性”关系的服务参数中的数据中读取和更新任意数据,则请考虑将服务作为会话 Bean 方法公开。
  3. 但在第二种情况下,请考虑查找可以使用第一种情况的相关数据的集群,以利于重用。

接着,我们将“自顶向下”研究一些应用程序体系结构,并应用我们过去几个月来讨论的内容。我们还希望将此文章进行整理,单独写一篇有关代码生成工具的文章,使得使用 EJB 进行自顶向下开发真正手到擒拿。

请始终牢记,在任何情况下,EJB 倡导者决不会说“从不”或“始终”之类的词。



参考资料



关于作者

作者照片

Geoff Hambrick 来自 Texas 的 Round Rock(在 Austin 附近),是 IBM Software Services for WebSphere Enablement Team 的首席顾问。Enablement Team 通常通过深层技术简报及短期概念验证为售前流程提供支持。Geoff 于 2004 年 3 月被选为 IBM 的杰出工程师 (Distinguished Engineer),他的工作是创建并传播用于开发 IBM WebSphere Application Server 上的 J2EE 应用程序的最佳实践。




对本文的评价










回页首


IBM 公司保留在 developerWorks 网站上发表的内容的著作权。未经IBM公司或原始作者的书面明确许可,请勿转载。如果您希望转载,请通过 提交转载请求表单 联系我们的编辑团队。
    关于 IBM 隐私条约 联系 IBM 使用条款