本文来自:http://hi.baidu.com/fuqilee/blog/item/8027adee6537ff202df534c1.html
在很多企业应用中有时需要在特定的时间运行一段代码,比如银行需要在晚上系统相对空闲的时间内进行日结的对帐,到了规定时间系统需要触发对帐服务,运行对帐程序,通过WebSphere Application Server和EJB定时器服务能解决这个问题。
1、 概述
在很多企业应用中有时需要在特定的时间运行一段代码,比如银行需要在晚上系统相对空闲的时间内进行日结的对帐,那么到了规定时间系统需要触发对帐服 务运行对帐程序,现在EJB定时器服务能解决这个问题,它是一个可靠的、事务性的、提供容器的服务,允许Bean提供者注册定时反馈的企业Beans,它 可以在特定时刻发生,或在某段时间之后发生,或以一定时间间隔重复发生。由于这个服务是可靠的,容器破坏的时候定时依然有效,企业Beans的激活与失 效、装载与保存周期都由定时器注册。定时器服务由EJB容器实现,定时器服务可以通过EJBContext接口新增的getTimerService() 方法来访问,它返回实现TimerService接口的对象:这个接口允许创建不同的定时器来支持在不同时间、不同时间间隔、不同时间周期时发生的定时反 馈。使用定时器服务的企业Beans的Bean类必须实现javax.ejb.TimedObject接口。在EJB2.1中,只有无序的会话Beans 和实体Beans可以注册为定时器服务。这个功能在以后的规范中可能会扩展到其它类型的Bean。
定时器服务适合长时间的业务处理模型,但并不适合用于实时的事件模型。
在 WebSphere Application Server 6中,EJB 定时服务将 EJB 计时器作为新的调度程序服务任务实施。缺省情况下,内部调度程序实例用于管理那些任务,定时任务存放在与服务器进程关联的 Cloudscape 数据库中。在集群环境中,任务必须存放在企业关系型数据库中。下面我们以DB2为例讲述怎样在集群环境中配置定时服务。
|
|
2、 创建用于定时服务的数据库
每个定时服务程序都需要一个数据库,以用于存储它的持久信息。数据库及其位置应当由应用程序开发者和服务器管理员决定。 定时服务程序使用这个数据库来存储任务,然后运行这些任务。定时服务程序性能极大地依赖于数据库的性能。如果需要每秒执行更多任务,您可以在更大型的系统 中运行定时服务程序守护程序,或通过使用多个定时服务程序对任务或分区使用的会话 bean 使用集群。但是,定时服务程序数据库最终会达到饱和状态,此时您就需要一个更大型或更优异的数据库系统。要获取有关调度程序拓扑的详细信息,请参阅技术资 料"WebSphere Enterprise Scheduler planning and administration guide"。
当您在每个定时服务程序配置中指定唯一的表前缀值时,多个定时服务程序可以共享一个数据库。这一共享可以降低定时服务程序数据库的管理成本。
TIPS:Oracle XA 数据库的限制,Oracle XA 不允许在全局事务环境中执行所需的模式操作。本地事务是不受支持的。如果您的调度程序使用 Oracle XA 数据源,您可以将调度程序配置临时更改为使用一个非 XA Oracle 数据源,或者使用提供的 DDL 文件手工创建表。如果使用管理控制台为配置为使用 Oracle XA 数据源的调度程序创建或删除调度程序表,您将接收到一条 SchedulerDataStoreException 错误消息并且操作将失败。
下面我们将以DB2为例讲述定时服务:
在机器hostdb上安装DB2后,打开 DB2 命令行窗口。
确保您拥有数据库系统的管理员权限,验证此数据库确实支持 Unicode(UTF-8)。 否则,此数据库无法存储 Java 代码中可以处理的所有字符,当客户机使用了不兼容的代码页时,这将导致代码页转换问题。要避免死锁,请确保将 DB2 隔离级别设置为"读稳定性"。如果需要,请输入命令 :
db2set DB2_RR_TO_RS=YES |
然后重新启动 DB2 实例以激活这一更改。在 DB2 命令行处理程序中输入以下命令使用示例名 timerdb 创建数据库:
db2 CREATE DATABASE scheddb USING CODESET UTF-8 TERRITORY en-us |
即可创建名为 timerdb 的 DB2 数据库。
现已为定时服务创建了 DB2 数据库。
|
|
3、 创建定时服务的表空间和表
在WAS6的安装目录下,有一个名为scheduler的目录。下面包含WAS容器用来管理定时服务的各种数据库SQL定义。对应于DB2的SQL 定义文件名为createSchemaDB2.ddl和createTablespaceDB2.ddl,修改这两个文件选择你所要新建的表空间名和你所 要的模式名称。这两个文件大致内容如下:
createTablespaceDB2.ddl CREATE TABLESPACE @SCHED_TABLESPACE@ MANAGED BY SYSTEM USING ( '@location@\@SCHED_TABLESPACE@' ); |
可以修改表空间名称,这个文件也可不做修改。然后修改createSchemaDB2.ddl 修改后的的结果可以去掉原来的模式名称,那么新建用户表的时候将使用缺省连接数据库的用户的模式名。
CREATE TABLE "TASK" ("TASKID" BIGINT NOT NULL , "VERSION" VARCHAR(5) NOT NULL , "ROW_VERSION" INTEGER NOT NULL , "TASKTYPE" INTEGER NOT NULL , "TASKSUSPENDED" SMALLINT NOT NULL , "CANCELLED" SMALLINT NOT NULL , "NEXTFIRETIME" BIGINT NOT NULL , "STARTBYINTERVAL" VARCHAR(254) , "STARTBYTIME" BIGINT , "VALIDFROMTIME" BIGINT , "VALIDTOTIME" BIGINT , "REPEATINTERVAL" VARCHAR(254) , "MAXREPEATS" INTEGER NOT NULL , "REPEATSLEFT" INTEGER NOT NULL , "TASKINFO" BLOB(102400) LOGGED NOT COMPACT , "NAME" VARCHAR(254) NOT NULL , "AUTOPURGE" INTEGER NOT NULL , "FAILUREACTION" INTEGER , "MAXATTEMPTS" INTEGER , "QOS" INTEGER , "PARTITIONID" INTEGER , "OWNERTOKEN" VARCHAR(200) NOT NULL , "CREATETIME" BIGINT NOT NULL ) IN "@SCHED_TABLESPACE@"; |
这两个文件修改完成后,在命令行运行db2cmd转到db2命令窗口。
然后运行db2batch -d timerdb -f createTablespaceDB2.ddl
和db2batch -d timerdb -f createSchemaDB2.ddl生成定时服务所需要的表空间和表。
运行完成后用下列命令验证:
Db2 connect to timerdb Db2 list tables |
你将会看到有以下四个表被创建:
Table/View Schema Type ------------ ------------- ----- LMGR ADMIN T LMPR ADMIN T TASK ADMIN T TREG ADMIN T |
其中主表task存放了定时程序的相关信息。
|
|
4、 创建新的集群
分别在hosta,hostb,hostc上完成WAS6安装后,我们需要创建3个节点来组成一个新的群集。
1)在hosta上创建一个Network DeployManagement节点,启动概要表创建向导:
选择创建Deployment Manager概要表:
点下一步直至完成
。2)分别在hostc和hostb两个节点上选择创建应用服务器概要表。
3)创建完成后在DeployManager概要上运行startManager.sh启动Network Manager。
4)启动完成后打开概要下的日志文件SystemOut.log查看soap端口,缺省为8879。
5)在hostb和hostc两个应用服务器节点上运行addNode.sh hosta 8879
6)运行完成后,打开ND管理控制台:http://hosta:9060/ibm/console
7)在服务器下新建一个群集timertest,创建两个成员为clus01,clus02。启动群集。
|
|
5、 创建定时服务的数据源
进入ND管理控制台,展开资源,点击JDBC 提供者,选择要新建的资源所在的服务器
点新建。按提示输入所需资料。点数据源,进入数据源页面。
新建一个名为testtimer的数据源,指定jndi名为jdbc/testtimer
测试连接通过后。做下一步设置。
|
|
6、 修改服务器设置
打开管理控制台。
单击服务器 >应用程序服务器 > 服务器名 > EJB 容器设置 > EJB 定时服务设置。 出现"定时服务设置"面板。
如果您要使用内部或预配置的调度程序实例,则单击使用内部 EJB 定时服务调度程序实例单选按钮。
如果您选择不更改缺省的设置,则此实例与 Cloudscape 数据库相关联。
更改数据源选择输入您所选的数据源别名。选择前面创建的jdbc/testtimer数据源。
输入表前缀为你创建表时的用户缺省模式名称,必须注意的是,在模式名称后面必须要带上一个小数点.。具体对应的每个值的意思可以点击帮助页面查看。
|
|
7、 开发基于J2EE标准的定时服务企业bean
下面的例子是在RAD环境下开发,要实现定时服务,EJB必须要实现javax.ejb.TimedObject接口。
EJB里面需要涉及到的接口或类分别是:
要实现的TimedObject和TimerService,Timer。
在EJB中的ejbContext增加了一个方法.getTimerService();用于获得TimerService类。但这个方法不能在 setEntityContext()、setSessionContext()、setMessageContext()方法中调用。
下面列出了三个接口的定义:
public interface TimedObject{ public void ejbTimeout(Timer timer); } public interface TimerService{ public Timer createTimer(Date expiration,Serializable info); public Timer createTimer(long duration,Serialzable info); public Timer createTimer(Date initalExpiration,long intervalDuration,Serializable info); public Timer createTimer(long initalDuration,long intevalDuration,Serializable info); public java.util.Collection getTimers(); } public interface Timer{ public void cancel(); public java.io.Serializable getInfo(); public Date getNextTimeout(); public long getTimeRemaining(); public TimerHandle getHandle();//这是一个local对象,不能传到remote client端使用 } |
对于Stateless SessionBean来说,不要在ejbCreate()和ejbRemove()中设置Timer。主要是因为ejbCreate和ejbRemove调用的时间和次数都因Container Vendor而异。可能导致错误设置Timer。
对MessageDriven Bean 而言,和Stateless SessionBean的情况基本相似。但是设置Timer应该在onMessage()里面。通过一个JMS来进行触发。
ejbTimedout是一个回调方法,执行具体的商业逻辑,那么怎样设置什么时间触发这个方法呢,我们利用javax.ejb.TimerSevice。该对象我们可以从EJBContext中获得该对象实例。
当定时器创建的特定时间到达后,容器就会触发ejbTimeout(),运行ejbTimeOut方法提,Bean在终止之前通过调用定时器 Cancel方法取消定时器,它是定时器接口的一部分,如果定时器被取消,ejbTimeout()方法就不会被调用了。定时器接口的 getHandle()方法返回一个序列化的handle对象。接下来,这个持续的Handle能够"非序列化",通过调用getTimer()方法得到 定时器。由于定时器是本地对象,TimerHandle不必通过Bean的远程接口或Web Services接口来传递。
具体步骤如下:
新建一个EJB项目otherTimer,在这个EJB项目里新建一个otherTimer的会话Bean。
在Bean实体里面需要实现两个方法:
startTimer()和ejbTimeOut。
在startTimer()方法里面,我们通过EJBCONTEXT取得一个TimerService然后创建一个Timer。这个timer将在 2005,9月19日晚上8点过5分触发,触发后,EJB容器会调用ejbTimeOut()方法运行具体的商业逻辑,并且这个Timer会在80000 毫秒后再次触发。
javax.ejb.TimerService ts=this.getSessionContext().getTimerService(); System.out.println("启动一个时钟!"); Timer timer = ts.createTimer(new Date(105,9,19,20,5,0),80000,"other timer"); |
|
|
8、 WAS Scheduler实现
基于WAS Scheduler实现定时服务,需要配置一个scheduler,在WAS管理控制台展开资源,点scheduler,新建一个scheduler指定 名称、JNDI名、数据源JNDI名(这里可以用前面设置的jndi/testtimer)和表前缀,跟前面设置服务器EJB定时服务容器设置类似。
设置完成后就可以使用scheduler来实现定时服务了。
在EJB模块中创建一个无状态会话bean,该 bean 实现了 com.ibm.websphere.scheduler.TaskHandler 远程接口中的 process() 方法。将您要创建的业务逻辑放入 process() 方法中。当运行任务时,将调用 process() 方法。Home 和 Remote 接口在部署描述符 bean 中必须设置如下:
com.ibm.websphere.scheduler.TaskHandlerHome com.ibm.websphere.scheduler.TaskHandler |
通过使用以下示例工厂方法创建 BeanTaskInfo 接口的一个实例。 使用 JSP 文件、servlet 或 EJB 组件创建实例,如以下代码示例所示。此代码必须与先前创建的 TaskHandler EJB 模块位于同一应用程序中:
Object schedulerObj = initialContext.lookup("java:comp/env/Scheduler"); BeanTaskInfo taskInfo = (BeanTaskInfo) schedulerObj.createTaskInfo(BeanTaskInfo.class) |
注: 创建 BeanTaskInfo 对象并不会将任务添加到持久存储中。它将为必要的数据创建一个占位符。直到调用调度程序中的 create() 方法,才会将任务添加到持久存储中。设置 BeanTaskInfo 对象中的参数。 这些参数定义了调用哪些会话 bean 以及何时调用它们。TaskInfo 接口包含可用于控制任务执行的各种 set() 方法,其中包括运行任务的时间以及运行任务时它执行的操作。
BeanTaskInfo 接口要求使用 setTaskHandler 方法设置 TaskHandler JNDI 名称或 TaskHandlerHome。如果使用 WASScheduler MBean API 来设置任务处理程序,则 JNDI 名称必须是标准的全局 JNDI 名称。
使用 TaskInfo 接口 API 方法设置参数,如以下代码示例所示:
java.util.Date startDate = new java.util.Date(System.currentTimeMillis()+30000); Object reportGenHomeObj = initialContext.lookup("java:comp/env/ejb/ReportGenerator"); TaskHandlerHome reportGenHome = (TaskHandlerHome)PortableRemoteObject.narrow (reportGenHomeObj,TaskHandlerHome.class); taskInfo.setTaskHandler(home); taskInfo.setStartTime(startDate); scheduler.create(taskInfo); |
那么EJB容器将在当前时间的30000毫秒后触发process方法,在taskinfo里面可以设置一些其他schduler的属性,比如运行次数,运行间隔等。
文章里涉及的J2EE标准的定时服务程序在附件的testTimer.ear里面,使用WAS Scheduler的程序在附件的AccountReport.ear里。其中AccountReport.ear是使用WAS自带的Sample程序修改后的程序。