Java-Programm nur einmal starten

Aus Byte-Welt Wiki
Zur Navigation springenZur Suche springen


Einleitung

Unter bestimmten Umständen kann es gewünscht sein, dass nur eine aktive Instanz eines Programms auf einem Rechner ausführbar ist. Sollte das Programm ein weiteres Mal aufgerufen werden, soll die gerade aktive, unsichtbare Instanz des Programms in den Fokus des Bedieners geholt werden. Es stellt sich also die Frage: Wie kann überprüft werden, ob ein Programm schon läuft und dann das zweite Starten verhindert werden?

Jedes Java-Programm wird in einer eigenen virtuellen Maschine (VM) gestartet, somit sind sie räumlich voneinander getrennt.

Im Web sind einige interessante Vorschläge zu finden, die beschreiben, wie man ein Java-Programm so programmiert, dass nur eine aktive Instanz im Arbeitsspeicher zugelassen wird, man also ein Java-Programm nur "einmal" ausführen kann.

Möglichkeiten zur Lösung gibt es. Hier einige Ansätze:

  • eine Lock-Datei
  • einen Port sperren
  • Client/Server-Anwendung
  • eine Kombination aus alle dem.

Jede der Lösungen bietet Vorteile, wie auch Nachteile in der Praxis. In unserem Artikel Java-Anwendung nur einmal ausführen - Java-Blog-Buch wurden die wichtigsten beschrieben.

Demnach ist einer der größten Nachteile beim Einsatz von Lockdateien, dass man mit einer bereits gestarteten Anwendung nicht kommunizieren kann. Man kann die laufende Instanz bspw. nicht sichtbar machen, wenn sie gerade verdeckt ist. Das Gleiche trifft auf die Port-Sperrung zu. Im schlimmsten Fall passiert also gar nichts und/oder der Bildschirm bleibt unverändert, wenn man versucht, das gewünschte Programm ein weiteres Mal zu starten.

Client-/Server-Anwendungen sind meist etwas komplexer, können aber derartige Probleme lösen.

Eine bisher nicht besprochene Lösung wird im Folgenden beschrieben. Eine Lösung mit Hilfe der seit dem JDK 1.0 mitgelieferten Java-Bibliothek für verteilte Anwendungen - RMI. Sie besticht durch ihre Einfachheit gegenüber einer Client-/Server-Anwendung und einem Maximum an Vorteilen.

RMI - eine Einführung

Um die in diesem Artikel besprochene Lösung zu programmieren, werden keine RMI-Kenntnisse vorausgesetzt, sie werden aber beim Verstehen des Codes und der Vorgehensweise helfen, so dass später die Implementierung in eigenen Programmen leichter fällt.

Daher empfehlen wir, bevor wir weiter machen, den kurz und knapp gehaltenen Einführungsartikel (RMI minimal) zu studieren.

Schritt für Schritt zum Start-Limit

Beginnen wir also nun, eine kleine Demo-Anwendung zu programmieren, mit dem Ziel, dass diese nur einmal gestartet werden kann. Weitere Starts werden nur die bereits aktive Anwendung in den Fokus des Benutzers holen.
Wenn man es genau nimmt, wird zwar eine weitere Instanz der Anwendung gestartet werden können, jedoch nach der Kommunikation mit der bereits laufenden Instanz sofort wieder beendet.

Die Ausgangslage - ein einfaches Fenster

Zunächst möchten wir eine einfache Anwendung erzeugen, die wir dann im weiteren Schritt mit den gewünschten Funktionen ausbauen.

 1 import java.awt.*;
 2 import java.awt.event.*;
 3 import javax.swing.*;
 4 
 5 /**
 6  * DemoFrame ist der Einstiegspunkt in die Anwendung.
 7  * Hier werden wichtige Prüfungen angestoßen und die Benutzeroberfläche zusammengebaut.
 8  * @author Gernot Segieth
 9  */
