[Из песочницы] Создание простой программы с RMI и параллельным доступом на графический интерфейс в Java

Когда мне пришлось сильно углубиться в использование RMI, я поняла, какое большое значение играет умение правильно реализовать параллельность в самом графическом интерфейсе программы. На мое удивление в интернете не было достаточно материала на эту тему и особенно качественных примеров, хотя тема для любого программиста несоменно очень важная. Именно поэтому я решила поделиться своим опытом.Кратко о RMI: программный интерфейс вызова удаленных методов в языке Java (источник). С помощью него можно например управлять данными на программе сервера с одного или множества компьютеров. Подробнее можно почитать на Хабре. Будем исходить из того, что с его основами вы уже знакомы. Необходимо так же иметь представление о нововведениях в Java 8, а именно — вам понадобятся лямбда-выражения. Хорошее объяснение есть здесь.Возможности применения RMI очень разнообразны. С помощью него можно сделать, например, чат или программу для голосования на Java. В моем же примере будет простой счетчик с графической оболочкой, которая выглядит следующим образом:

image

JLable с актуальным значением счетчика JButton «Плюс» поднимает значение счетчика на единицу JButton «Сброс» сбрасывает значение счетчика на единицу Однако, прежде чем перейти к GUI, создадим сам RMI-объект счетчик и RMI-сервер, на котором он будет хранится.

Счетчик — интерфейс «Counter»:

import java.rmi.Remote; import java.rmi.RemoteException;

public interface Counter extends Remote { final String NAME = «Counter»;

int reset () throws RemoteException;

int increment () throws RemoteException; } Клас инициализации счетчика «CounterClass»:

import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject;

public class CounterClass extends UnicastRemoteObject implements Counter {

private static final long serialVersionUID = 1L; private int counter;

public CounterClass () throws RemoteException { }

@Override public synchronized int reset () { this.counter = 0; return this.counter; }

@Override public synchronized int increment () { this.counter++; return this.counter; } } Сервер для RMI-счетчика «Counter_Server»:

import java.io.IOException; import java.rmi.AlreadyBoundException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;

public class Counter_Server { public static void main (final String[] args) throws IOException, AlreadyBoundException { CounterClass counter = new CounterClass (); Registry localReg = LocateRegistry.createRegistry (Registry.REGISTRY_PORT); localReg.bind (Counter.NAME, counter); System.out.println («Counter-Server runs»); } } Так как я исхожу из того, что RMI вам уже знакомо, то не буду объяснять эти классы по строчкам. Краткое объяснение: метод «reset» приравнивает переменной «counter» значение 0 и возвращает его назад, метод «increment» увеличивает значение переменной «counter» на 1 и возвращает его назад. В сервере создаем свой регистр со скелетоном CounterClass. После этого сервер можно уже запустить.

Наконец, переходим к графике. Создадим класс Counter_Client_GUI, который создает сам фрейм с GUI и одновременно через главный метод берет стаб для удаленного управления счетчика из ранее созданного регистра:

import Counter.Counter;

