 | 级别: 初级 Geoff Hambrick (ghambric@us.ibm.com), 杰出工程师, IBM
2005 年 4 月 01 日 使用设计不好的 EJB™ 组件可能导致在系统测试或者(更糟的情况)正式生产运行时产生严重的性能问题。EJB 倡导者说明了如何设计方法签名来最小化层之间的"隔阂",并最大程度的利用您的 EJB。
在每个专栏中,EJB 倡导者提出要点,客户和开发者采用独特的前后衔接的对话框方式,在对某一感兴趣的设计问题推荐解决方案的过程中进行交流。其中忽略了任何确定性的细节,并且不提供“革新的”或专有的体系架构。要了解更多信息,参见 EJB 倡导者简介。
问题
|
亲爱的 EJB 倡导者,
我的第一个 J2EE 项目是构建一个 Web 应用程序,在我们公司的产品目录中描述我们的产品,并且顾客可以将产品添加到购物车中进行购买。我是对象技术和实体 EJB 的强烈支持者。换句话说,一直到我开始在我的笔记本上使用 IBM® WebSphere® Studio Application Developer 的测试环境来测试我的系统时,我都是这样认为的。长话短说,我的一个 servlet 在加载一个使用实体 EJB 的页面来显示时花费了超过 5 秒钟的时间。当我重新编写这个 servlet 来使用 JDBC 时,所花的时间小于 1 秒。
我不愿意说这些,署名:
No Longer a Fan
|
实体 EJB 架构是有缺陷的,这一点现在非常明显,因为良好设计的 CMP 实体 EJB 同手写的 JDBC 组件之间的一一对比并没有显示这个级别上的不同。然而,在我的回复中,首先还是需要指出他们正集中在早期测试上。我的想法是通过指出他们可以尝试的额外的测试,该团队将可以指出他们自己存在的问题。
|
亲爱的 No Longer,
很高兴看到您正在使用本地而不是远程实体 EJB,因此您没有在远程方法调用上花费更多的性能。这个开支很容易使加载页面上所需要的时间加倍。使用实体 EJB 的一个最佳实践就是一直使用本地调用。在上个月的专栏中我们简要的讨论了这个方法。
同样,我也非常高兴得听到您很快构建了一个原型系统来测试本地实体 EJB 的性能,并与直接编写的 JDBC 来作为持久层的性能进行比较。许多团队直到他们已经构建完应用程序的时候都还没有进行架构性能的测试,最后导致大量的重写工作 -- 产品中可能有严重的问题。也有一些人会试着将持久机制隐藏在一个"access bean"的后面,使以后更容易切换,这样就像系统中加入了一个新层(JDBC 和实体 EJB 都是这个持久机制的抽象)
我猜我知道问题出在哪,但是我希望您打开对 JDBC 数据源的跟踪来查看一下到底执行了多少 SQL 语句(参阅 WebSphere Application Server Information Center)。同样您也可以打开 EJB 跟踪,就像 Matt Oberlin 在文章专家访谈中描述的那样,在 WebSphere Studio Application Developer(图 1)中实现起来是相当容易的。
图 1. 在 WebSphere Studio Application Developer 中打开 EJB 跟踪
更进一步,正如 Stacy Joines 在她的 book on WebSphere performance 文章中指出的那样,搜集准确的性能测量方法对于发现和修复瓶颈是非常重要的。我让您更加精确的捕获这些的原因是,在实体 bean 情况下,您会看到比直接的 JDBC 情况更多的 SQL 语句,这也就导致了性能上的异同。事实上,我预计您将看到为了从每个产品实体中读取每个属性,至少都多了一个 SQL 语句!
告诉我您发现了什么。
那么就 OK 了
EJB 倡导者
|
好的,坏的,难看的
|
亲爱的 EJB 倡导者,
您如何猜想的?我在每个页面上显示了十个产品,每个产品有五个属性(SKU,描述,价格,图片链接以及产品有效日期)。在末尾,对于实体 EJB 我执行了 51 个 SQL 语句,但是直接的 JDBC 只是执行了一个!很明显实体 EJB 并没有在任何程度上提高性能。看起来如果我选择 JDBC 是一个正确的选择。
签名:
No Longer a Fan
|
我希望 SQL 跟踪数据将使问题的实质变得清晰 -- "No Longer" 将不再依靠不严密的"秒表"测量方法,而是在放弃前试着去发现不同或者是问题的起因。帮助一些人觉醒比我想象得更加困难!这是我的回复:
|
亲爱的 No Longer,
我猜想您正在从整体事务的外部来调用您的实体,这就会导致每个实体的调用都在一个单独的事务中执行,并且导致其自有的 SQL 语句(取决于您的部署选项)。很清楚,发现这需要一个调用来返回下面的十个实体,对于每个实体还需要五个调用执行与其显示的属性相关的 get() 方法。在整体事物的外部调用实体很显然是个最坏实践(通常称为反模式)。事实上,避免在事物外部调用实体是非常重要的,一些 EJB 开发人员建议(比如这个 EJB 倡导者就是)在 EJB 部署描述符中将事务声明为"mandatory":在<container-transaction>标签中使用<trans-attribute>Mandatory</trans-attribute>。如果当实体被访问的时候没有一个已经初始化的事务,这个声明将抛出一个异常。
有两种方法可以封装会在全局事务中调用实体 EJB 的逻辑并能够在很大程度上提高性能。一是一个"简单的方法",另外一个是"正确的方法"。
这个简单的方法是在您的 servlet 中明确的编写代码来围绕着 EJB 的调用启动和结束一个全局事务,比如这样:
...
import javax.transaction.*;
...
public class YourServlet implements HttpServlet {
private InitialContext initCtx = new InitialContext();
public void doGet (
HttpServletRequest req, HttpServletResponse
){
UserTransaction userTran =
(UserTransaction)initCtx.lookup(
"java:comp/UserTransaction"
);
userTran.begin();
//Use entity to load data
...
userTran.commit();
}
...
}
|
有些团队会更进一步,创建一个 servlet 超类,使用一个 template inheritance 的技术来处理这一行为。这个超类将被声明为抽象的。他的 doGet() 方法将被声明为 final,并且将向下调用 YourServlet (继承这个行为)实现的抽象的 doGetYourParent() 方法。父类的代码看起来如下所示:
...
import javax.transaction.*;
...
public abstract class YourParentServlet implements HttpServlet {
private InitialContext initCtx = new InitialContext();
public final void doGet (
HttpServletRequest req, HttpServletResponse
){
UserTransaction userTran =
(UserTransaction)initCtx.lookup(
"java:comp/UserTransaction"
);
userTran.begin();
doGetYourParent();
userTran.commit();
}
protected abstract void doGetYourParent(
HttpServletRequest req, HttpServletResponse
);
...
}
|
为了实现这个模板,您的 servlet 子类所需要的更改是非常简单的:
- 更改 YourServlet 类的实现语句,将 HttpServlet 改为 YourParentServlet,以及
- 更改 doGet(),doPost() 和其他名称的 HttpServlet 方法为 <Method>YourParent()。
模板继承方法的一个好处是使始终的和透明的提高服务质量变得更加容易,像事务启动和停止,缓存校验,错误处理和您的团队想要包含用来保证稳健性的其他方法。
不管您用何种方法启动全局事务,都应该注意到跟踪到的 SQL 语句直线下降(取决于访问意图和其他部署选项;请引用 WebSphere Application Server Information Center,查看关于如何将属性比如 Collection Increment 设置为您想要读取的数字 -- 您的情况下为 10)。
但是即使您做了这些更改,并且消除了所有在全局事务外部调用 CMP 的地方,负载分析工具(在生产环境下测量系统的性能),比如 IBM Rational® Performance Tester,依旧会显示 JDBC 和实体 EJB 代码在吞吐量和 CPU 利用方面的重要不同点,即使概要工具比如 JInsight 和路径分析工具比如 IBM Tivoli® Monitoring for Transaction Performance 没有显示出异同。
修复代码的"正确方法"依靠您设计的细节。您可能已经非常接近了,因此我来问您一个问题:您是否正在使用一个 JavaServer™ Page 来表示来自于由 servlet 加载(J2EE 最佳实践)的 "data transfer object"页面(仅仅有 get/set 方法的 POJO)?或者这个 servlet 是否直接表示返回的 HTML?
OK,
EJB 倡导者
|

 |