10 public class DemoFrame {
11    private JFrame frame;
12 
13    public DemoFrame() {
14       frame = new JFrame("Demo-Frame");
15       frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
16       frame.addWindowListener(new WindowAdapter() {
17          public void windowClosing(WindowEvent e) {
18             exit();
19          }
20       });
21 
22       /* Position für später einzufügenden Code aus
23          Kapitel Zusammenspiel Server und Client */
24 
25       frame.add(createMainPanel());
26       frame.pack();
27       frame.setLocationByPlatform(true);
28       frame.setVisible(true);
29    }
30 
31    private JPanel createMainPanel() {
32       JPanel panel = new JPanel(new BorderLayout());
33       panel.setPreferredSize(new Dimension(600, 400));
34       return panel;
35    }
36 
37    private void exit() {
38       int option = JOptionPane.showConfirmDialog(
39             frame,
40             "Möchten Sie die Anwendung wirklich beenden?",
41             getAppTitle() + " - Beenden bestätigen",
42             JOptionPane.YES_NO_OPTION,
43             JOptionPane.QUESTION_MESSAGE);
44 
45       if (option == JOptionPane.YES_OPTION) {
46 
47          /* Position für später einzufügenden Code aus
48             Kapitel Zusammenspiel Server und Client */
49 
50          frame.dispose();
51       }
52    }
53 
54    public static void main(String[] args) {
55       try {
56 	 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
57       }
58       catch(Exception ex) {
59 	 System.out.println(ex);
60       }
61 
62       SwingUtilities.invokeLater(new Runnable() {
63          public void run() {
64             new DemoFrame();
65          }
66       });
67    }
68 }

Der Code der Anwendung ist schon für den nächsten Schritt vorbereitet, so dass in den folgenden Schritten weniger verwirrende Code-Änderungen durchgeführt werden müssen.

Die Ausgabe: ein leerer JFrame

Wir erzeugen also einen leeren JFrame.

In Zeile 15 schalten wir das Standardverhalten des JFrames beim Beenden (Klick auf den Schließen-Button des Fensters) aus. Damit der JFrame geschlossen werden kann, implementieren wir anschließend (Zeile 16 bis Zeile 20) einen WindowListener, der für die Beendigung des Programms sorgt. Man könnte das auch auch einfacher lösen, in dem man in Zeile 15 frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); schreiben würde. Aber wie erwähnt wird bereits ein später benötigtes Verhalten hier implementiert.

In Zeile 25 fügen wir das in Zeile 31 bis Zeile 35 definierte JPanel in dem JFrame ein. Das JPanel sorgt hier erst mal nur für die Größe des JFrames bei der Ausgabe auf dem Bildschirm. Später werden wir noch Code für das JPanel hinzufügen, der die Demo anschaulicher machen wird.

In Zeile 27 legen wir fest, dass die JVM unser Fenster dort auf dem Bildschirm positionieren wird, wie es den Regeln des Host-Systems zur Ausgabe von neuen Fenstern auf dem Bildschirm entspricht.

Die exit()-Methode schließt unsere Anwendung, wenn der Benutzer auf den entsprechenden Fenster-Button klickt.

Vorüberlegung

Gut, eine einfache GUI wurde aufgebaut. Später werden wir noch einige Objekte darin ablegen/einfügen, um eine sinnvollere Anwendung zu erhalten.

Folgende Frage müssen wir uns nun beantworten: Wie muss sich ein Programm verhalten, um zu ermitteln, ob ein gleichartiges Java-Programm bereits ausgeführt wird?

1. Wird das Programm das "erste" Mal gestartet, muss es zunächst nach einer weiteren aktiven gleichartigen Anwendung "suchen".
2. Sollte es keine aktive gleichartige Anwendung geben, ist die soeben gestartete Instanz die erste und wird dem Benutzer auf dem Bildschirm angezeigt.
3. Ist allerdings bereits eine gleichartige Anwendung aktiv, muss sich die zuletzt gestartete Instanz wieder beenden und die möglicherweise gerade nicht sichtbare erste Instanz in den Vordergrund des Bildschirms gebracht werden.

Punkt 1 klingt etwas nach dem klassischen Verhalten eines Clients. Er sucht nach einem Server, um seine Dienste zu nutzen.
Punkt 2 klingt nach dem Verhalten eines Servers. Er wird gestartet und bietet Dienste für Clients an, die sich mit ihm verbinden.
Punkt 3 klingt nach einer Interaktion von Client und Server. Der Client hat einen Server "gefunden", nutzt einen Dienst (zeige die Benutzeroberfläche an) und beendet die Verbindung bzw. sich selbst.

Das bedeutet, wir benötigen in unserem Programm das Verhalten eines Servers und eines Clients.

Und genau dieses Verhalten werden wir nun schrittweise mit Hilfe von RMI implementieren.

Definition des Remote-Interfaces

Hier nun also die Definition unseres Remote-Interfaces, welches alle Methodenrümpfe enthält, die unser Programm für das gewünschte Verhalten benötigt.

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

/**
 * Das Interface beschreibt die benötigten Methoden, die von einem RemoteTask implementiert werden müssen.
 * @author Gernot Segieth
 */
public interface RemoteTask extends Remote {

    /**
     * Wird prüfen, ob bereits ein RMI-Server online ist.
     * Der RMI-Server ist online, wenn bspw. der RMI-Port (1099) belegt ist, ein Remote-Objekt 
     * in der RMI-Registry gespeichert wurde und eine Methode dieses Objektes aufgerufen werden kann.
     * @return <code>true</code>, wenn festgestellt werden konnte, dass der RMI-Server bereits online ist,
     * sonst <code>false</code>.
     * @throws java.rmi.RemoteException
     */
    boolean isRunningAnotherInstance() throws RemoteException;
    
    /**
     * Wird die Benutzeroberfläche der bereits laufenden Programminstanz anzeigen.
     * @throws java.rmi.RemoteException
     */
    void showRunningInstance() throws RemoteException;
    
}

Die erste Methode wird also zur Ermittelung eines laufenden Servers benötigt, die Zweite, um die laufende (erste) Instanz sichtbar zu machen.

Implementierung der Remote-Funktionen

Die entsprechende Ausprogrammierung (Implementierung), der von unserem Remote-Interface vorgegebenen Methoden, sieht für unsere Anwendung nun so aus:

import java.awt.Window;
import java.rmi.RemoteException;

/**
 * RemotTaskImpl enthält die Aufgaben des RMI-Servers in Form von ausprogrammierten Methoden des Remote-Interfaces.
 */
public class RemoteTaskImpl implements RemoteTask {
    private final Window window;
    
    
    /**
     * Der RMI-Server kennt die laufende (erste) Anwendung und übergibt hier die Fenster-Instanz,
     * die in den Fokus des Benutzers geholt werden soll, wenn eine weitere gleichartige Anwendung
     * gestartet werden sollte.
     * @param window Die Benutzeroberfläche der "ersten" Instanz.
     */
    public RemoteTaskImpl(Window window) {
        this.window = window;
    }
    
    /**
     * Diese Methode kann vom Client nur aufgerufen werden, wenn ein RMI-Server aktiv ist
     * und von diesem das entsprechende Remote-Objekt bereitgestellt wurde.
     * Ist diese Methode aufrufbar, heißt das für den Client, dass bereits eine gleichartige
     * Anwendung aktiv ist.
     * @see RemoteTask
     * @return <code>true</code> wenn bereits eine andere, gleichartige Instanz aktiv ist.
     * @throws RemoteException 
     */
    @Override
    public boolean isRunningAnotherInstance() throws RemoteException {
        System.out.println("Another instance is already running.");
        return true;
    }  

    /**
     * Diese Methode wird vom RMI-Client aufgerufen, um dem RMI-Server mitzuteilen, dass er
     * die bereits laufende Instanz in den Fokus des Benutzers holen soll.
     * @see RemoteTask
     * @throws RemoteException 
     */
    @Override
    public void showRunningInstance() throws RemoteException {
        System.out.println("The instance that is already running is displayed.");
        window.setLocationRelativeTo(null);
        window.setVisible(true);
        window.toFront();
    }
}

Implementieren des Servers

import java.awt.Frame;
import java.rmi.AlreadyBoundException;
import java.rmi.NoSuchObjectException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;

