Когда мне пришлось сильно углубиться в использование RMI, я поняла, какое большое значение играет умение правильно реализовать параллельность в самом графическом интерфейсе программы. На мое удивление в интернете не было достаточно материала на эту тему и особенно качественных примеров, хотя тема для любого программиста несоменно очень важная. Именно поэтому я решила поделиться своим опытом.
Кратко о RMI: программный интерфейс вызова удаленных методов в языке Java (источник). С помощью него можно например управлять данными на программе сервера с одного или множества компьютеров. Подробнее можно почитать на Хабре. Будем исходить из того, что с его основами вы уже знакомы. Необходимо так же иметь представление о нововведениях в Java 8, а именно — вам понадобятся лямбда-выражения. Хорошее объяснение есть здесь.
Возможности применения RMI очень разнообразны. С помощью него можно сделать, например, чат или программу для голосования на Java. В моем же примере будет простой счетчик с графической оболочкой, которая выглядит следующим образом:
- 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 bereit");
}
}
Так как я исхожу из того, что 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.
Спасибо за внимание!
Автор: