Wednesday, January 31, 2007

中国服务器网

中国服务器网

关于P4安装ORACLE解决方法

Monday, January 29, 2007

Sunday, January 21, 2007

使用Spring进行JMS消息传递

我碰巧看到了一篇文章,是有关使用Spring框架来简化与IBM WebSphere MQ的交互的。这篇文章是对Spring中的JMS支持的相当不错的介绍,但是有一些重要的东西它却没有提到。

  Spring作为J2EE框架的地位,与最近BEA宣布在WebLogic中对Spring提供正式支持这则消息结合起来,就会使一些开发人员认为,文章中的代码可以不加修改地用于在WebLogic上运行的J2EE应用程序(很有可能,大部分人会选择使用WebLogic startup类而不是基于文件的JDNI来映射MQ ConnectionFactory和队列到WebLogic JNDI命名空间中,或通过支持Foreign JMS提供者来映射)。

  那么,把文章中的代码用于在WebLogic上运行的J2EE应用程序又会产生什么结果呢?如预料中的一样,它确实运行了——消息被发送和交付,没有出现异常,所以乍一看一切都很正常。直到您将其用于CMT或BMT事务,比如说下面来自一个会话bean的代码:
代码:

/**
* @ejb.bean
* type="Stateless"
* name="SpringTest"
* view-type="local"
* transaction-type="Container"
*
* @ejb.transaction
* type="RequiresNew"
*/
public class SpringTestBean implements SessionBean {
...
/** @ejb.interface-method */
public void sendMessage() throws Exception {
JmsSender jmsSender =
(JmsSender)springContext.getBean("jmsSender");
jmsSender.sendMesage("test");
// rollback CMT transaction
sessionContext.setRollbackOnly();
}
...
}


本来事务回滚之后,消息应该不在MQ中,但是实际情况是消息在MQ中。其原因非常简单——WebLogic需要使用特定的包装器来包装MQ ConnectionFactory对象,以确保在XA事务上下文中获取正确的资源。仅仅将对象放入WebLogic JNDI是不够的。开发人员应该通过EJB部署描述符中的resource-ref元素声明ConnectionFactory:
代码:


myQcf
javax.jms.QueueConnectionFactory
Container
Shareable

...

myQcf
mq.qcf

然后,在Spring上下文定义中,QueueConnectionFactory应该引用通过resource-ref映射的名字:






然后,在Spring上下文定义中,QueueConnectionFactory应该引用通过resource-ref映射的名字:
代码:



myQcf
javax.jms.QueueConnectionFactory
Container
Shareable

...

myQcf
mq.qcf

然后,在Spring上下文定义中,QueueConnectionFactory应该引用通过resource-ref映射的名字:






注意,该工厂是在java:comp/env命名空间中,而不是在JNDI全局作用域中进行查找。这将确保WebLogic所使用的将要参与全局事务的ConnectionFactory对象经过正确包装,然后上面的例子就会如预料那样运行了。

  虽然上面的例子正常运行了,但是还是有一些问题。因为现在所有使用Spring的JMS对象的操作都应该由某个正确定义了resource- ref的EJB 发起。这意味着,例如,开发人员要非常小心,不要把JMSSender作为依赖注入到某个可以不作为EJB调用序列(例如,调度程序之类)的一部分而执行的类或需要访问多个ConnectionFactory的类中。另一种方法是扩展Spring的 JndiObjectFactoryBean类,支持创建所要求的包装器。这种方法的问题是,(据我所知)该包装器API还没有说明文档。

  所以,最后结论是,最好不要假设Spring会“魔法”,然后就期待一切发生,还是要经常测试,确保它真的 管用。

评论

* 确实,没有魔法:即使在一个基于Spring的应用程序中,资源引用也需要得到正确定义,就像普通的J2EE应用程序与这些资源交互时一样。
resource-ref元素也可以在web.xml中声明,所以同样的功能也完全适用于普通的web应用程序(WAR部署单元),而不是只适用于 EJB。这是Spring应用程序直接与应用服务器的服务连接的最典型的场景:使用Spring驱动的事务,用 JtaTransactionManager作为后端,JndiObjectFactoryBean定义指向resource-ref JNDI位置(一个JDBC DataSource或一个JMS ConnectionFactory)。
发表人:juergen.hoeller,2005年10月20日,01:59 PM

* 是的,也可以在WAR中使用resource-ref,而且还要更好一些,因为可以以全局级别指定resource-ref,而对于EJB,如果我没有弄错的话,是针对每个bean。
如果定义了多个ConnectionFactory,一定要小心。因为常见的实践是在不同EJB的同一个“逻辑”名下定义它们,而且它们可以调用同一个共享的组件。
发表人:maximdim,2005年10月20日,02:33 PM


http://dev2dev.bea.com/blog/maximdim/archive/2005/10/jms_messaging_w.html

作者简介
Dmitri Maximovich是一位独立顾问,专长是软件设计、开发和技术培训。他具有超过12年的从业经验,而且很早就开始涉及J2EE。他的工作包括为金融业和医药业设计和开发任务关键的应用程序。

MySQL and WSAD 5

I had to figure this out yesterday and documented it in a blog entry at http://www.mischiefbox.com/blog/index.php?p=70 (where I also have some JSP scriptlet code for testing the data source).

The steps I followed are below:

# Create the Test Environment server.
From the Server perspective, create a new Test Environment server. I called mine TestEnv.

# Open the Server preferences.
These are hiding. Again, from the Server perspective, in the Servers folder, there is a TestEnv.wsi server configuration file. Double-click that to bring up the Server Preferences property page.

# Add the DataSource.
On the Data Sources tab (look on the bottom of the preferences page), you can add the MySQL data source. Add a new Server JDBC Provider by pressing the Add button. I called mine MySQL, with a description giving the MySQL version, and the implementation class com.mysql.jdbc.jdbc2.optional.MysqlConnectionPoolDataSource. If you configure the data source using the traditional JDBC Driver class, you will get a one-phase commit error message.

# Add the Data Source.
With the MySQL database selected, add a new Data Source. The name should be descriptive. The JNDI name should be something similar to jdbc/dsn, like any other DataSource JNDI name. The Data source helper class name is com.ibm.websphere.rsadapter.GenericDataStoreHelper. I left the rest of the values at their default settings. Press Finish when you’re done.

# Add the JDBC URL resource property.
With the new data source highlighted, add a single resource property. The name should be url, the type java.lang.String, and the value jdbc:mysql://localhost/dbi?user=user&password=password&autoReload=true. Alternatively, you could add the user, password, and other settings as appropriate to the driver (as documented) as new resource properties, but I chose not to do so.

# Add the JAAS authentication setting.
Go to the Security tab. Add a new JAAS Authentication Entry. For an alias, I chose the JNDI name. The user ID was the same as in the JDBC URL, as was the password. For a description, I chose the prosaic, “To make the database work.” You’re done working with the server configuration, so save it and close the property editor.

# Add the external resource to the web application.

1. Switch to the Web perspective.
2. Find your web.xml file (by default, under WebContent/WEB-INF) and double click to bring up the property page.
3. Select the References tab (again, at the bottom), and then the Resource Environment sub-tab (at the top).
4. Press the Add button to add a new resource.
5. Click on the new resource name to edit it, and change this to the JNDI name you assigned to the data source above.
6. Change the type to javax.sql.DataSource (press Browse… and type this in).
7. Set the WebSphere Bindings JNDI Name to the JNDI name assigned to the data source.
8. Save and close the web.xml property editor.

# (Re)start the test environment.
In the Server perspective, right click on the server name (in the lower left corner) and select Restart on the context menu. This will force the server data source changes to be loaded.

