`
javatar
  • 浏览: 1680852 次
  • 性别: Icon_minigender_1
  • 来自: 杭州699号
社区版块
存档分类
最新评论

GUI客户端Task设计

阅读更多
主流的GUI库都是采用单线程,不管是Java的还是C++的,Java里的Swing/AWT和JFace/SWT当然都是,
因MVC的盛行,也就是观察者模式,或者说事件通知方式,如果GUI设计成多线程,开发人员必须小心翼翼的开发,稍不留神就会出现死锁或死循环,非常难用,开发人员基本上会疯掉,
据说AWT前期也想发展成多线程GUI库,这样界面更快,最终因易用性而放弃。

OK,不扯远了,即然GUI是单线程库,也就是所有的GUI操作都是在同一个GUI线程上操作的,并发操作时,有个队列进行同步排队。
在Swing里面这个特殊的线程就是著名的EDT(EventDispatchingThread),所有的GUI的变更以及相互间的事件通知都是通过这个线程进行分发的,
Swing有个不好的容错性设计,就像浏览器都支持不正规的HTML,导致写HTML的人越来越不注意正确的写法,
Swing允许你在非GUI线程直接调用GUI控件,在其内部会检查当前线程是否为EDT线程,如果不是,会自动分发到EDT队列,
这就可能使开发人员不关注EDT线程,从而导致更多的错误写法,最后就是大家都抱怨Swing写的GUI很卡,
比如:
public static void main(String[] args) {
	// 在主线程直接调用UI
	JFrame frame = new JFrame();
	frame.show();
}

正确的写法是:
public static void main(String[] args) {
	// 如果不需要等待GUI执行完成,可以用SwingUtilities.invokeLater
	// 也可以用EventQueue.invokeAndWait和EventQueue.invokeLater
	SwingUtilities.invokeAndWait(new Runnable() {
		public void run() {
			// 在EDT线程调用UI
			JFrame frame = new JFrame();
			frame.show();
		}
	});
}

而Eclipse用的JFace/SWT,当非GUI线程调用UI控件时,直接报错,虽然开始会觉得麻烦,但却减少了更多的问题,
强制你必需用下面的方式调用:
// syncExec等价于Swing的invokeAndWait
// 或者asyncExec等价于Swing的invokeLater
Display.getDefault().syncExec(new Runnable() {
	public void run() {
		// GUI有关的设置
	}
});


上面从非UI线程调UI的问题可能不算太严重,因为Swing内部有做转发,
但反过来,在UI线程内做太多事,就很严重了,比如:
component.addActionListener(new ActionListener() {
	public void actionPerformed(ActionEvent e) {
		// 执行很长时间的任务,比如远程调用,大数据较验等 ...
	}
});

GUI进程就会被阻塞,整个Swing窗口就卡住了,用户如果这时点一下界面,会发现界面不响应。
这也就是我们今天要解决的问题,在JDK1.6之前版本,
Swing是没有提供工具类的,你可以自己启一个线程或线程池操作非UI的逻辑,如:
component.addActionListener(new ActionListener() {
	public void actionPerformed(ActionEvent e) {
		new Thread(new Runnable() {
			public void run() {
				// 执行很长时间的任务,比如远程调用,大数据较验等 ...
			}
		}).start();
	}
});

或者用线程池:
component.addActionListener(new ActionListener() {
	public void actionPerformed(ActionEvent e) {
		threadPool.execute(new Runnable {
			public void run() {
				// 执行很长时间的任务,比如远程调用,大数据较验等 ...
			}
		});
	}
});

这样做的问题在于,如果我执行完后,要在界面上展示执行完的结果,
怎么办呢?当然最直接的是:
component.addActionListener(new ActionListener() {
	public void actionPerformed(ActionEvent e) {
		threadPool.execute(new Runnable() {
			public void run() {
				// 执行很长时间的任务,比如远程调用,大数据较验等 ...
				// final 结果
				SwingUtilities.invokeLater(new Runnable() {
					public void run() {
						// 在UI上显示结果
					}
				});
			}
		});
	}
});

这样的代码看的头有点晕,并且还是有几个问题没解决:
1. 任务是凌散的,没有统一管理,如果要做管理面板怎么处理?
2. 任务如何取消,万一有慢到一年都执行不完的任务,想取消怎么办?
3. 没有统一的出错处理?出错了每个地方都要重复处理。
5. 任务的进度,如果要做进度条怎么处理?
6. 任务的执行过程没有事件通知,如果我想监听任务的变化,怎么处理?

针对这些问题,JDK1.6终于加了一个类来处理,叫SwingWorker,看一下它的API:
public abstract class SwingWorker<T, V> implements RunnableFuture<T> {

	// Runnable
	void run();
	
	// Future<T>
	void cancel(boolean);
	boolean isCancelled();
	boolean isDone();
	T get();
	T get(long, TimeUnit);
	
	// SwingWorker<T, V>
	T doInBackground() throws Exception;
	void publish(V...);
	void process(List<V>);
	void done();
	void execute();
	void setProgress(int);
	int getProgress();
	StateValue getState();
	void addPropertyChangeListener(PropertyChangeListener);
	void removePropertyChangeListener(PropertyChangeListener);
	void firePropertyChange(String, Object, Object);
	PropertyChangeSupport getPropertyChangeSupport();
}

其问题,主要有:
1. 如果不看文档,你很难猜到它的用法,这个类集实体域,会话域,服务域于一身,非常复杂,即表示任务本身(实体域),也包含执行过程的状态(会话域),同时也具有主动执行能力(服务域)。
2. 签名上的两个泛型,第一个是总任务返回结果的类型,第二个是子任务的返回结果,然而大部分时候用不上子任务,却必需声明两个泛型。
3. 里面的潜规则也不少,比如:doInBackground()的返回值是给done()方法用的,而且要通过get()方法获取到返回值。
4. 没有Title, Message等描述信息。
5. 这个类和Thread类一样,只能执行一次,多次执行是无效的,因为其有状态,所以这样做是正确的。
它的用法如下:
new SwingWorker<List<User>, Void> () { // 没有子任务,第二个泛型声明为Void
	// 独立的线程池执行doInBackground方法,执行完后,结果放在get方法的Future引用中
	protected List<User> doInBackground() throws Exception {
		return remoteService.findUsers();
	}
	// 在EDT线程执行done方法,所有与GUI的相关操作都应放在done方法中处理
	protected done() {
		try {
			// 通过get获取doInBackground返回的结果
			List<User> users = get();
			// 显示到UI
			tableModel.addAll(users);
		} catch (Exception) {
			// 捕获doInBackground抛出的异常,由get方法抛出
		}
	}
}.execute(); // 直接执行,内部有封装线程池和FutureTask

JSR296(Swing Application Framework)对上面的问题做了一些修补,
定义了一个Task类继承于SwingWoker,但不彻底,而且JSR296没有定稿,现阶段也不能用,

基于这种现状,我们需要扩展自己的Task方案,
接口使用如下:
TaskExecutor.execute(new TaskSupport() {
	@TaskTitle("获取用户列表任务")
	public void execute(TaskContext context) {
		context.put("users", remoteService.findUsers());
	}
	public void succeeded(TaskEvent event) {
		List<User> users = event.getTaskContext().get("users");
		tableModel.addAll(users);
	}
});

1. TaskSupport同时实现Task接口和TaskListener接口 (实体域)
2. Task表示任务本身 (实体域)
3. TaskListener为事件通知,所有的UI操作均放在Listener里处理 (实体域)
4. TaskContext为互上下文,保存执行过程中的状态 (会话域)
5. TaskEvent为事件信息 (会话域)
6. TaskExecutor提供执行能力,当然TaskExecutor可以派生出TaskService做为SPI,这样就可以同时兼容Swing/AWT和JFace/SWT (服务域)

接口签名如下:
/**
 * 任务接口
 * 
 * @author 梁飞
 */
public interface Task {

	/**
	 * 执行任务.
	 * 非UI的工作,全部放在该方法内做,包括远程调用,数据转换,数据检验等,
	 * 其它,UI相关的工作,放在TaskListner中处理。
	 * 
	 * @param context 任务上下文状态
	 */
	void execute(TaskContext context);

}


/**
 * 任务标题
 * 
 * @author 梁飞
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TaskTitle {
	
	/**
	 * 标题
	 * 
	 * @return 标题
	 */
	String value();

}


/**
 * 任务上下文 (非线程安全,只在一次执行过程中使用)
 * 
 * @author 梁飞
 */
public interface TaskContext {
	
	/**
	 * 保存数据
	 * 
	 * @param key 键
	 * @param value 值
	 */
	void put(String key, Object value);
	
	/**
	 * 获取数据
	 * 
	 * @param <T> 值类型
	 * @param key 键
	 * @return 值
	 */
	<T> T get(String key);
	
	/**
	 * 设置当前运行信息
	 * 
	 * @param message 当前运行信息
	 */
	void setMessage(String message);
	
	/**
	 * 获取当前运行信息
	 * 
	 * @return 当前运行信息
	 */
	String getMessage();

	/**
	 * 设置进度
	 * 
	 * @param progress 进度
	 */
	void setProgress(int progress);
	
	/**
	 * 获取进度
	 * 
	 * @return 进度
	 */
	int getProgress();

	/**
	 * 取消执行
	 */
	void cancel();
	
	/**
	 * 是否已取消
	 * 
	 * @return 是否已取消
	 */
	boolean isCancelled();
	
	/**
	 * 是否失败
	 * 
	 * @return 是否失败
	 */
	boolean isFailed();

	/**
	 * 是否成功
	 * 
	 * @return 是否成功
	 */
	boolean isSucceeded();
	
	/**
	 * 是否结束
	 * 
	 * @return 是否结束
	 */
	boolean isExecuted();
	
}


/**
 * 任务执行服务 (该接口实现类必需保证线程安全)
 * 
 * @author 梁飞
 */
public interface TaskService {

	/**
	 * 执行任务
	 * 
	 * @param task 任务
	 * @return 任务上下文
	 */
	TaskContext execute(final Task task);

}


/**
 * 任务执行器 (线程安全)
 * 
 * @author 梁飞
 */
public class TaskExecutor {

	// 任务执行者服务
	private static TaskService TASK_SERVICE = new SwingTaskService();

	/**
	 * 设置任务执行者服务
	 * 
	 * @param taskService 任务执行者服务
	 */
	public static void setTaskService(TaskService taskService) {
		if (taskService == null) {
			throw new IllegalArgumentException("taskService == null");
		}
		TASK_SERVICE = taskService;
	}

	/**
	 * 执行任务
	 * 
	 * @param task 任务
	 * @return 任务上下文
	 */
	public static TaskContext execute(Task task) {
		return TASK_SERVICE.execute(task);
	}

}


/**
 * 任务事件监听器
 * 
 * @author 梁飞
 */
public interface TaskListener extends java.util.EventListener {

	/**
	 * 开始执行
	 * 
	 * @param event 事件信息
	 */
	void executing(TaskEvent event);

	/**
	 * 执行结束 (不管是成功,失败,或取消,此方法均被调用)
	 * 
	 * @param event 事件信息
	 */
	void executed(TaskEvent event);

	/**
	 * 执行成功
	 * 
	 * @param event 事件信息
	 */
	void succeeded(TaskEvent event);

	/**
	 * 执行失败
	 * 
	 * @param event 事件信息
	 */
	void failed(TaskEvent event);

	/**
	 * 取消执行
	 * 
	 * @param event 事件信息
	 */
	void cancelled(TaskEvent event);
	
	/**
	 * 后台执行
	 * 
	 * @param event 事件信息
	 */
	void backgrounded(TaskEvent event);

	/**
	 * 前台执行
	 * 
	 * @param event 事件信息
	 */
	void foregrounded(TaskEvent event);

	/**
	 * 值变化
	 * 
	 * @param event 事件信息
	 */
	void valueChanged(TaskEvent event);

	/**
	 * 信息变化
	 * 
	 * @param event 事件信息
	 */
	void messageChanged(TaskEvent event);

	/**
	 * 进度变化
	 * 
	 * @param event 事件信息
	 */
	void progressChanged(TaskEvent event);

}


/**
 * 任务事件信息
 * 
 * @author 梁飞
 */
public class TaskEvent extends java.util.EventObject {

	private static final long serialVersionUID = -7251403985319158057L;

	private final Task task;
    
    private final TaskContext taskContext;

    public TaskEvent(Object source, Task task, TaskContext taskContext) {
		super(source);
		this.task = task;
		this.taskContext = taskContext;
	}

    /**
     * 获取事件所属任务
     * 
     * @return 任务
     */
	public Task getTask() {
		return task;
	}

	/**
     * 获取事件所在任务上下文
     * 
     * @return 任务上下文
     */
	public TaskContext getTaskContext() {
		return taskContext;
	}

}


/**
 * 事件监听器适配
 * 
 * @author 梁飞
 */
public class TaskAdapter implements TaskListener {

	@Override
	public void executing(TaskEvent event) {
	}

	@Override
	public void executed(TaskEvent event) {
	}

	@Override
	public void succeeded(TaskEvent event) {
	}

	@Override
	public void failed(TaskEvent event) {
	}

	@Override
	public void cancelled(TaskEvent event) {
	}

	@Override
	public void backgrounded(TaskEvent event) {
	}

	@Override
	public void foregrounded(TaskEvent event) {
	}

	@Override
	public void valueChanged(TaskEvent event) {
	}

	@Override
	public void messageChanged(TaskEvent event) {
	}

	@Override
	public void progressChanged(TaskEvent event) {
	}

}


/**
 * 任务基类
 * 
 * @author 梁飞
 */
public abstract class TaskSupport extends TaskAdapter implements Task {
}
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics