介 绍:
J2EE为应用开发提供了大量的功能,但是为Servlets开发提供的却很少。Servlets开发者可能很难使用J2EE提供的这些功能,既不愿意也没有足够的时间将简单的Servlets容器替换为一个大型的、提供了多余功能的J2EE服务器。无论如 何 ,利用J2EE的模块化特性,有可能通过将一些小型的、实现J2EE规范功能的应用组件集成到Servlet 容器中来增强Web应用,其中一项功能就是事务。
对于J2EE事务的完整描述可以参考其他文章,目前,只需要知道事务就是具有四个特性的、对资源(例如:数据库)的序列操作,事务的特性通常称为ACID:
原子化(Atomicity):事务的操作或者都成功(事务被提交),或者都失败(事务被回滚)。这是全有-全无属性。一个事务应该被看作是工作的一个单独单元。在事务中绝对没有混合了完成和未完成的操作。
一致性(Consistency):一个完成的事务将资源从一种有效状态变为另一种有效状态。对于一致性的具体例子就是数据库中的引用一致性和表中的主键。
隔离性(Isolation):在事务提交之前,事务中对于共享资源的改变在事务之外不具有可见性。独立性保证了事务不会访问并发修改的资源。
持续性(Durability):事务一旦成功完成,各项修改都能从失败中恢复过来。
JOTM(Java Open Transaction Manager)是一个由ObjectWeb组织开发的全功能的、开放源代码的单独事务管理器,它为Java应用提供了事务支持,并且完全兼容JTA(Java Transaction API)。你可以从JOTM的主页找到更多细节。将JTOM集成到Tomca(或者其它的Servlet容器)使得JSP和Servlet开发者可以用一种轻量级的方法用事务的优势创建更强壮的Web应用。
为了说明事务可以怎样增强Web应用,让我们来考虑一个经典的、使用浏览器与客户交互的ATM实例。
ATM实例:
场景:
用例非常简单:一个用户想要从ATM取钱。他提供他的名字,john_doe,和他要取的现金的数额,50元。如果在他银行帐户上有足够的钱,并且ATM有足够的现金,那么应用程序会提供给他现金并从他的银行帐户上扣除相应的金额。否则,操作会取消,除了返回错误信息之外,任 何 事情都不会发生。为了集中于事务处理并让事情变得简单,我们不需要关心有关安全问题,假设用户已经通过安全认证了。
这个非常简单的例子如果要实现强壮性二没有事务支持就会变得出奇的困难。一个客户端应用包括了对两个不同资源的操作:ATM和用户的银行帐户。在应用设计中自然就提出了ACIDity的问题,举例来说,如果ATM的操作成功而银行帐户的操作失败(可能由于通信故障),用户将会取到现金而银行帐户不会被更新,对银行来说是个糟糕的消息。
更不幸的是,如果银行帐户更新正确而ATM提供现金时出现错误,那么用户取不到现金,但是他的银行帐户会扣除相应的数额。
为了防止上述事情发生,在你的应用程序中,你可以:
1) 连接两个资源并通知它们用户目前正在进行的所有操作;
2) 询问它们是否可以进行这些操作,并且…;
3) 如果它们都认同,则要求它们执行操作。
如果另一个操作在第二、三部之间从用户的帐户上提钱,那么即使以上的解决方案仍然不够强壮,可能会发生现金提取错误,比如,如果用户的银行帐户不允许出现负数的情况。
这里就是事务可以让你的应用更简单和强壮的地方了,通过将所有对两种资源的操作封装在同一个事务中,可以为你解决ACIDity的问题。
应用程序设计:
数据层:
在数据层,我们需要两个不同的数据库,在每个数据库中都存在一张表。为了让例子更加真实,我们会使用两个不同的数据库,因为从不属于用户帐户所在银行的ATM上取钱是确实可能出现的情况。(根据以下信息配置数据库)
banktest包括account表,表示用户帐户信息。
atmtest包括atm表,表示ATM信息。
逻辑层:
在逻辑层,我们有三个类访问资源并在资源上进行各种操作:
foo.BankAccount表示指定用户的银行帐户并通过JDBC对数据库中的account表进行操作。
bar.ATM表示ATM并通过JDBC对atm表进行操作。
bar.CashDelivery使用上面两个类完成用户取钱的操作。
所有的逻辑都在CashDelivery.java类的deliverCash方法中完成。
javax.transaction.UserTransaction
接口被用来指定是否使用事务,所有在utx.begin()和utx.commit()(或者utx.rollback())之间的操作都会在一个事务中完成。这就保证了你的Web应用不会陷入上面的场景中所讨论的痛苦之中。
感谢事务,让应用的逻辑变得如此简单,仅仅包括如下几步:
1)
开始事务;
2)
与用户的银行联系,并从帐户上支取相应的数额;
3)
告诉ATM提供现金;
4)
结束事务
a)
如果所有的事都成功完成,那么提交事务;
b)
否则,回滚事务
5)
将事务的结果向用户报告。如果事务成功完成,那么用户得到现金,并且帐户上会扣除相应的数额。否则,什么事都不会发生。
示例一
:
CashDelivery.java
public boolean deliver(String client, int value) {
InitialContext ctx = new InitialContext();
UserTransaction utx = (UserTransaction)
ctx.lookup("java:comp/UserTransaction");
...
boolean success = false;
try {
//
开始事务
utx.begin();
//
与用户开户银行通信
...
BankAccount account = new BankAccount(client);
// ...
并从帐户上扣除相应的数额
.
account.withdraw(value);
//
联系
ATM...
ATM atm = new ATM();
// ...
提取现金给用户
.
atm.deliverCash(value);
//
所有事情都成功完成
.
success = true;
} catch (Exception e) {
//
有错误发生
//
必须向用户提供信息
explanation += e.getMessage();
} finally {
try {
if (success) {
/*
所有事情都成功完成
,
我们提交事务
.
*
只有在现在
,
用户帐户上才真正扣除相应的数额,
*
并且现金被送到用户手中
*/
utx.commit();
} else {
/*
有错误发生
,
我们回滚事务
.
*
任 何 在事务中进行的操作都不会真正发生
*/
utx.rollback();
}
} catch (Exception e) {
/*
在事务完成过程中有错误发生
*
我们仍然保证在事务中进行的操作不会真正发生
*/
//
我们必须向用户报告有关信息
explanation += "/n" + e.getMessage();
//
最后,事务没有成功
success = false;
} finally {
return success;
}
}
}
表示层
:
在表示层,由两个JSP文件组成:
atm.jsp
:现金支取应用,用于发送用户信息和需要支取的现金数额到
bar.CashDelivery
类,并显示用户操作的结果。
admin.jsp
:管理控制台用于显示和更新与两个资源有关的信息。(并非程序设计的一部分,但是必须要添加以便使资源更新操作变得简单,比如向用户帐户存钱)
图1 应用程序设计
配置数据库:
关于数据库,我们使用MySQL 4.0.12 和附带的JDBC驱动。缺省的,MySql不提供事务支持,为了让其支持事务,在创建表的时候必须声明是InnoDB类型。另外,为了能够提供InnoDB类型支持,你必须将MySQL配置文件(my.cnf)中的#skip-innodb一行注释起来。
在本例中设置MySQL用户为javauser,口令为javaude,需要确认该用户已经被创建并且具有创建数据库的权限。
创建数据库和表的脚本在示例文件的scripts/目录下。运行脚本文件将会创建account表并插入两条用户记录:
john_doe帐户上有100元
jane_doe帐户上有600元。
示例二:创建account表
mysql> CREATE DATABASE banktest;
mysql> USE banktest;
mysql> CREATE TABLE account(
-> client VARCHAR(25) NOT NULL PRIMARY KEY,
-> money INT)
TYPE=InnoDB;
mysql> INSERT INTO account VALUES("john_doe", 100);
mysql> INSERT INTO account VALUES("jane_doe", 600);
mysql> SELECT * FROM account;
+----------+-------+
| client | money |
+----------+-------+
| john_doe | 100 |
| jane_doe | 600 |
+----------+-------+
脚本文件同样会创建ATM表,并提供可以支取500元现金的记录:
示例三:创建atm表
mysql> CREATE DATABASE atmtest;
mysql> USE atmtest;
mysql> CREATE TABLE atm(
-> id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
-> cash INT) TYPE=InnoDB;
mysql> INSERT INTO atm VALUES(null, 500);
mysql> SELECT * FROM atm;
+----+------+
| id | cash |
+----+------+
| 1 | 500 |
+----+------+
最后,你需要拷贝JDBC驱动到$Tomcat安装目录$/shared/lib目录下。
获取并安装Tomcat
本文基于Tomcat 4.1.18 及以上版本编写,确认你没有使用以前的版本,安装Tomcat并没有任 何 特殊之处,只需要下载并解压缩就可以了。
获取并安装JOTM
为了要使用JOTM,你必须从最新的二进制发行版下载并解压缩,从lib目录拷贝*.jar文件(除了log4j.jar、common-cli.jar和jotm_iiop_stubs.jar之外)到$Tomcat安装目录$/shared/lib目录下,一切都完成了。
配置Tomcat
你现在需要配置Tomcat以便使其能够从JNDI中获取UserTransaction和DataSource对象(用于foo.BankAccount和bar.ATM中)。
首先,告诉Tomcat在你的Web应用中会用什么JNDI名字寻找数据源。这项工作在web.xml文件中完成,下面列出了web.xml文件的内容。对于银行帐户数据源,JNDI名字为java:comp/env/jdbc/bankAccount
,但是你只需要给出在
java:comp/env/
之后的名字。
Tomcat
会使用
JNDI
机制解析余下部分的内容。对于
ATM
数据源也同样处理。
示例四
:
web.xml
<web-app>
<resource-env-ref>
<description>Bank Account DataSource</description>
<resource-env-ref-name>jdbc/bankAccount</resource-env-ref-name>
<resource-env-ref-type>javax.sql.DataSource</resource-env-ref-type>
</resource-env-ref>
<resource-env-ref>
<description>ATM DataSource</description>
<resource-env-ref-name>jdbc/ATM</resource-env-ref-name>
<resource-env-ref-type>javax.sql.DataSource</resource-env-ref-type>
</resource-env-ref>
</web-app>
现在你需要告诉Tomcat如 何 检索定义在web.xml中的资源,这些工作在bank.xml文件中完成,下面列出了bank.xml文件的内容。对于银行帐户和ATM资源,必须设置正确的参数以便Tomcat将数据源与你的Web应用联系起来。更详细的描述可以在Tomcat JNDI How-to文档中找到。
有一个参数非常特别:factory。使用该参数设置的类会在Web应用查找JNDI时创建一个数据源。另一个在web.xml中描述的重要资源是UserTransaction。这项资源被java:comp/UserTransaction
用来区分是否使用事务支持。该项资源的实现是
JOTM
提供的。
示例五:bank.xml
<Context path="/bank" docBase="bank.war" debug="0" reloadable="true" crossContext="true">
<!-- Description of the DataSource "jdbc/bankAccount" -->
<Resource name="
jdbc/bankAccount" auth="Container" type="javax.sql.DataSource" />
<ResourceParams name="jdbc/bankAccount">
<parameter>
<!-- Factory of the DataSource -->
<name>factory</name>
<value>org.objectweb.jndi.DataSourceFactory</value>
</parameter>
<parameter>
<name>url</name>
<value>jdbc:mysql://localhost/banktest</value>
</parameter>
<!-- other parameters include:
o username - name of database user
o password - password of the database user
o driverClassName - JDBC Driver name
-->
...
</ResourceParams>
<!-- Description of the DataSource "jdbc/ATM" -->
<Resource name="
jdbc/ATM" auth="Container" type="javax.sql.DataSource" />
<!-- same type of parameters than for resource "jdbc/bankAccount" -->
<ResourceParams name="jdbc/ATM">
...
</ResourceParams>
<!-- Description of the resource "UserTransaction -->
<Resource name="
UserTransaction" auth="Container" type="javax.transaction.UserTransaction" />
<ResourceParams name="UserTransaction">
<parameter>
<name>factory</name>
<value>org.objectweb.jotm.UserTransactionFactory</value>
</parameter>
<parameter>
<name>jotm.timeout</name>
<value>60</value>
</parameter>
</ResourceParams>
</Context>
发布Web应用
一旦你安装了JOTM和Tomcat,发布和调用Web应用就非常简单了。首先,下载bank.tgz文件并解压缩。拷贝bank.xml和bank.war到$Tomcat安装目录$/webapps目录下,然后,启动Tomcat:
> cd $CATALINA_HOME/bin
> ./catalina.sh run
Using CATALINA_BASE: /home/jmesnil/lib/tomcat
Using CATALINA_HOME: /home/jmesnil/lib/tomcat
Using CATALINA_TMPDIR: /home/jmesnil/lib/tomcat/temp
Using JAVA_HOME: /usr/local/java
May 6, 2003
5:56:00 PM org.apache.commons.modeler.Registry loadRegistry
INFO: Loading registry information
May 6, 2003
5:56:00 PM org.apache.commons.modeler.Registry getRegistry
INFO: Creating new Registry instance
May 6, 2003
5:56:00 PM org.apache.commons.modeler.Registry getServer
INFO: Creating MBeanServer
May 6, 2003
5:56:07 PM org.apache.coyote.http11.Http11Protocol init
INFO: Initializing Coyote HTTP/1.1 on port 8080
Starting service Tomcat-Standalone
Apache Tomcat/
4.1.24
-LE-jdk14
你可以从日志中注意到JOTM并没有启动,只有在你首次访问DataSource的时候它才会启动,那时候,你会得到如下信息:
May 6, 2003 5:56:20 PM org.objectweb.jotm.Jotm <init>
INFO: JOTM started with a local transaction factory that is not bound.
May 6, 2003 5:56:20 PM org.objectweb.jotm.Jotm <init>
INFO: CAROL initialization
打开浏览器指向http://localhost:8080/bank/来使用Web应用。
使用Web应用
该Web应用的首页包括两个链接:
1) 到现金提取页面,在那里你可以模拟从ATM上提取现金;
2) 到管理控制台页面,在那里你可以检查或更新与ATM和你所创建的银行帐户有关的信息。
在操作之前,ATM中有500元,John Doe的银行帐户有100元,Jane Doe的银行帐户有600元。
如果John Doe想要支取400元,事务会失败,因为在他的银行帐户上没有足够的钱可以支取。结果会像下面这样:
Client ID: john_doe, value: $400
Cash can not be delivered to you
because: not enough money in your account (only $100).
如果Jane Doe想要支取550元,事务也会失败,因为在ATM机中没有足够的现金。结果会像下面这样:
Client ID:
jane_doe, value: $
550
Cash can not be delivered to you
because: not enough cash available from this ATM (only
$500).
如果John Doe想要支取50元,事务会成功完成。结果如下所示:
Client ID:
john_doe, value: $
50
Please take your cash ($50)
Thank you!
总 结
这个简单的例子演示了Servlets可以通过使用事务达到强壮性和简单性,保证在任 何 情况下的正确操作。Tomcat和JOTM的结合给Servlets提供了一种轻量级的方法得到事务的优越性。
除了例子中演示的功能之外,JOTM还提供了更多的功能。JOTM提供如下功能帮助你增强你的Web应用:
完整的分布式事务支持:如果你的应用程序各层(数据、业务、表示层)在不同的JVM中运行,可以使用全局的事务跨越多个JVM。事务的内容可以通过RMI/JRMP或者RMI/IIOP来传播。
与JDBC集成:示例中使用XAPool,一种XA兼容的JDBC连接池,与数据库进行交互。XAPool与Jakarta的DBCP项目很相似,在上面添加了XA兼容性,这是将JTA事务与
JDBC集成的必须方法。
与JMS集成:JOTM可以与JORM集成,JORM是ObjectWeb组织开发的一个JMS服务实现,通过集成提供事务支持的JMS消息。你可以将JMS消息发送和数据库更新发生在同一个事务中。
Web Service事务:JOTM提供了BTP(Business Transaction Protocol)的实现,JOTM-BTP,可以为你的Web Service添加事务行为。
以上功能的文档和示例可以在JOTM的发行版本和它的网站上找到。