/**
 * Server ist der RMI-Server, der Remote-Objekte in der RMI-Registry speichert
 * und Anfragen von Clients beantwortet.
 * Die Klasse erwartet mindestens die Instanz der Fenster-Klasse, das als Single-Fenster behandelt werden soll.
 *
 * @author Gernot Segieth
 */
public class Server {
    private RemoteTaskImpl task;
    private String bindName;
    
    /**
     * Erzeugt einen RMI-Server.
     * Die übergebene Fenster-Instanz wird bei Bedarf sichtbar gemacht.
     * @param frame Die Instanz der Fenster-Klasse, die sichtbar gemacht werden soll.
     * @throws RemoteException
     * @throws AlreadyBoundException 
     */
    public Server(Frame frame) throws RemoteException, AlreadyBoundException {
        this(frame, Registry.REGISTRY_PORT);
    }  

    public Server(Frame frame, int port) throws RemoteException, AlreadyBoundException {
        this.bindName = frame.getTitle();
        LocateRegistry.createRegistry(port);

        task = new RemoteTaskImpl(frame);
        RemoteTask stub = (RemoteTask) UnicastRemoteObject.exportObject(this.task, 0);
        //RemoteServer.setLog(System.out);

        Registry registry = LocateRegistry.getRegistry();
        registry.bind(this.bindName, stub);

        System.out.println("Remote object stored in RMI registry.");
    }
    
    /**
     * Entfernt das Remote-Objekt aus der RMI-Registry.
     * Der RMI-Server wird damit heruntergefahren.
     * @throws NoSuchObjectException 
     * @throws java.rmi.NotBoundException 
     */
    public void shutDown() throws NoSuchObjectException, RemoteException, NotBoundException {
        UnicastRemoteObject.unexportObject(this.task, true);
        Registry registry = LocateRegistry.getRegistry();
        registry.unbind(this.bindName);
    }

    /**
    public static void main(String[] args) throws RemoteException, AlreadyBoundException {
        Server server = server = new Server("rmi://localhost/SingleWindow");
    }
    */
}

Implementieren des Clients

import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Client ist der RMI-Client, der nach passenden Remote-Objekten in der RMI-Registry sucht
 * und die bereit gestellten Methoden für Anfragen an den Server aufruft.
 * Die Klasse erwartet mindestens die URI (Adresse), unter der nach Remote-Objekten gesucht werden soll.
 * @author Gernot Segieth
 */
public class Client {

    private RemoteTask task;

    public Client(String taskName) throws RemoteException {
        this(taskName, Registry.REGISTRY_PORT);
    }

