2007年5月23日星期三

SwingWorker for you

想必大家已经知道 SwingWorker 已经加入到了 javax.swing 包中了。它的前身经过好几个阶段改进的,如果你阅读网上的例子就会发现你阅读的例子跟你下载的包不兼容。
SwingWorker 在被正式加入到JDK6中之前叫做:org.jdesktop.swingworker.SwingWorker.java, 除了这个类之外,还包括 org.jdesktop.swingworker.AccumulativeRunnable.java 和 org.jdesktop.swingworker.SwingPropertyChangeSupport.java. 我并不打算深入介绍这几个类的源码,而是利用实际的例子来描述 SwingWorker 给 GUI 程序带来的便利。

如果写过GUI程序的开发者肯定对界面的响应度及界面冻结会有所了解。首先我得说明的是,这个问题并不是SWING特有的,所有的GUI框架如果没有处理 好都会存在这种问题,举例来说,
1. 在网络不好的环境下,在 windows 的文件浏览器中请求一个FTP地址或任何服务器的地址时,我们可以看到“灰块”(所谓灰块,是由于界面元素刷新队列被某个长时间的任务给阻塞,造成本该立 即刷新的界面得不到处理,显示出来的效果就是一块被扯得扭曲了的区域。)
2. 用过 PLSQL Developer 的开发者肯定也体会过屏幕冻结的感受吧。


费话少说,进入我们的 SwingWorker 之旅。

SwingWorker有几个重要的概念:
  1. 初始线程(Initial threads) 一般来讲是应用的主线程 (运行main 方法的那个线程) 
  2. 工作者线程(Worker Thread) 在主线程中生成的,用于执行那些长时间操作的线程
  3. 事件调度线程(Event Dispath Thread) 所有界面相关的行为都应该在这个线程进行,并且一定要保持快速的响应。
如下图是利用 NetBeans Profile 监控到线程:
  • main - 初始线程
  • Our Swingworker #1 - 工作者线程
  • AWT-Envent-Queue-0 事件调度线程
  • 其它的线程不在我们感兴趣之列




我们要记住的是:
  • 所有长时间的操作都不应该放在事件调度线程(EDT-Event Dispatch Thread,专门用来处理与界面响应相关的操作)中,否则界面在这段时间内将变得无法响应。
  • 所有对SWING组件的更新(在其已经被显示出去后)都应当通过EDT来访问,否则有可能造成线程死锁或界面根本没有反映作出的更改。
不要被前面这几段弄晕了,只要弄清楚了 Swing 的线程体系,其实大部分工作JDK 已经为你做好了。我们所要做的就是按照范例实现我们的代码。这样就肯定安全可靠。


我们的例子是一个登录界面,这个界面要求输入用户名与密码,然后点击登录,正常情况下将与中心数据库进行通信。但考虑到文章的长度,我们将使用 Thread.sleep 来演示和长时间的网络操作。

最终的界面将如下所示,我们这里也不介绍怎样制作界面(是的,我的确是使用 NetBeans 的 Gui Builder 来制作的,但使用了一个自定义的 JImagePanel)




如果阅读过别人的代码或自己认真写过SWING方面的代码,在没有使用 SwingWorker 的 GUI 工程中,一定会有如下代码:

java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
// 长时间的操作
}
});


这样做的目的就是为了使长时间的操作在另外的线程中运行。

为了使用面象对象的方式来处理这种行为,我们现在使用 SwingWorker。下面是我们的例子的代码:


import org.jdesktop.swingworker.SwingWorker;
import org.pprun.interviewofprologic.db.domain.UserBusinessDelegate;

/**
* Worker thread for login operation.
* @author pprun
*/
public class LoginSwingWorker extends SwingWorker {
private String username;
private char[] password;

private Exception exception;

/** Creates a new instance of LoginSwingWorker */
public LoginSwingWorker(String aUserName, char[] aPassword) {
this.username = aUserName;
this.password = aPassword;
}

/**
* 所有的后台操作都在这里,如果要动态将处理的数据发出去的话(比如数据库查询的应用),
* 可以在这里调用 publish 方法.
*/
@Override
protected Boolean doInBackground() throws Exception {
try {
// 我们注释掉了这段代码,取而代之以假想的 Thread.sleep
// UserBusinessDelegate cbd = UserBusinessDelegate.getInstance();
// final boolean result = cbd.login(username, password);

//return result;

try {
Thread.sleep(5000);

} catch (InterruptedException interruptedException) {
interruptedException.printStackTrace();
}

return true; // 如果想看看登录失败时的效果,return false;

} catch( Exception anyException ) {
exception = anyException;
throw exception;
}
}


/**
* Returns the exception thrown by the method doInBackground, if any, or
* null if no exception was generated.
* @return The exception generated by the call to doInBackground, or null
* if no exception was generated.
*/
public Exception getDoInBackgroundException() {
return exception;
}
}



在触发端,点击 Ok 按钮时:


private void okJButtonActionPerformed(java.awt.event.ActionEvent evt) {
// ...

loginSwingworker = new LoginSwingWorker(username, password);

loginSwingworker.addPropertyChangeListener(this);
loginSwingworker.execute();

// ...
}



属性改变监听器的实现方法,监听在 SwingWorker 中的改变:


public void propertyChange(PropertyChangeEvent pce) {
if (pce.getSource() == loginSwingworker) {
// property change event coming from the loginSwingworker
if (pce.getPropertyName().equals("state") &&
loginSwingworker.getState() == SwingWorker.StateValue.DONE ) {
// loginSwingWorker 完成,但有可能抛出异常
loginSwingworker.removePropertyChangeListener( this );
if (loginSwingworker.getDoInBackgroundException() != null ) {
// 抛出异常
if (true) {
infoJLabel.setText("Error in login!");
} else {

infoJLabel.setText("Error in login!");
}
}

} else if (loginSwingworker.isCancelled()) {
// loginSwingowrker 被取消时的代码
}
}



关于在 Swing 中使用并发的详细资料,参见 Concurrency in Swing

没有评论: