|
亲爱的 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 倡导者
|