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

в 7:48, , рубрики: EventDispatchThread, EventQueue, GUI, java, java 8, lambda, rmi, thread, Программирование, счетчик

Когда мне пришлось сильно углубиться в использование 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 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.

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

Автор:

Источник

* - обязательные к заполнению поля