|
将难看的东西隐藏在一个定制的 facade 后面
|
亲爱的 EJB 倡导者,
您是对的,我使用一个 JSP 来表示 Model 2 方法后面的页面。换句话说,该 servlet 加载 10 个"ProductView"对象(与您的数据传输对象是一样的,只不过它是序列化的),并且接下来调用 JSP。为了更清晰的表述,下面是该 servlet 有关的代码,用您上一个回复中简易方式来编写:
...
import javax.transaction.*;
...
public class YourServlet implements HttpServlet {
private InitialContext initCtx = new InitialContext();
public void doGet (
HttpServletRequest req, HttpServletResponse
){
UserTransaction userTran =
(UserTransaction)initCtx.lookup(
"java:comp/UserTransaction"
);
userTran.begin();
ProductLocalHome home =
(ProductLocalHome)initCtx.lookup(
"java:comp/env/ProductLocal"
);
Collection products = home.findNextNFrom(last, count);
int size = 0;
ProductView[] results = new ProductView[products.size];
Iterator i = products.iterator();
ProductView product = null;
while (i.hasNext()) {
ProductLocal product = (ProductLocal)i.next();
ProductView result = new ProductView();
// Each of these used to cause a tran and SQL
result.setSku(product.getSku());
result.setDesc(product.getDesc());
result.setPrice(product.getPrice());
result.setImage(product.getImage());
result.setData(product.getDate());
results[size++] = result;
}
// Set into the HttpServletRequest
req.setAttribute("ProductView", results);
// Invoke the ProductView JSP (not interesting here)
...
userTran.commit();
}
...
} |
顺便说一句,该 JSP 使用一个自定义标签来操纵 ProductView 对象数组(事实上这个标签模拟"bean tag",可以操纵任何对象的数组),其中的"bean property tags"可以用来代替属性。我希望这已经足够详细。
我很高兴得发现这个"简单方法"代码事实上使实体 EJB 的性能同 JDBC 已经非常接近(通过 JInsight 来测量)。我同样使用了"mandatory" CMT 属性,该属性检验了它在事务外部维护了所有到 CMP 的调用,然而,通过使用我们的负载测试工具,发现使用 JDBC 仍然更有优势(我们现在使用 LoadRunner,但是将看一下您提到的 Rational Performance Tester)。
谢谢,但是我仍旧署名:
No Longer a Fan
|
这时,我已经获得了比我希望得更多的信息。No Longer 提供了代码示例,比仅仅使用描述或者图更加精确。并且,看起来他们接受了使用更加精确的系统性能测量方法,比如加载和路径分析工具的提示。
如果 No Longer 已经在其中混合了 HTML 表示代码(视图)来获取数据,我将更深入地研究一下 servlet 最佳实践(不是延伸,因为我同样是 J2EE 倡导者)。我不得不解释使用数据传输对象来在 servlet 和 JSP 之间传输数据的最佳经验。在我的回复中包含的代码将看起来同 No Longer 的十分相似。
|
亲爱的 No Longer,
谢谢您的代码示例。比起其他形式我更喜欢这种,因为代码是关系到性能"出现偏差"的地方。在查看设计是否遵循最佳实践方面,没有什么能打败静态分析。通过加载和路径分析,您可以获取一个如何发现和修复瓶颈的相当完整的描述。
非常高兴听到您正在遵循 Model 2 最佳实践,并且更进一步来为数组提供自定义操作标签。同样使用数据传输对象也是非常好的,基本上同服务数据对象类似。有了这个架构,可以使"正确的方法"获得一个全局事务,也是"最简单的方法"。
换句话说,您可以创建一个会话虚包(session facade)EJB 来封装与为页面搜集数据相关的逻辑(ProductView 对象数组)。这个模式在 上个月的专栏 讨论过,同样在 Kyle Brown 的书中(参阅参考资料)。该会话虚包看起来就象这样:
extends SessionBean {
public ProductView[] getCatalog (
ProductKey lastKey, int count
){
ProductLocalHome home =
(ProductLocalHome)initCtx.lookup(
"java:comp/env/ProductLocal"
);
// This call used to cause a trans and SQL
Collection products = home.findNextNFrom(last, count);
int size = 0;
ProductView[] results = new ProductView[products.size];
Iterator i = products.iterator();
ProductView product = null;
while (i.hasNext()) {
ProductLocal product = (ProductLocal)i.next();
ProductView result = new ProductView();
// Each of these used to cause a tran and SQL
result.setSku(product.getSku());
result.setDesc(product.getDesc());
result.setPrice(product.getPrice());
result.setImage(product.getImage());
result.setData(product.getDate());
results[size++] = result;
}
// Instead of setting into the HttpServletRequest
return results;
}
...
}
|
正如您在上面那个例子中可能注意到的那样,与数据传输对象一起使用 Model 2 架构的好处是,您的 servlet doGet() 方法中的大部分逻辑都移动到了会话虚包 getCatalog() 方法中。这种移动的一个好处是获取产品下一页的逻辑现在在 servlet 上下文的外面也是可用的(就象来自于一个消息驱动 bean,或者另外一个 EJB)。同样也会提供一个远程接口(由 WebSphere Studio Application Developer 中的工具自动生成),使其对于一个 J2EE 客户端可用。数据传输对象的使用最小化了不同层之间的间隔 == 仅仅需要一个无状态调用。无论如何,该 servlet 不再需要处理事务。看起来就像:
...
public class YourServlet implements HttpServlet {
private InitialContext initCtx = new InitialContext();
public void doGet (
HttpServletRequest req, HttpServletResponse
){
MySessionLocalHome home =
(MySessionLocalHome)initCtx.lookup(
"java:comp/env/MySession"
);
// Get the last key field as before
ProductView[] home.create().getProductView(last, 10);
//Load array into HttpServletRequest and invoke JSP
...
}
...
} |
我知道看起来好像这个最佳实践将启动和停止事务的代码替换为调用 EJB 方法的代码,但是除了在其他情况下重用逻辑之外,还有一些其他的好处。首先,本地会话引用可以被缓存在 servlet init() 方法中来消除在 doGet() 中的查找。第二,最重要的是,处理事务可能是非常复杂的,尤其是涉及到异常的时候。操作不当可能导致"漏洞",导致他们自己种类的性能问题。简而言之,另外一个最佳实践是尽可能的使用容器管理的事务。
无论如何,这个“正确方法”代码本质上将同“简单方法”代码(使用了本地会话接口)运行的一样快。但是仍然存在下面这个问题:使用本地实体的端到端性能将仍旧比不上使用 JDBC(现在在会话 EJB 后面,其有效的封装了来自于视图的模型)。原因是即使实体上的 get<Property>() 方法是本地的,仍然需要许多开销来检查安全和事务。对这些开销做一个评估,大约有 10000 个指令,在这个例子中通过路径分析工具测量将会是:50 x 10000 = 500000 。但是如果上面的"数量"是 100 或者访问的属性数量是 100,将会是什么情况。指令的总数为 10 亿,其已经是一个可测量的差异了。与"测量"相关联的这个现象是为什么负载测试是发现真实性能差异的最好方法的原因。路径测试工具让您发现最可能的故障位置,根据这个故障位置您可以跟踪静态分析(代码检查)。在这个案例中,它在访问数据传送对象的一个属性时所花的指令估计为 10s,不是数千个 10s -- 其数量级比访问一个全局事务下的本地 EJB 要更好。
获得 EJB 最佳性能的关键是,使用数据传输对象和自定义的方法在一个调用里为给定的用例创建、获取和设置所有需要的属性。这就最小化了会化虚包和实体 EJB 之间的隔阂。通常,您可以使用正确的方法集来设计 EJB,这样用户在查询后只需要做一次调用 -- 创建、获取、更新或者删除方法也是一样的。下面的代码图示了使用自定义 get 方法的实体 EJB:
public abstract ProductBean extends EntityBean {
// Here are the properties needed for the custom method
// NOW NO LONGER ON INTERFACE TO PREVENT INDIVIDUAL ACCESS
public abstract ProductKey getSku() ;
public abstract void setSku(ProductKey value);
public abstract String getDesc() ;
public abstract void setDesc(String value);
public abstract BigDecimal getPrice() ;
public abstract void setPrice(BigDecimal value);
public abstract String getImage() ;
public abstract void setImage(String value);
public abstract Date getDate () ;
public abstract void setDate(Date value);
// Probably more properties than these above
// Here is the custom get method&
public ProductView getProductView (
){
ProductView result = new ProductView();
// Now cannot cause a tran and SQL
result.setSku(getSku());
result.setDesc(getDesc());
result.setPrice(getPrice());
result.setImage(getImage());
result.setDate(getDate());
return result;
}
...
} |
为了加强自定义方法的使用,许多 EJB 设计者仅仅在接口上公开自定义方法,比如:
public interface Product extends EJBLocalObject {
public ProductView getProductView ();
public void setProductView(ProductView value);
} |
The home would have the custom creates and finds:
public interface ProductHome extends EJBLocalHome {
public Product create(ProductView value);
public Collection findNextNFrom(ProductKey last, int count);
} |
您可能已经注意到我们有使用"Local"作为实体 EJB 接口名的一部分(home 和 bean 也是一样)。因为我从没有公开到实体 EJB 的远程接口,它看起来过度的加长了类名。
无论如何,会化虚包将作如下的改变来遵循这些自定义的方法:
public class YourSession extends SessionBean {
public ProductView[] getCatalog (
ProductKey lastKey, int count
){
ProductHome home =
(ProductHome)initCtx.lookup(
"java:comp/env/ProductLocal"
);
// This call used to cause a trans and SQL
Collection products = home.findNextNFrom(last, count);
int size = 0;
ProductView[] results = new ProductView[products.size];
Iterator i = products.iterator();
ProductView product = null;
while (i.hasNext()) {
// Treat a next on an iterator of entities like a find
Product product = (Product)i.next();
// Now only one call after find, which is the ideal
results[size++] = product.getProductView();
}
// Instead of setting into the HttpServletRequest
return results;
}
...
} |
请注意我们只是把会话 EJB getCatalog() 循环中的代码移到了实体 EJB getProductView() 方法中;接下来我们将这段代码用一个方法调用代替。如果您加载使用这种方法的测试代码,您将发现使用实体 EJB 比 JDBC 多了另一个合理的花销。它使实体 EJB 比 JDBC 更加容易维护,所以这个交易是值得的。
并且为了后面的引用,这个 UML 图显示了通用的端到端架构。它说明了 JSP 和 servlet,会话和实体 EJB,以及 key 和视图数据转换对象之间的联系:
图 2. 说明了端到端架构的 UML 图
我希望这个讨论可以帮助您恢复对 EJB 的热爱。至少,我希望您能有更多的新工具和技术来精确的测量性能差异和评估权衡。
那么 OK,
您的 EJB 倡导者
|

 |

|
结束语
通过这个交流,我们看到为什么在本地实体 EJB 前使用会话虚包是如此的重要,其保障对于每个 UI 实践仅有一个事务,最小化到数据库的 SQL 调用。更有意义的是,我们看到了在实体 EJB 上使用数据传输对象(现在称为 SDO)和自定义方法的重要性,其最小化了不同层之间的隔阂,即便在使用本地接口的时候也是这样。并且,顺便说一下,我们看到使用 SDO 使您能至始至终把数据从实体传送到表现 HTML 的 JavaServer Page,中间穿过了会话虚包和 servlet(各自为模型和视图控制器)。
我们讨论了如何使用模板继承来向 servlet 代码透明的添加行为(比如启动和提交全局事务)。虽然使用会话虚包最小化了这种方法的需求,但在 doGet() 和 doPost() 的上下文中调用了多个会话 EJB 时,在 servlet 中的模板继承仍然是十分有用的。
我们还讨论了在实体 EJB 上强制声明事务,以及不向实体接口公开单个的 CMP 属性。这两个最佳实践都能增强您的使用策略。
我们提到的一个流程(而不是设计)最佳实践是关于使用加载和路径分析工具来测量性能,接下来分析代码和配置来发现和修复瓶颈。这个建议是使用诸如 Rational Performance Tester、用于事务性能的 Tivoli Monitoring 以及 JInsight 等工具来捕获调用的数量,来回传送的数据量以及每个调用所花费的时间。同样,我还提示了一个最佳实践,静态分析应该基于代码,而不是类或者高级的序列图(虽然这些对于获取整体概况来说比较有用)
接下来是什么....
如果您有使用任何类型 EJB 的有趣的问题,请随时联系 EJB 倡导者。另外,在下一个专栏中,我们将研究服务数据对象,以及 EJB(实体和会话)将如何参与到面向服务体系结构中。
参考资料
- 您可以参阅本文在 developerWorks 全球站点上的 英文原文。
-
EJB 倡导者:正确进行 EJB 交叉引用
-
Java 2 Platform, Enterprise Edition (J2EE) 规范
-
使用 IBM WebSphere, Second Edition 进行企业级 Java 编程, 作者:Kyle Brown, Gary Craig, Greg Hester, Russell Stinehour, W. David Pitt, Mark Weitzel, JimAmsden, Peter M. Jakab, Daniel Berg. 由 Martin Fowler 作序.
-
Java Web 站点的性能分析, 作者:Stacy Joines, Ruth Willenborg 和 Ken Hygh.
-
IBM Rational 性能测试
-
IBM Tivoli 事务性能监控
-
WebSphere Application Server 信息中心
-
专家访谈: Matt Oberlin 谈 WebSphere Studio Application Developer
- 在开发者书店购买折价的 WebSphere 书籍
关于作者  | 
|  |
Geoff Hambrick 是 IBM Software Services for WebSphere Enablement Team 的一个高级顾问,住在 Round Rock,Texas(Austin 附近)。Enablement Team 通常通过深入的技术简报和短期的概念验证销售支持,来支持售前流程。为了表彰其在创立和宣传开发在 WebSphere Application Server 上运行的 J2EE 的最佳实践,在 2004 年 3 月份,Geoff 被评为 IBM 杰出工程师。 |
对本文的评价
|  |