# (Re)deploy your web application.
Deploy your web application (right click on the EAR and select “Run on Server…"). If you have a good test, you should be able to verify that your data source is working.

Saturday, January 20, 2007

在Spring中集成Hibernate事务管理

本文试图解释如何使用Spring来集成组件(包括组件的事务关系)。在J2EE应用程 序中,连接到单个存储数据没有什么困难。但是一旦要求集成企业级组件的时候,情况就复杂了。一个组件一般会受到一个或多个存储数据的支持,因此当我们提到 集成一个或多个组件的时候,我们就认为需要跨越多个组件、维护多个数据存储中的原子操作。J2EE服务器为这些组件提供了一个容器,这个容器可以管理这些 事务性原子操作和跨组件的隔离。如果我们没有使用J2EE服务器,Spring可以帮助我们。Spring在集成组件服务和它们相关的事务关系的时候,是 基于控制倒置(Inversion of Control)的。

  集成(Assembling)组件事务

  假设在我们的企业组件库中,已经拥有了一个
审计
(audit) 组件,客户端可以调用它的服务方法。后来,当我们希望建立一个订单处理系统的时候,我们发现了一个设计需求:OrderListManager组件服务也 需要审计组件服务。OrderListManager建立和管理订单,因此所有的OrderListManager服务都有自己的事务属性。当我们在 OrderListManager服务内部调用审计组件的时候,会把OrderListManager服务的事务关系(context)传递到审计服务 中。也许在未来某个时候,某个新的业务服务组件也需要审计组件服务,但是该审计服务将会在一个完全不同的事务关系中被调用。其实际结果是,虽然审计组件的 功能仍然没有变化,但是它可以与其它的业务服务功能组合使用,使用混合和匹配(mix-and-match)的事务属性来提供不同的运行时(run time)事务行为。

  图1显示了两个相互独立的调用关系流。在流1中,如果客户端拥有TX关系,OrderListManager要 么参与它,要么启动一个新的TX,这依赖于Client是否在某个TX中,以及为OrderListManager方法提供了什么样的TX属性。 OrderListManager服务接下来调用AuditManager方法的时候,这样的解释也是正确的。
在Spring中集成Hibernate事务管理
图1:集成组件事务

   EJB架构通过允许组件集成器宣告式地(declaratively)提供正确的事务属性来实现这种灵活性。我们没有研究宣告式事务管理的替代物(称为 编程式事务控制),因为它涉及到改变代码来影响不同的运行时事务行为。几乎所有的J2EE应用程序服务器都按照X/Open XA规范提供了分布式的事务管理器来适应两步提交(Two-Phase Commit)协议。现在的问题是,在EJB服务器之外,我们能利用相同的功能吗?Spring就是一个替代解决方案。让我们来看看Spring是如何帮 助我们解决事务集成问题的。

  使用Spring进行事务管理

  我们将看到一个轻 量级的事务架构,它能够管理组件级的事务集成。Spring就是一个解决方案,它的优势在于它没有像JNDI 数据源那样嵌入到J2EE容器服务中。还有一点值得注意,如果我们希望把这个轻量级的事务架构插入到已有的J2EE容器中,也没有任何困难。看起来它在两 者之间的平衡性方面做得很好。

  另一方面,Spring 轻量级事务架构还使用了面向方面编程(AOP)框架。Spring AOP框架组件使用了激活了AOP的Spring bean工厂。通过在组件服务层(在一个Spring特定的配置文件applicationContext.xml中)指定事务特性,就把各种事务划分开 来了。

<beans>

<!-其它一些代码... -->

<bean id="orderListManager" class="org.springframework.transaction
.interceptor.TransactionProxyFactoryBean">
 <property name="transactionManager">
  <ref local="transactionManager1"/>
 </property>
 <property name="target">
  <ref local="orderListManagerTarget"/>
 </property>
 <property name="transactionAttributes">
  <props>
   <prop key="getAllOrderList">
    PROPAGATION_REQUIRED
   </prop>
  <prop key="getOrderList">
   PROPAGATION_REQUIRED
  </prop>
  <prop key="createOrderList">
   PROPAGATION_REQUIRED
  </prop>
  <prop key="addLineItem">
   PROPAGATION_REQUIRED,
   -com.example.exception.FacadeException
  </prop>
  <prop key="getAllLineItems">
   PROPAGATION_REQUIRED,readOnly
  </prop>
  <prop key="queryNumberOfLineItems">
   PROPAGATION_REQUIRED,readOnly
  </prop>
 </props>
</property>
</bean>

</beans>

  一旦我们在服务层指定了事务属性,它们(即事务属性)就可以被org.springframework.transaction.PlatformTransactionManager接口的具体实现所截取和解释。该接口如下所示:

public interface PlatformTransactionManager{
 TransactionStatus getTransaction(TransactionDefinition definition);
 void commit(TransactionStatus status);
 void rollback(TransactionStatus status);
}

  Hibernate事务管理器

  由于我们已经决定把Hibernate作为ORM工具,我们必须编写一个Hibernate特定的事务管理器实现,我们下一步就做这个工作。

<beans>

<!-- other code goes here... -->

<bean id="transactionManager1" class="org.springframework.orm.hibernate. HibernateTransactionManager">
<property name="sessionFactory">
<ref local="sessionFactory1"/>
</property>
</bean>

</beans>


设计:管理多个组件中的事务

  现在我们讨论一下"集成组件事务"到底是什么意思。你可能注意到了,提供给OrderListManager(它是域中的一个服务层组件)的TX属性有所不同。图2显示了业务域对象模型(BDOM)中能够识别出来的主要对象:

在Spring中集成Hibernate事务管理(2)
图2:业务域对象模型(BDOM)


  为了演示目的,我们列举了域中对象的一些非功能性的需求(NFR):

  · 业务对象必须保存在数据库中appfuse1。
· 审计需要记录到一个独立的数据库appfuse2中,为了安全,它必须放在防火墙后面。
  · 业务组件应该能重复使用。
  · 必须审计业务服务层的每次尝试的所有活动。

  考虑到上面一些需求,我们决定OrderListManager服务将代理任何对已有的AuditManager组件的审计日志调用。这就形成了图3所示的详细设计:

在Spring中集成Hibernate事务管理(2)
图3:组件服务的设计

   这儿要重点注意的是,由于NFR(非功能性需求)的约束,我们把与OrderListManager(订单管理)相关的对象映射到了appfuse1数 据库,把与Audit(审计)相关的对象映射到了appfuse2。接着,为了达到审计的目的,OrderListManager组件调用 AuditManager组件。我们都认为OrderListManager组件中的方法是事务性的,因为我们使用这个服务来建立订单和条目。那么 AuditManager组件中的服务是什么样的呢?由于我们认为AuditManager组件中的服务执行审计跟踪,因此我们对尽可能保持审计轨迹(即 跟踪记录)很感兴趣,要保留系统中任何可能的业务行为的轨迹。这会引起另一种需求--即使主业务活动失败了,我们也需要建立一个审计条目。 AuditManager组件也需要自己的事务,因为它也需要与自己的数据库交互操作。如下所示:

<beans>

<!-- other code goes here... -->
<bean id="auditManager"
class="org.springframework.transaction.
interceptor.TransactionProxyFactoryBean">
 <property name="transactionManager">
  <ref local="transactionManager2"/>
 </property>
 <property name="target">
  <ref local="auditManagerTarget"/>
 </property>
 <property name="transactionAttributes">
  <props>
   <prop key="log">
    PROPAGATION_REQUIRES_NEW
   </prop>
  </props>
 </property>
</bean>

</beans>

   现在我们把精力集中在两个业务服务上,也就是createOrderList和addLineItem。请注意,我们并没有采用最好的设计策略--你可 能注意到了addLineItem方法抛出FacadeException异常,但是createOrderList却没有。在产品环境中,你可能希望每 个服务方法都必须处理异常情况。

public class OrderListManagerImpl
implements OrderListManager{

 private AuditManager auditManager;

 public Long createOrderList(OrderList orderList){
  Long orderId = orderListDAO.createOrderList(orderList);
  auditManager.log(new AuditObject(ORDER + orderId, CREATE));

  return orderId;
 }

 public void addLineItem (Long orderId, LineItem lineItem)
 throws FacadeException{
  Long lineItemId = orderListDAO.addLineItem(orderId, lineItem);
  auditManager.log(new AuditObject(LINE_ITEM + lineItemId, CREATE));

  int numberOfLineItems = orderListDAO.
  queryNumberOfLineItems(orderId);
  if(numberOfLineItems > 2){
   log("Added LineItem " + lineItemId + " to Order " + orderId + "But rolling back *** !");
   throw new FacadeException("Make a new Order for this line item");
  }
  else{
   log("Added LineItem " + lineItemId + " to Order " + orderId + ".");
  }
 }

 //其它代码...
}

   为了演示的目的,我们建立了一个异常处理模块,引入了另外一条业务规则:一个订单不能包含两个以上订单项。我们现在应该注意到在 createOrderList和addLineItem中对auditManager.log()方法的调用。你应该已经注意到为上面的方法提供的事务 属性了。

<bean id="orderListManager" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
 <property name="transactionAttributes">
  <props>
   <prop key="createOrderList">
    PROPAGATION_REQUIRED
   </prop>
   <prop key="addLineItem">
    PROPAGATION_REQUIRED,-com.
    example.exception.FacadeException
   </prop>
  </props>
 </property>
</bean>

<bean id="auditManager" class="org.
springframework.transaction.interceptor.
TransactionProxyFactoryBean">
 <property name="transactionAttributes">
  <props>
   <prop key="log">
    PROPAGATION_REQUIRES_NEW
   </prop>
  </props>
 </property>
</bean>

   PROPAGATION_REQUIRED等同于EJB中的TX_REQUIRED,PROPAGATION_REQUIRES_NEW等同于EJB中 的TX_REQUIRES_NEW。如果我们希望服务方法一直在事务中运行,就可以使用PROPAGATION_REQUIRED。我们使用 PROPAGATION_REQUIRED的时候,如果某个TX已经在运行中,那么bean方法加入那个TX,否则Spring轻量级TX管理器将为你重 新启动一个。如果我们希望在组件服务被调用的时候,一般情况下启动新事务,那么就可以使用PROPAGATION_REQUIRES_NEW属性了。

   我们还说明了,如果addLineItem方法产生FacadeException类型的异常,它就应该回滚(roll back)事务。这是另外一种粒度(granularity)层次,通过它我们可能细微地控制TX如何终止(即在碰到异常的情况下如何终止)。前缀-符号 表明回滚TX,+符号表明提交TX。

  下一个问题是,为什么我们给log方法赋予PROPAGATION_REQUIRES_NEW? 这是我们的需求所驱动的:无论主服务方法发生了什么情况,我们都必须把系统中每次建立和添加订单项的尝试轨迹记录在审计中。这意味着,即使我们在 createOrderList和addLineItem实现的内部遇到了任何异常情况,我们也得记录审计轨迹。只有我们启动新的TX并在新TX关系中调 用log方法的情况下,这才是可行的。这就是为什么给log赋予了PROPAGATION_REQUIRES_NEW TX属性:如果我们调用auditManager.log(new AuditObject(LINE_ITEM + lineItemId, CREATE));

  成功了,auditManager.log()就在新的TX上下文关系中发生,如果auditManager.log()自身是成功的(也就是没有产生异常),它就会被提交。

建立示例环境

  为了建立示例环境,我遵循了参考书Spring Live的步骤:

  1.下载和安装下面的组件。在操作的时候,要注意准确的版本号,否则可能遇到版本不匹配的问题。

  ·JDK 1_5_0_01 以上版本

  ·Apache Tomcat 5.5.7

  ·Apache Ant 1.6.2

  ·Equinox 1.2

  2.在系统中设置下面一些环境变量:

  ·
JAVA
_HOME

  ·CATALINA_HOME

  ·ANT_HOME

  3.给PATH环境变量添加下面一些内容,或者使用完整路径执行脚本:

  ·JAVA_HOME\bin

  ·CATALINA_HOME\bin

  ·ANT_HOME\bin

  4.为了设置Tomcat,在文本编辑器中打开$CATALINA_HOME/conf/tomcat-users.xml文件,检查下面的行是否存在。如果不存在,就加上:

<role rolename="manager"/>
<user username="admin" password="admin" roles="manager"/>

   5.建立一个基于Struts、Spring和Hibernate的web应用程序,我们把Equinox作为初始应用程序框架--它拥有预定义的文件 夹结构、所有需要的.jar文件,以及Ant建立脚本。把Equinox解压到一个文件夹中,这个过程会建立一个equinox文件夹。把当前目录切换到 equinox文件夹,输入ANT_HOME\bin\ant new -Dapp.name=myusers命令。它会在equinox的同一层次建立一个叫做myusers的文件夹。它的内容如下所示:

在Spring中集成Hibernate事务管理(3)
图4:Equinox myusers应用程序文件夹模板

  6.删除myusers\web\WEB-INF文件夹中所有的.xml文件。

  7.把equinox\extras\struts\web\WEB-INF\lib\struts*.jar文件复制到myusers\web\WEB-INF\lib文件夹中,这样示例应用程序也支持Struts了。

  8.把示例代码中的myusersextra.zip解压到某个目录中。把当前目录切换到新建立的myusersextra文件夹。复制myusersextra文件夹中的所有内容,粘贴(或覆盖)到myusers文件夹中。

   9.打开命令行提示符并把当前目录切换到myusers文件夹。执行CATALINA_HOME\bin\startup。从myusers文件夹中启 动Tomcat是很重要的,否则数据库会被建立在myusers文件夹的外面,会导致我们在执行build.xml中定义的某些事务的时候出错。

   10.打开第二个命令提示符,把当前目录切换到myusers。执行ANT_HOME\bin\ant install。这个过程会建立应用程序并把它部署到Tomcat中。执行操作的时候,我们可能注意到在myusers中建立了一个db目录,用于存放数 据库appfuse1和appfuse2。

  11.打开浏览器,验证myusers应用程序是否部署在http://localhost:8080/myusers/了。

   12.如果需要重新安装应用程序,需要执行ANT_HOME\bin\ant remove,接着通过执行CATALINA_HOME\bin\shutdown来关闭Tomcat。接下来删除CATALINA_HOME\ webapps文件夹中任意的myusers文件夹。接着通过执行CATALINA_HOME\bin\startup重新启动Tomcat,通过执行 ANT_HOME\bin\ant install安装应用程序。

运行示例

  为了运行测试示例,我们在myusers\test\com\example\service中提供了一个JUnit测试类OrderListManagerTest。要执行它,请在我们建立应用程序的命令行上执行下面的命令:

CATALINA_HOME\bin\ant test -Dtestcase=OrderListManager

  测试案例被分成了两个主要的部分:第一部分建立了一个包含两个订单项的订单,接着把两个订单项链接到该订单。如下所示,它会成功地运行:

OrderList orderList1 = new OrderList();
Long orderId1 = orderListManager.createOrderList(orderList1);
log("Created OrderList with id ’" + orderId1 + "’...");
orderListManager.addLineItem(orderId1,lineItem1);
orderListManager.addLineItem(orderId1,lineItem2);

  下一部分执行了类似的操作,但是这次我们试图给订单添加三个订单项,会出现一个异常:

OrderList orderList2 = new OrderList();
Long orderId2 = orderListManager.createOrderList(orderList2);
log("Created OrderList with id ’" + orderId2 + "’...");
orderListManager.addLineItem(orderId2,lineItem3);
orderListManager.addLineItem(orderId2,lineItem4);
//我们知道此处会产生一个异常
try{
 orderListManager.addLineItem(orderId2,lineItem5);
}
catch(FacadeException facadeException){
 log("ERROR : " + facadeException.getMessage());
}

  控制台打印的输出信息如图5所示:

在Spring中集成Hibernate事务管理(4)
图5:客户端的控制台输出信息

   我们建立了Order1,并添加了订单项ID为1和2的订单项。接着我们建立了Order2,并试图给它添加三个订单项。添加前两个订单项(订单项ID 分别是3和4)是成功的,但是图5显示,当我们试图给Order2添加第三个订单项(ID为5)的时候,业务方法遇到了异常。因此,业务方法TX被回滚 了,订单项ID为5的订单项不会保存到数据库中。这在图6和图7中可以证实,从控制台上执行下面的命令可以看到这两个图:
CATALINA_HOME\bin\ant browse1

在Spring中集成Hibernate事务管理(4)
图6:appfuse1数据库中的订单

在Spring中集成Hibernate事务管理(4)
图7:appfuse1数据库中建立的订单项

下一步,也是最重要的,示例显示订单和订单项保存在appfuse1数据库中,而审计对象保存在appfuse2数据库中。实际上,OrderListManager中的服务方法与多个数据库交互。用下面的命令打开appfuse2数据库可以看到审计轨迹,如图8所示:
CATALINA_HOME\bin\ant browse2

在Spring中集成Hibernate事务管理(5)
图8:appfuse2数据库中建立的审计轨迹,包含了失败的TX条目

   图8中的最后一行需要特别注意。RESOURCE数据列表明"它与LineItem5对应"。但是如果我们回头看图7会发现,没有与LineItem5 对应的订单项。这儿出错了吗?实际上,没有出现任何错误,图8中这额外的一行也是本文全部内容所解释的部分。我们现在讨论发生了什么情况。
我们知 道addLineItem()拥有PROPAGATION_REQUIRED,log()方法拥有PROPAGATION_REQUIRES_NEW。此 外,addLineItem()内部调用了log()方法。因此,当我们试图给Order2添加第三个订单项的时候,就引发了异常(根据业务规则),它将 回滚这个订单项的建立和链接操作。但是,由于log()也是在addLineItem()中调用的,并且由于log()拥有 PROPAGATION_REQUIRES_NEW TX属性,addLineItem()的回滚并不会回滚log(),因为log()在新TX中发生。

   我们现在对log()的TX属性做一些修改。我们不使用PROPAGATION_REQUIRES_NEW,而是把它改变为 PROPAGATION_SUPPORTS。PROPAGATION_SUPPORTS属性允许服务方法在客户端TX中运行(如果该客户端拥有TX上下文 关系),否则该方法会不带TX运行。你可能需要重新安装应用程序,这样数据库中已有的数据就可以被清除了。如果要重新安装,请查看前面部分中的第12步。

   如果我们重新运行,我们将体验到稍微的不同。这次,当我们试图给Order 2添加第三个订单项的时候也碰到了异常。它会回滚(试图添加第三个订单项的)事务。接着这个方法调用了log()方法。但是,由于log()方法的TX属 性为PROPAGATION_SUPPORTS,log()将会在与addLineItem()方法相同的TX上下文关系中被调用。由于 addLineItem()回滚了,log()也回滚了,导致没有回滚TX的审计记录。因此在图9中没有审计轨迹条目与失败的TX对应!
在Spring中集成Hibernate事务管理(5)
图9:appfuse2数据库中的审计轨迹,没有与失败的TX对应的条目

  造成这种不同的事务行为的唯一的修改是我们改变了Spring配置中的TX属性,如下所示:

<bean id="auditManager"
class="org.springframework.transaction.
interceptor.TransactionProxyFactoryBean">
<property name="transactionAttributes">
<props>
<!-- prop key="log">
PROPAGATION_REQUIRES_NEW
</prop -->
<prop key="log">
PROPAGATION_SUPPORTS
</prop>

</props>
</property>
</bean>

  这就是宣告式事务管理的效果,自从EJB开始的时候,我们就讨论它了。但是,我们知道自己需要高端的应用程序服务器来寄宿EJB组件。现在我们知道即使没有EJB服务器,使用Spring,我们也可以看到类似的结果。

  总结

   本文为J2EE世界中的两个强者:Spring和Hibernate之间的结合提供了一条光明大道。通过提升两者的能力,我们拥有了用于容器管理的持久 性(CMP)、容器管理的关系(CMR)和宣告式事务管理的替代技术。即使Spring并非设计为替代EJB的,但是它提供的特性,例如无格式java对 象的宣告式事务管理,也使用户在很多项目中可以省去EJB。

  找到EJB的替代物并非本文的目标,但是我们试图找到解决手头问题的最可行的技术方案。因此,我们需要进一步研究Spring和Hibernate这个轻量级组合的能力,这也是读者未来需要探究的主题。





Friday, January 19, 2007

Core J2EE Patterns: Patterns index page

http://java.sun.com/blueprints/corej2eepatterns/Patterns/index.html

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,并且需要了解数据库结构。

Blog Archive