    public Client(String taskName, int port) throws RemoteException {
        try {
            Registry registry = LocateRegistry.getRegistry(port);
            task = (RemoteTask) registry.lookup(taskName);
        } catch (RemoteException | NotBoundException ex) {
            Logger.getLogger(Client.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    /**
     * Ermittelt, ob bereits ein anderer RMI-Server online ist.
     *
     * @return <code>true</code>, wenn festgestellt werden konnte, dass bereits
     * eine RMIRegistry bzw. ein RMI-Server gestartet wurde, sonst <code>false</code>.
     * @throws RemoteException
     */
    public boolean isRunningAnotherInstance() throws RemoteException {
        return task.isRunningAnotherInstance();
    }
    
    public void showRunningInstance() throws RemoteException {
        task.showRunningInstance();
    }    

    /**
    public static void main(String[] args) throws RemoteException {
        Client client = new Client("rmi://localhost/SingleWindow");
    }
    */
}

Zusammenspiel Server und Client

Nehmen wir uns an dieser Stelle noch mal die eingangs geschriebene Fenster-Klasse vor.

In die dort markierte Position im Code fügen wir folgendes Fragment ein:

      try {
         Server server = new Server(frame, port);
      } catch (RemoteException | AlreadyBoundException ex) {
         System.err.out(ex);
         try {
            Client client = new Client(frame.getTitle(), port);
            boolean isRunning = client.isRunningAnotherInstance();
            System.out.println("Remote object " + (isRunning ? "is" : "is not") + " available.");
            if (isRunning) {
               client.showRunningInstance();
               System.exit(0);
            }
         } catch (RemoteException re) {
            System.err.out(re);
         }
     }

Außerdem ändern wir noch die private Methode zum Beenden unseres Programms. In die exit()-Methode setzen wir folgendes Fragment an die markierte Position:

        try {
           server.shutDown();
           server = null;
        } catch (RemoteException | NotBoundException ex) {
           System.err.out(ex);
        }

Was passiert hier nun? Zunächst erzeugen wir ein Server-Objekt. Den Code dazu haben wir weiter oben bereits fertig gestellt. Der Server erwartet die Fenster-Instanz dieses gerade gestarteten Programms, sowie den Port (standardmäßig 1099), an dem er auf nachfolgende Programm-Instanzen warten soll. Aus der Fenster-Instanz entnimmt der Server den Titel des Frames und benutzt ihn zur Registrierung des Remote-Objektes.

Ist der Port, an dem der Server lauschen soll allerdings bereits belegt, kann das bedeuten, dass der Port von einer früher gestarteten Programm-Instanz belegt wurde. Daher wird eine Fehlermeldung in die Console geschrieben und eine Instanz eines Clients angelegt. In der RMI-Fehlermeldung wird u.a. angegeben, dass der Port bereits gebunden wurde. Das ist aber in diesem Fall kein Problem. Wir konnten damit rechnen, dass bereits ein Programm dieses (unseres) Typs früher gestartet wurde.

Der Client erwartet den Titel der Fenster-Klasse, den er zum Vergleich an den Server übergibt. Wurde ein Remote-Objekt unter dem Namen unserer Fenster-KLasse in der RMIRegistry abgelegt, handelt es sich dabei um ein von uns früher gestartes Programm. Der Client fragt nun beim Server nach, ob es bereits ein registriertes Remote-Objekt gibt. Eine Ausgabe zum Ergebnis dieser Anfrage wird in die Console geschrieben.

Meldet der Server, dass es eine laufende Instanz unserer Fenster-Klasse gibt, bittet der Client den Server, diese Instanz im Vordergrund des Bildschirms anzuzeigen. Danach beendet sich der Client.

In die exit()-Methode setzen wir an der markierten Position das 2. Fragement. Damit wird der Server vor dem Beenden herunter gefahren. Er löst die Port-Bindung und löscht das Remote-Objekt aus der RMIRegistry.

Das war's. Nun testen wir. Also Code kompilieren und Anwendung starten.

Testen der Anwendung

Baustelle.png Dieser Beitrag wird derzeit noch bearbeitet. Der Text ist deshalb unvollständig und kann Fehler oder ungeprüfte Aussagen enthalten.

Ausbau der Anwendung

Problem mit dieser Lösung

Wie wir gesehen haben, eröffnen sich Möglichkeiten für Probleme bei der Anwendung von RMI in unserer Anwendung.

  • Zunächst kann man nicht vorhersehen, ob der von uns verwendete Port nicht schon von einer anderen Software belegt wurde. In diesem Fall bekommen wir nichts von unserem Programm zu sehen. Aber auch, wenn wir nur den RMI-Port benutzen, lauern hier weitere Stolperdrähte.
  • Was passiert eigentlich, wenn wir mehrere Programme geschrieben haben, die alle diese nette Funktion des "Nur-einmal-startens" unterstützen? Man könnte zwar für jedes unserer Programme einen eigenen/anderen Port verwenden. Aber da gibt es sicher auch Grenzen.

Lösung des Problems

Baustelle.png Dieser Beitrag wird derzeit noch bearbeitet. Der Text ist deshalb unvollständig und kann Fehler oder ungeprüfte Aussagen enthalten.


Fragen

Das Thema wurde nicht ausreichend behandelt? Du hast Fragen dazu und brauchst weitere Informationen?

Besuche uns im Byte-Welt-Forum, wir helfen Dir gerne!


Dir hat dieser Artikel gefallen? Schreibe einen Kommentar

Du musst angemeldet sein, um einen Kommentar abzugeben.