--sunfruit
用Java事件处理机制实现录制回放功能
目前在一些java应用程序的GUI测试工具,可以提供捕获用户操作的能力并在代码被修改之后能够自动回放用户的操作。文章将分析Java的事件 处理模型及其原理,介绍了基于事件源识别的捕获/回放所需要了解的关键技术并给出了两种实现方式。
1、 Java事件介绍
1.1什么是事件
首先我们来回答"什么是事件"这一基本问题。其实事件本身就是一个抽象的概念,他是表现另一对象状态变化的对象。在面向 对象的程序设计中,事件消息是对象间通信的基本方式。在图形用户界面程序中,GUI组件对象根据用户的交互产生各种类型的事件消息,这些 事件消息由应用程序的事件处理代码捕获,在进行相应的处理后驱动消息响应对象做出反应。我们在GUI上进行叫化操作的时候,在点击某个可 响应的对象时如,按钮,菜单,我们都会期待某个事件的发生。其实围绕GUI的所有活动都会发生事件,但Java事件处理机制却可以让您挑选出 您需要处理的事件。事件在Java中和其他对象基本是一样的,但有一点不同的是,事件是由系统自动生成自动传递到适当的事件处理程序。
1.2Java事件处理的演变
当java的开发者开始解决用java创建应用程序这一问题时,他们就认识到java事件模型的必要性。下面对java事件处理的发展做简要的概 括。
在JDK1.0的版本采用用的事件模型,提供了基本的事件处理功能。这是一种包容模型,所有事件都封装在单一的类Event中,所有事件 对象都由单一的方法handleEvent来处理,这些定义都在Component类中。为此,只有Component类的子类才能充当事件处理程序,事件处理传递 到组件层次结构,如果目标组件不能完全处理事件,事件被传递到目标组件的容器。
JDK1.1是编程界的一次革命,修正了前面版本的一些缺陷,同时增加了一些重要的新功能如,RMI、JNI、JDBC、JavaBean。在事件模型 上基本框架完全重写,并从Java1.0模型迁移到委托事件模型,在委托模型中事件源生成事件,然后事件处理委托给另一段代码。
从JDK1.2开始,引入了Swing包事件处理模型功能更强大,更加可定制GUI组件与他们相关联的支持类。在后面的版本基本保持了整个事 件模型,但加入了一些附加事件类和接口。在1.3版本开始引入Rebot类,它能模拟鼠标和键盘事件,并用于自动化测试、自动运行演示、以及 其他要求鼠标和键盘控制的应用程序。
我们把JDK1.0事件处理模型成为java 1.0事件模型,而从jdk1.1后的版本事件处理模型称为Java 2事件处理模型。
2、 Java 2事件处理模型
在Java1.0事件处理模型中事件处理是以如下方法执行的。deliverEvent()用于决定事件的目标,目标是处理事件的组件或容器,此过程 开始于GUI层的最外部而向内运作。当按一个button时,如果检测到是该按钮激发的事件,该按钮会访问它的deliverEvent()方法,这一操作由 系统完成。一旦识别目标组件,正确事件类型发往组件的postEvent()方法,该方法依次把事件送到handleEvent()方法并且等待方法的返回值 。"true"表明事件完全处理,"false"将使postEvent()方法联系目标容器,希望完成事件处理。
下面给一个实例:
import java.applet.*;
import java.awt.*;
public class Button1Applet extends Applet{
public void init(){
add(new Button("Red"));
add(new Button("Blue"));
}
public boolean action(Enent evt,Object whatAction){
if( !( evt.target instanceof Button))return false;
String buttonlabel=(String)whatAction;
if(buttonlabel=="Red")setBackground(Color.red);
if(buttonlabel==" Blue")setBackground(Color.blue);
repaint();
return true;
}
}
|
在Java2处理事件时,没有采用dispatchEvent()-postEvent()-handleEvent()方式,采用了监听器类,每个事件类都有相关联的监听器 接口。事件从事件源到监听者的传递是通过对目标监听者对象的Java方法调用进行的。
对每个明确的事件的发生,都相应地定义一个明确的Java方法。这些方法都集中定义在事件监听者(EventListener)接口中,这个接 口要继承java.util.EventListener。 实现了事件监听者接口中一些或全部方法的类就是事件监听者。 伴随着事件的发生,相应的状态通常都封装在事件状态对象中,该对象必须继承自java.util.EventObject。事件状态对象作为单参传递给应响 应该事件的监听者方法中。 发出某种特定事件的事件源的标识是:遵从规定的设计格式为事件监听者定义注册方法,并接受对指定事件监听者接口实例的引用。 有时,事件监听者不能直接实现事件监听者接口,或者还有其它的额外动作时,就要在一个源与其它一个或多个监听者之间插入一个事件适配 器类的实例,来建立它们之间的联系。
我们来看下面一个简单的实例:
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
public class SimpleExample extends JFrame {
JButton jButton1 = new JButton();
public SimpleExample() {
try {
jbInit();
}
catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
SimpleExample simpleExample = new SimpleExample();
}
private void jbInit() throws Exception {
jButton1.setText("jButton1");
jButton1.addActionListener(new SimpleExample_jButton1_actionAdapter(this));
jButton1.addActionListener(new SimpleExample_jButton1_actionAdapter(this));
this.getContentPane().add(jButton1, BorderLayout.CENTER);
this.setVisible(true);
}
void jButton1_actionPerformed(ActionEvent e) {
System.exit(0);
}
}
class SimpleExample_jButton1_actionAdapter implements java.awt.event.ActionListener {
SimpleExample adaptee;
SimpleExample_jButton1_actionAdapter(SimpleExample adaptee) {
this.adaptee = adaptee;
}
public void actionPerformed(ActionEvent e) {
adaptee.jButton1_actionPerformed(e);
}
}
|
3、 事件捕获与回放
3.1 Java事件生命周期
Java事件和万事一样有其生命周期,会出生也会消亡。下图3.1给出了Java事件生命周期的示意图,
事件最初由事件源产生,事件源可以是GUI组件Java Bean或由生成事件能力的对象,在GUI组件情况下,事件源或者是组件的同位体( 对于Abstract Window Toolkit[awt]GUI组件来说)或组件本身(对于Swing组件来说)。事件生成后放在系统事件队列内部。现在事件处于事件分发线程的控 制下。事件在队列中等待处理,然后事件从事件队列中选出,送到dispatchEvent()方法,dispatchEvent()方法调用processEvent()方法并将 事件的一个引用传递给processEvent()方法。此刻,系统会查看是否有送出事件的位置,如果没有这种事件类型相应的已经注册的监听器,或 者如果没有任何组件受到激活来接收事件类型,事件就被抛弃。当然上图显示的是AWTEvent类的子类的生命周期。dispatchEvent()方法和proc essEvent()方法把AWTEvent作为一个参数。但对,javax.swing.event并不是AWTEvent子类,而是从EventObject直接继承过来,生成这些事件 的对象也会定义fireEvent()方法,此方法将事件送到包含在对象监听器列表内的那种类型的任何监听器。
3.2 Java事件捕获
从上面的分析我们知道,任何事件产生到dispatchEvent()方法分发方法前,所有的事件都是存放在系统事件的队列中,而且所有的事件都 由dispatchEvent()方法来分派。所以只要能重载dispatchEvent()方法就可以获取系统的所有事件,包括用户输入事件。一般来说,系统事件 队列的操作对用户来说是可以控制。它在后台自动完成所要完成的事情,使用EventQueue类可以查看甚至操纵系统事件队列。
Java提供了EventQueue类来访问甚至操纵系统事件队列。EventQueue类中封装了对系统事件队列的各种操作,除dispatchEvent()方法 外,其中最关键的是提供了push()方法,允许用特定的EventQueue来代替当前的EventQueue。只要从EventQueue类中派生一个新类,然后通过p ush()方法用派生类来代替当前的EventQueue类即可。这样,所有的系统事件都会转发到派生EventQueue类。然后,再在派生类中重载dispatch Event()方法就可以截获所有的系统事件,包括用户输入事件。下面一段代码给出一个操纵EventQueue的实例:
import java.awt.*;
import java.awt.event.*;
public class GenerateEventQueue extends Frame implements ActionListener{
Button button1 = new Button();
TextField textField1 = new TextField();
public GenerateEventQueue() {
try {
jbInit();
}
catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
GenerateEventQueue generateEventQueue = new GenerateEventQueue();
}
private void jbInit() throws Exception {
button1.setLabel("button1");
button1.addActionListener(this) ;
textField1.setText("textField1");
this.add(button1, BorderLayout.SOUTH);
this.add(textField1, BorderLayout.CENTER);
EventQueue eq=getToolkit().getSystemEventQueue() ;
eq.postEvent(new ActionEvent(button1,ActionEvent.ACTION_PERFORMED,"test" )) ;
addWindowListener(new WinListener());
setBounds(100,100,300,200);
setVisible(true);
}
public void actionPerformed(ActionEvent e) {
textField1.setText("event is :"+e.getActionCommand()) ;
}
}
class WinListener extends WindowAdapter{
public void windowClosing(WindowEvent we){
System.exit(0) ;
}
}
|
运行结果如下图所示:
在文本域中首先出现的是"event is :test",这是因为首先得到处理的是EventQueue对象发送到系统事件队列上的ActionEvent。
下面的代码简单说明了如何捕获事件:
import java.awt.EventQueue;
import java.awt.*;
import java.util.*;
public class MyQueueEvent extends EventQueue {//定义EventQueue的子类
public MyQueueEvent() {
}
public static void main(String[] args) {
SimpleExample.main(new String[]{null}) ;
MyQueueEvent myQueueEvent1 = new MyQueueEvent();
Toolkit.getDefaultToolkit().getSystemEventQueue().push(myQueueEvent1) ;
}
//在这里重载事件分发的方法
public void dispatchEvent(AWTEvent ae){
if(ae.getSource() instanceof javax.swing.JButton)
System.out.println("My apture:"+((javax.swing.JButton)ae.getSource()).getText()) ;
super.dispatchEvent(ae);
}
|
这个程序可以打印出当前应用的所有的事件,可以将这些事件中选出你需要的事件保存当然你还需要解析该控件的特征。在上面加黑部 分的代码,打印事件源控件的名称。
除此之外,还可以通过实现java.awt.event. AWTEventListener接口实现对事件的捕获。这个侦听器接口可以接收Component or MenuComponent 以及它们的派生类在整个系统范围内所分发的事件,AWTEventListeners只是被动的监控这些事件。如果要监控系统事件,除了要实现接口,还 要用Toolkit的addAWTEventListener方法注册这个侦听器。
下面我们来看一个实例:
import java.awt.AWTEvent;
import java.awt.Frame;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.AWTEventListener;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import java.lang.ref.WeakReference;
public class MyAWTEventListener implements AWTEventListener{
private static MyAWTEventListener s_singleton = null;//保证该类只被初始化一次
public static MyAWTEventListener getInstance(){
if(s_singleton==null){
s_singleton=new MyAWTEventListener();
}
return s_singleton;
}
private MyAWTEventListener(){
//注意下面这行代码,如果没有这行代码,将无法接收到系统分发的事件
// 下面代码在注册时,只请求了接收WINDOW_EVENT_MASK事件
//但实际上,你可以接收其他AWTEvent中定义的事件类型
Toolkit.getDefaultToolkit().addAWTEventListener(this,
AWTEvent.COMPONENT_EVENT_MASK
);
}
/*
这就是接口方法的实现
*/
public void eventDispatched(final AWTEvent theEvent) {
processEvent(theEvent);
}
private static void processEvent(final AWTEvent theEvent) {
System.out.println(theEvent.getSource() ) ;//打印事件源
switch (theEvent.getID()) {
case WindowEvent.WINDOW_OPENED:
//System.out.println(((Frame)theEvent.getSource()).getTitle() ) ;
case WindowEvent.WINDOW_ACTIVATED:
case WindowEvent.WINDOW_DEACTIVATED:
case WindowEvent.WINDOW_CLOSING:
default: break;
}
}
}
|
3.3 Java事件回放
事件的回放其实比较简单了,比如我们现在记录的是frame1下的jButton1点击事件回放。看下面一段简单的程序,只要点一下jButton1, 就在控制台打印一次"click me"的字符串。
import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
public class Frame1 extends JFrame {
private JButton jButton1 = new JButton();
public Frame1() {
try {
jbInit();
}
catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Frame1 frame1 = new Frame1();
frame1.setVisible(true) ;
}
private void jbInit() throws Exception {
jButton1.setText("jButton1");
jButton1.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(ActionEvent e) {
jButton1_actionPerformed(e);
}
});
this.setTitle("Test");
this.getContentPane().add(jButton1, BorderLayout.CENTER);
}
void jButton1_actionPerformed(ActionEvent e) {
System.out.println("click me") ;
}
}
|
下面是回放的程序,在下面的程序中用到了java.awt.Robot类,这个类通常用来在自动化测试或程序演示中模拟系统事件,在某些需要 控制鼠标或键盘的应用程序中这个类也是很有用,这个类主要的目的就是为方便的实现java的GUI自动化测试平台。在事件回放时,我们同样需 要该类来模拟生成系统的事件,完成记录的操作的回放,在下面的代码中,给出了一个简单的例子。
import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
public class TestReplay extends Thread{
public static void main(String[] args) {
try{
//启动要回放的应用程序
Frame1.main(new String[]{null}) ;
//等应用程序启动后延迟3秒再进行回放
Thread.currentThread().sleep(3000) ;
Robot robottest=new Robot();
robottest.waitForIdle();
//根据标题名获取当前应用的主窗体,在本例中为"test"
Frame jframe=getFrame("test");;
//根据给定的窗体和窗体中要find的控件的名称来获取控件的引用
JButton jbtn=getButton(jframe,"jButton1");
//将鼠标移到控件所在的位置
robottest.mouseMove(jbtn.getLocationOnScreen().x+jbtn.getWidth()/2
,jbtn.getLocationOnScreen().y+jbtn.getHeight()/2) ;
//在控件所在位置,生成鼠标点击事件
robottest.mousePress(InputEvent.BUTTON1_MASK ) ;
robottest.mouseRelease(InputEvent.BUTTON1_MASK ) ;
}catch(Exception ee){
ee.printStackTrace() ;
}
}
//获得标题为title的frame
private static Frame getFrame(String title){
Frame[] jframes=(Frame[])JFrame.getFrames();
for(int i=0;i<jframes.length ;i++){
if(jframes[i].getTitle().equalsIgnoreCase(title))return jframes[i];
}
return null;
}
//获取某一个frame下的某个名为jButton1的控件
private static JButton getButton(Frame jf,String text){
/*注意下面这行代码,因为实例比较简单只有ContentPane一个Container类型的控件,
如果在JFrame中有多个Container控件//的话,必须进行递归处理,搜索出所有的控件
*/
Component[] coms=((JFrame)jf).getContentPane().getComponents();
for(int i=0;i<coms.length ;i++){
if(!(coms[i] instanceof JButton))continue;
if(((JButton)coms[i]).getText().equalsIgnoreCase(text))return (JButton)coms[i];
}
return null;
}
public void run(){
}
}
|
该程序运行完,你会发现在控制台同样打印出了:
"click me"的字符串说明事件被正确回放了。
当然还可以通过直接操纵系统事件队列实现输入事件的回放。先通过记录下的窗口/组件名获得对应窗口引用,然后重构鼠标/键盘事件 ,最后将重构的事件直接放入系统事件队列,由分派线程执行后续的事件分派工作。还需要解决关键问题如何能根据窗口名称获得其引用。这 里还是可以通过系统事件队列来实现的,因为Java程序在新建/删除一个容器时都会向系统事件队列发出一个Containerevent事件,其中包含了 对该容器的引用。所以,事件回放器在载入被测测试程序后便监视系统队列,截获所有的Containerevent事件。如果新建容器,便获得新建Con tainer的引用。因为所有的Container都实现了getComponets(),可以返回所有该容器所包含的组件或容器,只需要保存到一个HashMap结构中 ,需要时检索出来就可以了。该过程所用到的知识,其实在上面都有提到而且在实际引用中,既然Robot已经帮我们完成许多事情,也没有必要 自己再去重构一个鼠标或键盘事件了,不过有兴趣的朋友也可以去试试。
4、 结束语
随着我国软件业的发展,软件测试技术作为软件质量保证的重要环节越来越受到重视,而在基于GUI的应用中采用自动化测试工具可以提高 软件测试的有效性和效率,特别在回归测试中可以大大减少人力投入,还可以提高测试脚本的复用。因此,软件自动测试平台开发已经成为软 件测试的一个重要领域。本文介绍了基于Java的GUI应用的自动测试平台开发需要的基本但关键的捕获、回放功能,所有相关系统开发其实都离 不开本文说的方法。