A Simple Executor Pattern for Exposing SwingWorker to Your Swing UI Framework Clients
Join the DZone community and get the full member experience.
Join For FreeJava Swing provides a utility class
called the SwingWorker that enables Swing applications execute long running tasks
in the background. Although, the SwingWorker provides the necessary
functionality with all the necessary features (discussed later in the
article), it has some design drawbacks (e.g., in order to use SwingWorker, one
needs to extend it). This article demonstrates how the SwingWorker can
be wrapped as an executor & exposed to clients using meaningful interfaces
that separate the background processing concerns from the UI rendering concerns. Many of ideas in this article are borne out of work I did at previous organizations and inputs from other architects. I would like to acknowledge those contributions here.
For some background..
Most Swing developers are aware that Swing operates on a single UI thread model. This is true for several other desktop UI frameworks as well such as Android, SWT, .NET, Flash/Flex, etc. In a single UI threading model, all UI events (e.g., button clicks, window resizes, mouse events, keyboard events, etc.) are routed through a single event dispatch queue on a single thread.
The reason for a single event dispatching thread is of course to maintain the integrity of the UI when there are multiple UI events potentially trying to update the state of the same UI components at the same time; hence all events need to be handled in the order they are received on a single thread. Some of the common challenges faced by UI developers due to the single UI threading model are:
1) ensuring the integrity of the UI if updates to the UI components are made from threads other than the main UI thread (none of UI frameworks mentioned above guarantee thread safety if updates to the UI are made from non-UI threads)
2) handling time-intensive or long running tasks on the event handling or UI thread. Imagine a button click downloading a large file from the server which freezes the UI, not allowing the user to type or press a button or even close the application window.
The solution provided by Swing
and other UI libraries to work through the above challenges is to allow users
to execute the long running task in background threads & once the task is completed,
make the results available to the UI on the event dispatch thread. Any
interaction with the UI is scheduled on the UI thread while the long running
operations are executed on other threads. Utility methods in Swing such as SwingUtlities.invokeLater(Runnable runnable)
allow UI operations to be scheduled on event dispatch thread. A typical example (pseudo-code) of how an
implementation using SwingUtilities. invokeLater(Runnable
runnable) would look like is shown below:
//run on separate thread public void run() { Object result = performLongRunningOperation(); //schedule in UI thread SwingUtilities.invokeLater(new Runnable() { public void run() { updateUI(result); } }); }
Since Java 6, the standard JDK provides the SwingWorker class that allows users to implement long running tasks in background threads without having to create their own threads. The SwingWorker provides the following key features:
1. It is an abstract class that is meant to be subclassed by the application for the purpose of executing long running tasks in the background.
2. The SwingWorker provides an abstract doInBackground() method for subclasses to plug in their code to execute the long running task. As the method name suggests, the doInBackground() method is run on a background thread transparently using a pre-configured thread pool. doInBackground() is paired with done() method that gets run on the UI thread (EDT).
3. The SwingWorker class implements the java.util.concurrent.Future interface. This interface allows the background task to provide a return value to the other threads. Other methods on the interface allow cancellation of the background task and checking whether the background task is done or been cancelled.
4. If the background task returns intermediate results that the program would like to render on the UI, this can be accomplished using publish() and process() methods. publish() is typically executed within doInBackground() while process() is a callback that is invoked in the event dispatch thread.
5. Finally, SwingWorker provides properties progress, state that track the status/state of the task and event handling methods to handle the changes in the values of these properties that are invoked on the event dispatch thread.
Below is a sample Swing demo application that we will use to showcase the usage of the new SwingBackgroundTaskExecutor class/interfaces. The demo application is a single window application that displays audio albums in JTable. The audio album data is retrieved in chunks in a background thread using the SwingWorker and rendered incrementally on the UI. There is a progress bar that displays the progress of the data retrieval task from 0 to 100 %.
Here are some of the basic classes in the demo application.
The Album class that represents the bean data displayed on the table:
public class Album { protected String title; protected String artist; protected String genre; public Album(String title, String artist, String genre) { this.title = title; this.artist = artist; this.genre= genre; } public Album() { this.title = ""; this.artist = ""; this.genre= ""; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getArtist() { return artist; } public void setArtist(String artist) { this.artist = artist; } public String getGenre() { return genre; } public void setGenre(String genre) { this.genre = genre; } }
The AlbumTableModel class represents the table model that binds the album data to the JTable.
class AlbumTableModel extends AbstractTableModel { public static final int COL_TITLE = 0; public static final int COL_ARTIST = 1; public static final int COL_GENRE = 2; protected String[] columnNames; private List<Album> albums = new ArrayList<Album>(); public AlbumTableModel(String[] columnNames) { this.columnNames = columnNames; } @Override public int getRowCount() { return albums.size(); } @Override public int getColumnCount() { return this.columnNames.length; } @Override public String getColumnName(int column) { return columnNames[column]; } public Object getValueAt(int row, int column) { Album album = this.albums.get(row); switch (column) { case COL_TITLE: return album.getTitle(); case COL_ARTIST: return album.getArtist(); case COL_GENRE: return album.getGenre(); default: return new Object(); } } public void addRows(List<Album> albumsToAdd) { if (albumsToAdd == null || albumsToAdd.isEmpty()) { return; } int firstRowAdded = albums.size(); albums.addAll(albumsToAdd); int lastRowAdded = albums.size(); this.fireTableRowsInserted(firstRowAdded, lastRowAdded - 1); } }
Finally, the implementation of the background task AlbumBackgroundTask that retrieves the album data in the background & publishes results (List<Album>) in chunks to the UI thread.
class AlbumBackgroundTask extends SwingWorker<AlbumTableModel, List<Album>> { private static final int CAPACITY = 5; private static final int N_THREADS = 4; private AlbumTableModel albumTableModel; AlbumBackgroundTask(AlbumTableModel albumTableModel) { this.albumTableModel = albumTableModel; } @Override protected AlbumTableModel doInBackground() throws Exception { setProgress(0); Callable<List<Album>> loadAlbumTask = new Callable<List<Album>>() { @Override public List<Album> call() throws Exception { return loadAlbumData(); } private List<Album> loadAlbumData() { List<Album> albums = new ArrayList<Album>(); int index1 = (int) (Math.random() * 10); int index2 = (int) (Math.random() * 10); for (int i = (index1 <= index2) ? index1 : index2; i < ((index1 <= index2) ? index2 : index1); i++) { albums.add(allAlbums.get(i)); } return albums; } }; // execute tasks to retrieve albums concurrently ThreadPoolExecutor executor = new ThreadPoolExecutor(N_THREADS, N_THREADS, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(CAPACITY), new ThreadPoolExecutor.CallerRunsPolicy()); Queue<Future<List<Album>>> taskQueue = new LinkedList<Future<List<Album>>>(); for (int i = 0; i < 5; i++) { taskQueue.add(executor.submit(loadAlbumTask)); } int progress = 0; while (!taskQueue.isEmpty()) { Future<List<Album>> loadTask = taskQueue.remove(); if (loadTask.isDone()) { progress += 20; setProgress(progress); publish(loadTask.get()); } } return this.albumTableModel; } @Override protected void process(List<List<Album>> albumChunks) { for (List<Album> albumRows : albumChunks) { albumTableModel.addRows(albumRows); } } @Override public void done() { Toolkit.getDefaultToolkit().beep(); startButton.setEnabled(true); setCursor(null); // turn off the wait cursor } }
A screenshot of the application in action is shown below:
Although the SwingWorker is very good at what it does, it does suffer a few design drawbacks. For one, from an object oriented design perspective, it combines a lot of responsibilities in a single class (e.g., background task logic , UI rendering logic, etc.), although it can be argued that all these contribute to a single intent (that of executing a long running task in an external thread & rendering the results in the UI thread). That said, it is still possible to split the class into multiple classes/interfaces, each serving a specific purpose.
Also, any application code that needs this functionality has to necessarily subclass the SwingWorker.
Finally, most enterprise Swing based frameworks & toolkits that typically build an abstraction layer to hide Swing implementation details from the application code would find the need to wrap the SwingWorker to shield application client code from directly interacting with the SwingWorker.
Enter
SwingBackgroundTaskExecutor, a
proposed construct that uses the Executor pattern to wrap the SwingWorker & expose a meaningful interface
to clients looking to implement long running background tasks.
public class SwingBackgroundTaskExecutor { public <V, T> Future<T> execute(SwingBackgroundTask<T, V> backgroundTask, SwingUiRenderer<T, V> renderer) { … } } public interface SwingBackgroundTask<T, V> { T doInBackground() throws Exception; V getNextResultChunk(); int getProgress(); } public interface SwingUiRenderer<T, V> { void processIntermediateResults(List<V> intermediateResults, int progress); void done(T result); }
The SwingBackgroundTaskExecutor is implemented as a singleton executor. Whenever the application code requires a long running background task, it would get the singleton executor instance & pass its own implementations of the SwingBackgroundTask<T, V> and the SwingUiRenderer<T, V> interfaces. T and V are generic types corresponding to the final and intermediate result types.
As
shown above, the implementation of the long running task logic is to be
provided in SwingBackgroundTask.doInBackground().
If progress status is required, SwingBackgroundTask. doInBackground()
should store the progress as a bounded int value between 0 and 100 and
return this in SwingBackgroundTask.getProgress().
If intermediate results are to be provided, the interface should store the
intermediate results and return the next available result chunk via the SwingBackgroundTask.getNextResultChunk() method. The executor would poll for the intermediate
results in a while loop until the task is done & publish the
results to the SwingUiRenderer.processIntermediateResults()
method. If intermediate rendering is not required, SwingBackgroundTask.getNextResultChunk() can return null.
The final result is returned by the SwingBackgroundTask.doInBackground() method upon task completion & passed on by the executor to the SwingUiRenderer.done().
The SwingBackgroundTask<T, V> and SwingUiRenderer<T, V> expose the background task processing and Ui rendering as separate interfaces. The SwingBackgroundTask<T, V> methods are run on a background thread while SwingUiRenderer<T, V> methods are invoked on the UI or event dispatch thread (EDT).
Let us look take a quick look at the implementation of the SwingBackgroundExecutor:public class SwingBackgroundTaskExecutor { private static final SwingBackgroundTaskExecutor instance = new SwingBackgroundTaskExecutor(); private SwingBackgroundTaskExecutor() { } public static SwingBackgroundTaskExecutor getInstance() { return instance; } static class BackgroundSwingWorker<T, V> extends SwingWorker<T, V> { private final SwingBackgroundTask<T, V> backgroundTask; private final SwingUiRenderer<T, V> uiRenderer; public BackgroundSwingWorker(SwingBackgroundTask<T, V> backgroundTask, SwingUiRenderer<T, V> uiRenderer) { super(); this.backgroundTask = backgroundTask; this.uiRenderer = uiRenderer; } @Override protected T doInBackground() throws Exception { .. } @Override protected void process(List<V> intermediateResults) { .. } .. } public <V, T> Future<T> execute(SwingBackgroundTask<T, V> backgroundTask, SwingUiRenderer<T, V> renderer) { BackgroundSwingWorker<T, V> swingWorker = new BackgroundSwingWorker<T, V>(backgroundTask, renderer); swingWorker.execute(); return swingWorker.getFuture(); } }
The SwingBackgroundExecutor is implemented as a singleton Executor instance that wraps the SwingWorker as static inner class called BackgroundSwingWorker.
Whenever the client code requires a long running background task, it would get the singleton SwingBackgroundExecutor instance & pass its own implementations of the SwingBackgroundTask and SwingUiRenderer interfaces to the SwingBackgroundExecutor.execute() method . The background executor would create a new instance of BackgroundSwingWorker for every invocation of execute & execute the BackgroundSwingWorker instance. SwingBackgroundExecutor.execute() returns a Future<T> that can be used by the client to cancel the task, check for completion status, etc.
A quick look at the implementation of the BackgroundSwingWorker.doInBackground() may
be in order
protected T doInBackground() throws Exception { Callable<T> callable = new Callable<T>() { public T call() throws Exception { return BackgroundSwingWorker.this.backgroundTask .doInBackground(); } }; ExecutorService executorService = Executors.newFixedThreadPool(1); Future<T> wrappedFuture = executorService.submit(callable); while (!wrappedFuture.isDone() && !wrappedFuture.isCancelled()) {
//cancel the wrapped future if original task was cancelled
if (isCancelled())
{
wrappedFuture.cancel(true);
}
V result = this.backgroundTask.getNextResultChunk();
if (result != null) {
this.publish(result);
}
} this.uiRenderer.done(wrappedFuture.get()); return wrappedFuture.get(); }
The implementation of BackgroundSwingWorker.doInBackground() wraps the actual background task as a callable & submits it in a separate thread. While the wrapped future is not done or cancelled, it processes the intermediate results & publishes it. Please note that if the original future task is cancelled by the user, this would result in cancellation of the wrapped future as well.
Using the new SwingBackgroundExecutor, the client code looks as follows:
/** * Invoked when the user presses the start/cancel button. */ public void actionPerformed(ActionEvent evt) { if ("start".equalsIgnoreCase(evt.getActionCommand())) { startButton.setEnabled(false); progressBar.setValue(0); setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); BackgroundTask task = new BackgroundTask( (AlbumTableModel) this.albumTableModel); UiRenderer renderer = new UiRenderer( (AlbumTableModel) this.albumTableModel); SwingBackgroundTaskExecutor executor = SwingBackgroundTaskExecutor .getInstance(); this.future = executor.execute(task, renderer); } else { Toolkit.getDefaultToolkit().beep(); startButton.setEnabled(true); this.future.cancel(true); setCursor(null); // turn off the wait cursor } }
In conclusion, this article demonstrates how the SwingWorker can be effectively wrapped as an Executor and exposed to client code using meaningful interfaces.
Opinions expressed by DZone contributors are their own.
Trending
-
RBAC With API Gateway and Open Policy Agent (OPA)
-
Building a Java Payment App With Marqeta
-
Developers Are Scaling Faster Than Ever: Here’s How Security Can Keep Up
-
WireMock: The Ridiculously Easy Way (For Spring Microservices)
Comments