Weiter zum Inhalt

D) Java-Anwendung nur einmal ausführen

Die grundlegende Logik wurde bereits erläutert, weshalb ich an dieser Stelle nicht noch einmal alles wiederholen werde. Stattdessen sollten Sie anhand der vorhergehenden Abschnitte und der Kommentare im Code die Funktionen der Klasse ohne Probleme nachvollziehen können.

package de.jbb.sic;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;

/**
 * SingleInstanceController ist eine Klasse, mit der ueberprueft werden kann, ob
 * eine andere, mit dem SingleInstanceController registrierte, Klasse bereits auf
 * dem System laeuft (Port- und Klassenabhaengig). Sollte eine solche Klasse
 * bereits ausgefuehrt worden sein, besteht die Moeglichkeit mit dieser zu
 * kommunizieren.
 */
public class SingleInstanceController {

  private boolean result = false;
  private File file = null;
  private ObjectOutputStream oos = null;
  private ObjectInputStream ois = null;
  private ServerSocket server = null;
  private Socket client = null;
  private ArrayList<ApplicationStartedListener> listener = null;
  private String appname = null;

  /**
   * Konstruktor für ein neues SingleInstanceController Objekt. Uebergeben
   * wird ein File-Objekt, dass auf den Ort verweist, wo der aktuelle Port
   * gespeichert werden soll. Zusaetzlich wird eine eindeutige
   * Programmbezeichnung uebergeben (z. B. package + classenname), anhand der
   * identische Applikationen identifiziert werden koennen.
   *
   * @param file Speicherort
   * @param masterclass eindeutige Programmbezeichnung
   * @see SingleInstanceController(String)
   */
  public SingleInstanceController(File file, String appname) {

    this.file = file;
    this.appname = appname;
    this.listener = new ArrayList<ApplicationStartedListener>();
  }

  /**
   * Konstruktor für ein neues SingleInstanceController Objekt. Es wird ein
   * default Pfad für den Port erstellt. Es wird eine eindeutige
   * Programmbezeichnung uebergeben (z. B. package + classenname), anhand der
   * identische Applikationen identifiziert werden koennen. Dieser Konstruktor
   * ist nicht zu empfehlen da auch andere Applikationen, die den
   * SingleInstanceController verwenden, auf dieses File zugreifen koennten.
   *
   * @param masterclass aufrufende Klasse
   * @see SingleInstanceController(File, String)
   */
  public SingleInstanceController(String appname) {
    this(new File(System.getProperty("java.io.tmpdir") + "/923jhakE53Kk9235b43.6m7"), appname);
  }

  /**
   * Fuegt einen ApplicationStartedListener hinzu
   *
   * @param asl
   */
  public void addApplicationStartedListener(ApplicationStartedListener asl) {
    this.listener.add(asl);
  }

  /**
   * Entfernt einen ApplicationStartedListener
   *
   * @param asl
   */
  public void removeApplicationStartedListener(ApplicationStartedListener asl) {
    this.listener.remove(asl);
  }

  /**
   * Ueberprueft, ob die Applikation schon registriert wurde und somit bereits
   * ausgefuert wird
   *
   * @return true falls sichergestellt werden kann, dass die Applikation
   *         bereits laeuft
   */
  public boolean isOtherInstanceRunning() {

    if (!this.file.exists()) {
      return false;
    }
    return sendMessageToRunningApplication(new ClassCheck(this.appname));
  }

  /**
   * Schickt der registrierten Applikation eine Nachricht als Object.
   *
   * @param obj das zu sendende Objekt
   * @return true falls die Nachricht erfolgreich uebermittelt wurde
   */
  public boolean sendMessageToRunningApplication(final Object obj) {

    this.result = false;
    try {
      this.client = new Socket("localhost", getPortNumber());
      // In einem neuen Thread kommunizieren, um einen Deadlock zu verhindern
      new Thread(new Runnable() {

        public void run() {
          try {
            SingleInstanceController.this.oos = new ObjectOutputStream(SingleInstanceController.this.client.getOutputStream());
            SingleInstanceController.this.ois = new ObjectInputStream(SingleInstanceController.this.client.getInputStream());
            SingleInstanceController.this.oos.writeObject(obj);
            SingleInstanceController.this.oos.flush();
            SingleInstanceController.this.result = SingleInstanceController.this.ois.readBoolean();
          }
          catch (IOException e) {
            SingleInstanceController.this.result = false;
          }
        }
      }).start();
      // Falls nach 1 Sekunde keine Antwort = Server nicht erreichbar
      for (int i = 0; i < 10; i++) {
        if (this.result == true) {
          break;
        }
        try {
          Thread.sleep(100);
        }
        catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
      this.client.close();
      return this.result;
    }
    catch (IOException e) {
      return false;
    }
  }

  /**
   * Registriert diese Applikation als gestartet. Sollte die Applikation
   * nochmal gestartet werden, so kann diese registrierte Applikation darueber
   * informiert werden
   *
   * @return true wenn die Applikation erfolgreich registriert wurde
   */
  public boolean registerApplication() {

    try {
      if (!this.file.exists()) {
        if (!this.file.getParentFile().mkdirs() && !this.file.getParentFile().exists()) {
          return false;
        }
        if (!this.file.createNewFile()) {
          return false;
        }
      }
      BufferedWriter wuffy = new BufferedWriter(new FileWriter(this.file));
      int port = getFreeServerSocket();
      if (port != -1) {
        startServer();
      }
      wuffy.write(String.valueOf(port));
      wuffy.close();
      return true;
    }
    catch (IOException e) {
      return false;
    }
  }

  /**
   * Listener informieren, dass Nachricht angekommen ist
   *
   * @param obj Objekt fuer die Listener
   */
  protected void messageArrived(Object obj) {

    for (ApplicationStartedListener asl : this.listener) {
      asl.messageArrived(obj);
    }
  }

  /**
   * Listener informieren, dass selbe Applikation gestartet wurde und eine
   * Verbindung aufgebaut hat
   */
  protected void applicationStartet() {

    for (ApplicationStartedListener asl : this.listener) {
      asl.applicationStarted();
    }
  }

  /**
   * Listener informieren, dass eine andere Applikation versucht hat sich
   * anzumelden
   */
  protected void foreignApplicationStarted(String name) {

    for (ApplicationStartedListener asl : this.listener) {
      asl.foreignApplicationStarted(name);
    }
  }

  /**
   * Ermittelt die zuletzt eingetragene Port-Nummer
   *
   * @return die zuletzt eingetragene Port-Nummer
   */
  private int getPortNumber() {

    try {
      BufferedReader buffy = new BufferedReader(new FileReader(this.file));
      int port = Integer.parseInt(buffy.readLine().trim());
      buffy.close();
      return port;
    }
    catch (Exception e) {
      return -1;
    }
  }

  /**
   * Startet den Server, um mit spaeter gestarteten Applikationen zu
   * kommunizieren
   */
  private void startServer() {

    new Thread(new Runnable() {

      public void run() {
        while (true) {
          try {
            SingleInstanceController.this.client = SingleInstanceController.this.server.accept();
            if (SingleInstanceController.this.client.getInetAddress().isLoopbackAddress()) {
              new Thread(new Runnable() {

                public void run() {
                  try {
                    SingleInstanceController.this.oos = new ObjectOutputStream(SingleInstanceController.this.client.getOutputStream());
                    SingleInstanceController.this.ois = new ObjectInputStream(SingleInstanceController.this.client.getInputStream());
                    Object obj = SingleInstanceController.this.ois.readObject();
                    if (obj instanceof ClassCheck) {
                      if (obj.toString().equals(SingleInstanceController.this.appname)) {
                        SingleInstanceController.this.oos.writeBoolean(true);
                        applicationStartet();
                      }
                      else {
                        SingleInstanceController.this.oos.writeBoolean(false);
                        foreignApplicationStarted(obj.toString());
                      }
                    }
                    else {
                      messageArrived(obj);
                      SingleInstanceController.this.oos.writeBoolean(true);
                    }
                    SingleInstanceController.this.oos.flush();
                    SingleInstanceController.this.client.close();
                  }
                  catch (IOException e) {
                    e.printStackTrace();
                  }
                  catch (ClassNotFoundException e) {
                    e.printStackTrace();
                  }
                }
              }).start();
            }
          }
          catch (IOException e) {
            e.printStackTrace();
          }
        }
      }
    }).start();
  }

  /**
   * Ermittelt einen freien Port zwischen 2000 und 10000
   *
   * @return den 1. freien Port oder falls kein freier Port gefunden wurde -1
   */
  private int getFreeServerSocket() {

    for (int i = 2000; i < 10000; i++) {
      try {
        this.server = new ServerSocket(i);
        return i;
      }
      catch (IOException ignore) {}
    }
    return -1;
  }
}

Ein Beispiel zur Implementierung des SingleInstanceControllers finden Sie auf der nächsten Seite.


{ 10 } Comments

  1. Illuvatar | 29. Dezember 2008 um 12:31 | Permalink

    Eine kleine Anmerkung zur FileLock-Methode: Seit Java NIO gibt es die Möglichkeit, “echte” File-Locks zu realisieren. Hierzu wird die Methode FileChannel#tryLock verwendet.

    Dadurch fallen ein Großteil der Nachteile weg. Eine Kommunikation ist allerdings immer noch nicht möglich. Solch ein FileLock wird aber eben automatisch aufgehoben sobald das Programm nicht mehr läuft. Das wichtige Kriterium wäre nicht mehr, ob die Datei existiert, sondern ob tryLock ein FileLock oder null zurückgibt. Ob die Datei noch existiert, kann dafür als Kriterium verwendet werden, ob das Programm sauber beendet wurde.

  2. Stefan Kiesel | 29. Dezember 2008 um 12:55 | Permalink

    Hallo Illuvatar,

    vielen Dank für deinen Beitrag. Ich habe die tryLock Methode der Klasse FileChannel nicht angesprochen, da das Verhalten von FileLock sehr systemspezifisch ist, und eine Kommunikation zwischen den beiden Applikationen nicht mehr möglich wird.

    Bei Gelegenheit werde ich dieses Verfahren als kleinen Zusatz aber ergänzen.

    Gruß
    Stefan

  3. pcworld | 16. April 2009 um 15:10 | Permalink

    Super Tutorial!

    Sowas hätte ich denke ich kaum zu Stande gebracht, zumindest nicht so genau :-)

    Gruß,
    pcworld

  4. Stefan | 4. November 2009 um 15:54 | Permalink

    SingleInstanceController.this.client.getInetAddress().getCanonicalHostName()
    liefert bei mir “127.0.0.1″ statt “localhost”. Damit akzeptiert der Server den Client nicht und die Sache geht schief.

    Scheint eine Eigenheit von MS-Vista zu sein? Hab’ verschiedene JDKs probiert.

  5. Stefan Kiesel | 4. November 2009 um 19:42 | Permalink

    Hi Stefan,

    ein solches Problem ist mir nicht bekannt. Ich habe den Quellcode aber einmal ausgebessert. Anstatt von getCanonicalHostName().equals("localhost") wird jetzt einfach isLoopbackAddress() geschrieben.

    Über eine Rückmeldung, ob es jetzt funktioniert oder nicht, würde ich mich freuen.

    Gruß
    Stefan

  6. Stefan | 5. November 2009 um 12:19 | Permalink

    Hi Stefan,

    ja. Das klappt jetzt auch unter Vista.

    Danke und schöne Grüße,
    Stefan

  7. Stefan | 15. Dezember 2009 um 12:14 | Permalink

    Ich muss in main() explizit System.exit() aufrufen sonst hängt das Programm bis ich Ctl-C drücke. Da wartet die JVM bis sich der Thread beendet.
    Könnte helfen: http://java.sun.com/j2se/1.5.0/docs/api/java/lang/Thread.html#setDaemon(boolean)

  8. Stefan Kiesel | 15. Dezember 2009 um 12:22 | Permalink

    Hallo Stefan,

    ich kann leider gerade nicht nachvollziehen, wovon Sie reden. Das System.exit(0); in der Example Klasse kann auch ohne Probleme weggelassen werden.

    Grüße
    Stefan

  9. Stefan | 15. Dezember 2009 um 12:53 | Permalink

    Nicht in der Klasse sondern im main() muss ich
    exit-en sonst hängt das Programm.

    public static void main(String[] args) {
    new Example();
    System.exit();
    }

    Passiert auf Ubuntu Java Version 1.6.0_14
    Ich denke der exit bricht den Thread ab. Sonst wartet die JVM bis sich der Thread selbst beendet (was er nicht tut).

  10. Stefan Kiesel | 15. Dezember 2009 um 13:01 | Permalink

    Kann ich leider nicht nachvollziehen, hab auch leider keine installierte Ubuntu-Version. Nur damit es zu keinen Missverständnissen kommt: Das zuerst gestartete Programm soll bis zu einem manuellen Abbruch durchlaufen. Alle weiteren Instanzen beenden sich sofort nach dem Start und dem Austausch der Nachrichten selbst.

    Grüße
    Stefan

Kommentar verfassen

Dein E-Mail wird nicht veröffentlicht oder weitergegeben. Pflichtfelder sind mit * markiert.