public class Counter_Client_GUI extends JFrame {

private static final long serialVersionUID = 1L; protected Counter counter; protected JLabel counterLabel;

public Counter_Client_GUI (final Counter counter) { this.setDefaultCloseOperation (WindowConstants.DISPOSE_ON_CLOSE);

this.counter = counter; this.counterLabel = new JLabel (»0», SwingConstants.CENTER); final JButton incrementButton = new JButton («Плюс»); final JButton resetButton = new JButton («Сброс»);

incrementButton.addActionListener (this: incrementClicked); resetButton.addActionListener (this: resetClicked);

this.setLayout (new GridLayout (0, 1)); this.add (this.counterLabel); this.add (incrementButton); this.add (resetButton); this.setSize (300, 200); this.setVisible (true); } public static void main (String[] args) throws RemoteException, NotBoundException { Registry reg = LocateRegistry.getRegistry («localhost»); Counter counter = (Counter) reg.lookup (Counter.NAME); new Counter_Client_GUI (counter); } Тут уже стоит объяснить некоторые строки:

incrementButton.addActionListener (this: incrementClicked) — лямбда-выражение, тело Listener описано в методе incrementClicked в этом же классе; resetButton.addActionListener (this: resetClicked) — лямбда-выражение, тело Listener описано в методе resetClicked в этом же классе; Registry reg = LocateRegistry.getRegistry («localhost») — в данном примере и серер и клиент находятся на одном компьютере, поэтому вместо ссылки на регистр задаем «localhost». Перед следующим шагом необходимо понять, для чего нужен в данном случае параллельный подход. Если мы реализуем обновления JLable самым обычным способом, например:

this.counterLabel.setText (String.valueOf (novoeZnacheniePeremennoiCounter)); То это с огромной вероятностью приведет к постоянному замораживанию фрейма и при этом долгое время невозможно будет нажать ни на одну кнопку, хотя нажатия при этом регистрируются и при размораживании фрейма один за другим начнут исполняться, что приведет к хаосу. Это происходит из-за того, что в данном случае все действия с графической оболочкой будет принимать на себя лишь один единственный Thread — EventDispatchThread. И не стоит забывать, что ходя в данном примере клиент и сервер находятся на одном компьютере, управление счетчиком все-равно совершается удаленно, поэтому может возникнуть сбой в RMI регистре или же задержка доставки команды на сервер (кроме того это только пример, а в реальной программе клиент и сервер конечно же не находятся на localhost).

Теперь приступаем к самой важной части — описываем методы incrementClicked и resetClicked, вводя при этом необходимую параллельность:

protected void incrementClicked (final ActionEvent ev) { new Thread (this: incrementOnGUI).start (); } protected void resetClicked (final ActionEvent ev) { new Thread (this: resetOnGUI).start (); } Объяснение: для кадого нажатия на кнопку создаем новый Thread и запускаем его.

Внутри каждого Thread будет вот что:

protected void incrementOnGUI () { try { final int doAndGetIncrement= this.counter.increment (); final String newLabelText = String.valueOf (doAndGetIncrement); EventQueue.invokeLater (() → this.counterLabel.setText (newLabelText));

} catch (final RemoteException re) { final String message = «Fehler:» + re.getMessage (); EventQueue.invokeLater (() → JOptionPane.showMessageDialog (this, message)); } } protected void resetOnGUI () { try { final int doAndGetReset= this.counter.reset (); final String newLabelText = String.valueOf (doAndGetReset); EventQueue.invokeLater (() → this.counterLabel.setText (newLabelText));

} catch (final RemoteException re) { final String message = «Fehler:» + re.getMessage (); EventQueue.invokeLater (() → JOptionPane.showMessageDialog (this, message)); } } EventQueue.invokeLater (…) — ключевой момент программы. EventQueue с английского «очередь событий» это функция, которая (содержится в Java) посылает задание текущего Thread-а на очередь к выполнению в главный Thread. В нашем случае задание это обновление счетчика this.counterLabel.setText (newLabelText) или вывод сообщения об ошибке JOptionPane.showMessageDialog (this, message). Это обязательно необходимо для того, чтобы не возникло запутанности среди работы множества созданных Thread-ов. Например, метод будет считать в одном Thread-е в таблице количество строк, а другой Thread будет удалять строки. С большой вероятностью полученное число будет неправильным. В конечном итоге EventQueue содержит список задач, которые выполняются по очереди или по доступности, не мешая любой другой работе с графическим интерфейсом.

Стоит отметить, что в теле EventQueue.invokeLater (…) так же находится лямбда-выражение. В этой программе они применены для более лучшего и понятного внешнего вида кода.

Вот и все. Теперь параллельность работы с графической оболочкой программы обеспечена в любом случае даже при работе с RMI.

Спасибо за внимание!

© Habrahabr.ru