Статьи

Простой шаблон Executor для предоставления SwingWorker вашим клиентам Swing UI Framework

Java Swing предоставляет служебный класс SwingWorker, который позволяет приложениям Swing выполнять долгосрочные задачи в фоновом режиме. Хотя SwingWorker предоставляет необходимые функциональные возможности со всеми необходимыми функциями (обсуждаемыми далее в этой статье), он имеет некоторые конструктивные недостатки (например, чтобы использовать SwingWorker, необходимо его расширить). В этой статье показано, как SwingWorker может быть упакован как исполнитель и открыт для клиентов с помощью содержательных интерфейсов, которые отделяют проблемы фоновой обработки от проблем визуализации пользовательского интерфейса. Многие идеи в этой статье основаны на работе, которую я делал в предыдущих организациях, и на вкладе других архитекторов. Я хотел бы отметить этот вклад здесь.

Для некоторого фона ..

Большинство разработчиков Swing знают, что Swing работает с единой моделью потока пользовательского интерфейса. Это справедливо и для некоторых других каркасов пользовательского интерфейса рабочего стола, таких как Android, SWT,   .NET, Flash / Flex и т. Д. В единой модели потоков пользовательского интерфейса все события пользовательского интерфейса (например, нажатия кнопок, изменение размера окна, события мыши, события клавиатуры). и т. д.) маршрутизируются через одну очередь отправки событий в одном потоке.

Причиной единственного потока диспетчеризации событий является, конечно, поддержание целостности пользовательского интерфейса, когда существует несколько событий пользовательского интерфейса, потенциально пытающихся обновить состояние одних и тех же компонентов пользовательского интерфейса в одно и то же время; следовательно, все события должны обрабатываться в порядке их получения в одном потоке. Некоторые из общих проблем, с которыми сталкиваются разработчики пользовательского интерфейса из-за единой модели потоков пользовательского интерфейса:

1) обеспечение целостности пользовательского интерфейса, если обновления компонентов пользовательского интерфейса производятся из потоков, отличных от основного потока пользовательского интерфейса (ни одна из упомянутых выше платформ пользовательского интерфейса не гарантирует безопасность потока, если обновления пользовательского интерфейса производятся из потоков, не связанных с пользовательским интерфейсом)

2)   обработка трудоемких или длительных задач в потоке обработки событий или пользовательского интерфейса. Представьте себе нажатие кнопки при загрузке большого файла с сервера, который замораживает пользовательский интерфейс, не позволяя пользователю вводить или нажимать кнопку или даже закрывать окно приложения.

Решение, предоставленное Swing и другими библиотеками пользовательского интерфейса для решения вышеуказанных проблем, состоит в том, чтобы позволить пользователям выполнять долгосрочную задачу в фоновых потоках, а после ее завершения предоставлять результаты в пользовательский интерфейс в потоке диспетчеризации событий. Любое взаимодействие с пользовательским интерфейсом планируется в потоке пользовательского интерфейса, в то время как длительные операции выполняются в других потоках. Служебные методы в Swing, такие как SwingUtlities.invokeLater (Runnable runnable), позволяют планировать операции пользовательского интерфейса в потоке диспетчеризации событий . Типичный пример (псевдокод) того, как реализация использует SwingUtilities. invokeLater (Runnable runnable) будет выглядеть так, как показано ниже:

//run on separate thread
public void run()
{
    Object result = performLongRunningOperation();
    //schedule in UI thread
    SwingUtilities.invokeLater(new Runnable()
    {
        public void run()
        {
              updateUI(result);
        }
    });
}

Начиная с Java 6, стандартный JDK предоставляет класс SwingWorker, который позволяет пользователям реализовывать долгосрочные задачи в фоновых потоках без необходимости создавать свои собственные потоки. SwingWorker предоставляет следующие основные функции:

1.  Это абстрактный класс, который предназначен для использования в подклассах приложения для выполнения долгосрочных задач в фоновом режиме.

2.   SwingWorker предоставляет абстрактный doInBackground ()   метод для подклассов , чтобы включить в свой код , чтобы выполнить давно запущенной задачи. Как следует из названия метода, метод doInBackground () выполняется в фоновом потоке прозрачно с использованием предварительно настроенного пула потоков. doInBackground () в паре с методом done (), который запускается в потоке пользовательского интерфейса (EDT).

3.  Класс SwingWorker реализует интерфейс java.util.concurrent.Future .   Этот интерфейс позволяет фоновой задаче возвращать значение другим потокам. Другие методы интерфейса позволяют отменить фоновую задачу и проверить, была ли фоновая задача выполнена или была отменена.

4.  Если фоновая задача возвращает промежуточные результаты, которые программа хотела бы отобразить в пользовательском интерфейсе, это можно выполнить с помощью методов publish () и process () . publish () обычно выполняется в doInBackground (), а process () — это обратный вызов, который вызывается в потоке диспетчеризации событий. 

5.  Наконец, SwingWorker предоставляет свойства хода выполнения , состояния, которые отслеживают состояние / состояние задачи, и методы обработки событий для обработки изменений значений этих свойств, которые вызываются в потоке диспетчеризации событий.

