目前在一些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()方法和processEvent()方法把AWTEvent作為一個參數。但對,javax.swing.event并不是AWTEvent子類,而是從EventObject直接繼承過來,生成這些事件的對象也會定義fireEvent()方法,此方法將事件送到包含在對象監聽器列表內的那種類型的任何監聽器。
3.2 Java事件捕獲
從上面的分析我們知道,任何事件產生到dispatchEvent()方法分發方法前,所有的事件都是存放在系統事件的隊列中,而且所有的事件都由dispatchEvent()方法來分派。所以只要能重載dispatchEvent()方法就可以獲取系統的所有事件,包括用戶輸入事件。一般來說,系統事件隊列的操作對用戶來說是可以控制。它在后臺自動完成所要完成的事情,使用EventQueue類可以查看甚至操縱系統事件隊列。
Java提供了EventQueue類來訪問甚至操縱系統事件隊列。EventQueue類中封裝了對系統事件隊列的各種操作,除dispatchEvent()方法外,其中最關鍵的是提供了push()方法,允許用特定的EventQueue來代替當前的EventQueue。只要從EventQueue類中派生一個新類,然后通過push()方法用派生類來代替當前的EventQueue類即可。這樣,所有的系統事件都會轉發到派生EventQueue類。然后,再在派生類中重載dispatchEvent()方法就可以截獲所有的系統事件,包括用戶輸入事件。下面一段代碼給出一個操縱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事件。如果新建容器,便獲得新建Container的引用。因為所有的Container都實現了getComponets(),可以返回所有該容器所包含的組件或容器,只需要保存到一個HashMap結構中,需要時檢索出來就可以了。該過程所用到的知識,其實在上面都有提到而且在實際引用中,既然Robot已經幫我們完成許多事情,也沒有必要自己再去重構一個鼠標或鍵盤事件了,不過有興趣的朋友也可以去試試。
4、 結束語
隨著我國軟件業的發展,軟件測試技術作為軟件質量保證的重要環節越來越受到重視,而在基于GUI的應用中采用自動化測試工具可以提高軟件測試的有效性和效率,特別在回歸測試中可以大大減少人力投入,還可以提高測試腳本的復用。因此,軟件自動測試平臺開發已經成為軟件測試的一個重要領域。本文介紹了基于Java的GUI應用的自動測試平臺開發需要的基本但關鍵的捕獲、回放功能,所有相關系統開發其實都離不開本文說的方法。