Ниже приведен пример демонстрационного приложения Swing, которое мы будем использовать для демонстрации использования нового класса / интерфейсов SwingBackgroundTaskExecutor .   Демонстрационное приложение — это приложение с одним окном, которое отображает аудиоальбомы в JTable. Данные аудиоальбома извлекаются порциями в фоновом потоке с помощью SwingWorker и постепенно визуализируются в пользовательском интерфейсе. Существует индикатор выполнения, который отображает ход выполнения задачи поиска данных от 0 до 100%.

Вот некоторые из основных классов в демонстрационном приложении.

Класс Album, представляющий данные компонента, отображаемые в таблице:

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;
    }
}

Класс AlbumTableModel представляет модель таблицы, которая связывает данные альбома с 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);
		}

}

Наконец, реализация фоновой задачи AlbumBackgroundTask, которая извлекает данные альбома в фоновом режиме и публикует результаты (List <Album>) порциями в поток пользовательского интерфейса.

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
		}

}

Скриншот приложения в действии показан ниже:

Таблица альбомов

Хотя SwingWorker очень хорош в том, что он делает, он имеет несколько недостатков дизайна. С одной стороны, с точки зрения объектно-ориентированного проектирования, он объединяет множество обязанностей в одном классе (например, логику фоновых задач, логику рендеринга пользовательского интерфейса и т. Д.), Хотя можно утверждать, что все они способствуют единому намерению (что выполнения длительной задачи во внешнем потоке и рендеринга результатов в потоке пользовательского интерфейса). Тем не менее, все еще возможно разделить класс на несколько классов / интерфейсов, каждый из которых служит определенной цели.

Кроме того, любой код приложения, который нуждается в этой функциональности, должен обязательно наследовать SwingWorker.

Наконец, большинство корпоративных сред и наборов инструментов на основе Swing, которые обычно создают уровень абстракции, чтобы скрыть детали реализации Swing от кода приложения, обнаружили бы необходимость обернуть SwingWorker, чтобы оградить клиентский код приложения от непосредственного взаимодействия с SwingWorker.

Введите SwingBackgroundTaskExecutor , предлагаемую конструкцию, которая использует шаблон Executor для обертывания SwingWorker и предоставления осмысленного интерфейса клиентам, желающим реализовать долгосрочные фоновые задачи.

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);

}

SwingBackgroundTaskExecutor реализуются как одноплодный исполнитель. Всякий раз, когда код приложения требует длительной фоновой задачи, он получает экземпляр синглтон-исполнителя и передает свои собственные реализации интерфейсов SwingBackgroundTask <T, V> и SwingUiRenderer <T, V> . T и V являются общими типами, соответствующими типам конечных и промежуточных результатов. 

Как показано выше, реализация логики долгосрочной задачи должна быть предоставлена ​​в SwingBackgroundTask.doInBackground () . Если требуется статус выполнения, SwingBackgroundTask. doInBackground () должен хранить прогресс в виде ограниченного значения int в диапазоне от 0 до 100 и возвращать его в SwingBackgroundTask.getProgress () . Если необходимо предоставить промежуточные результаты, интерфейс должен хранить промежуточные результаты и возвращать следующий доступный блок результатов через SwingBackgroundTask. метод getNextResultChunk () . Исполнитель будет опрашивать промежуточные результаты в цикле while, пока задача не будет выполнена, и опубликовать результаты в SwingUiRenderer.processIntermediateResults ().метод. Если промежуточный рендеринг не требуется, SwingBackgroundTask. getNextResultChunk () может вернуть ноль .

Окончательный результат возвращается методом SwingBackgroundTask.doInBackground () по завершении задачи и передается исполнителем в SwingUiRenderer.done ().

SwingBackgroundTask <Т, V> и SwingUiRenderer <Т, V>  подвергать фоновую обработку задач и рендеринг Ui в виде отдельных интерфейсов. SwingBackgroundTask <Т, V> методы выполняются в фоновом потоке , а SwingUiRenderer <Т, V> методы вызываются на пользовательском интерфейсе или отправки события поток (EDT).

Давайте посмотрим на реализацию 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();
       }
}

SwingBackgroundExecutor реализован как одноэлементный экземпляр Executor, который оборачивает SwingWorker в статический внутренний класс с именем BackgroundSwingWorker.

Всякий раз, когда клиентскому коду требуется длительно выполняемая фоновая задача, он получает единственный экземпляр SwingBackgroundExecutor и передает свои собственные реализации интерфейсов SwingBackgroundTask и SwingUiRenderer методу SwingBackgroundExecutor.execute () . Фоновый исполнитель будет создавать новый экземпляр BackgroundSwingWorker для каждого вызова execute и execute экземпляра BackgroundSwingWorker . SwingBackgroundExecutor.execute () возвращает Future <T>, который может использоваться клиентом для отмены задачи, проверки состояния завершения и т. Д.

Беглый взгляд на реализацию BackgroundSwingWorker.doInBackground () может быть в порядке

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(); }

Реализация BackgroundSwingWorker.doInBackground () оборачивает реальную фоновую задачу как вызываемую и передает ее в отдельный поток. Хотя перенесенное будущее не сделано или не отменено, оно обрабатывает промежуточные результаты и публикует его. Обратите внимание, что если пользователь отменяет исходное задание на будущее, это также приведет к отмене перенесенного будущего.

Используя новый SwingBackgroundExecutor , клиентский код выглядит следующим образом:

	/**
	 * 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
		}
	}

В заключение, эта статья демонстрирует, как SwingWorker может быть эффективно упакован как Executor и представлен клиентскому коду с помощью содержательных интерфейсов.