Wie implementiere ich eine Java-Anwendung mit einer einzelnen Instanz?


89

Manchmal sehe ich viele Anwendungen wie msn, Windows Media Player usw., die Einzelinstanzanwendungen sind (wenn der Benutzer ausgeführt wird, während die Anwendung ausgeführt wird, wird keine neue Anwendungsinstanz erstellt).

In C # verwende ich dafür die MutexKlasse, aber ich weiß nicht, wie ich das in Java machen soll.


Ein sehr einfacher Ansatz mit Java NIO siehe vollständiges Beispiel stackoverflow.com/a/20015771/185022
AZ_

Antworten:


62

Wenn ich diesen Artikel glaube , von:

Bei der ersten Instanz wird versucht, einen Listening-Socket auf der localhost-Schnittstelle zu öffnen. Wenn der Socket geöffnet werden kann, wird davon ausgegangen, dass dies die erste Instanz der zu startenden Anwendung ist. Wenn nicht, wird davon ausgegangen, dass eine Instanz dieser Anwendung bereits ausgeführt wird. Die neue Instanz muss die vorhandene Instanz benachrichtigen, dass ein Start versucht wurde, und dann beenden. Die vorhandene Instanz übernimmt nach Erhalt der Benachrichtigung und löst ein Ereignis an den Listener aus, der die Aktion verarbeitet.

Hinweis: Ahe erwähnt im Kommentar, dass die Verwendung InetAddress.getLocalHost()schwierig sein kann:

  • In der DHCP-Umgebung funktioniert dies nicht wie erwartet, da die zurückgegebene Adresse davon abhängt, ob der Computer über Netzwerkzugriff verfügt.
    Die Lösung bestand darin, die Verbindung zu öffnen InetAddress.getByAddress(new byte[] {127, 0, 0, 1});
    Wahrscheinlich im Zusammenhang mit Fehler 4435662 .
  • Ich habe auch den Fehler 4665037 gefunden, der als erwartete Ergebnisse Folgendes meldet getLocalHost: IP-Adresse des Computers zurückgeben, vs. Tatsächliche Ergebnisse: Rückgabe 127.0.0.1.

Es ist überraschend haben getLocalHostRückkehr 127.0.0.1auf Linux , aber nicht unter Windows.


Oder Sie können ManagementFactoryObjekt verwenden. Wie hier erklärt :

Die getMonitoredVMs(int processPid)Methode empfängt als Parameter die aktuelle Anwendungs-PID und fängt den Anwendungsnamen ab, der über die Befehlszeile aufgerufen wird. Beispielsweise wurde die Anwendung über den c:\java\app\test.jarPfad gestartet , und die Wertvariable lautet " c:\\java\\app\\test.jar". Auf diese Weise erfassen wir nur den Anwendungsnamen in der Zeile 17 des folgenden Codes.
Danach durchsuchen wir JVM nach einem anderen Prozess mit demselben Namen. Wenn wir ihn gefunden haben und die Anwendungs-PID unterschiedlich ist, bedeutet dies, dass dies die zweite Anwendungsinstanz ist.

JNLP bietet auch eine SingleInstanceListener


3
Beachten Sie, dass die erste Lösung einen Fehler aufweist. Wir haben kürzlich festgestellt, dass InetAddress.getLocalHost()dies in der DHCP-Umgebung nicht wie erwartet funktioniert, da die zurückgegebene Adresse davon abhängt, ob der Computer über Netzwerkzugriff verfügt. Die Lösung bestand darin, die Verbindung mit zu öffnen InetAddress.getByAddress(new byte[] {127, 0, 0, 1});.
Ahe

2
@Ahe: ausgezeichneter Punkt. Ich habe Ihren Kommentar sowie Referenzen zu Oracle-Sun-Fehlerberichten in meine bearbeitete Antwort aufgenommen.
VonC

3
Laut JavaDoc wird InetAddress.getByName(null)die Adresse der Loopback- Schnittstelle zurückgegeben. Ich denke, dies ist besser als die manuelle Angabe von 127.0.0.1, da dies theoretisch auch in Nur-IPv6-Umgebungen funktionieren sollte.
Kayahr


1
@Puce Sicher, kein Problem: Ich habe diese Links wiederhergestellt.
VonC

65

Ich verwende die folgende Methode in der Hauptmethode. Dies ist die einfachste, robusteste und am wenigsten aufdringliche Methode, die ich je gesehen habe. Ich dachte, ich würde sie teilen.

private static boolean lockInstance(final String lockFile) {
    try {
        final File file = new File(lockFile);
        final RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
        final FileLock fileLock = randomAccessFile.getChannel().tryLock();
        if (fileLock != null) {
            Runtime.getRuntime().addShutdownHook(new Thread() {
                public void run() {
                    try {
                        fileLock.release();
                        randomAccessFile.close();
                        file.delete();
                    } catch (Exception e) {
                        log.error("Unable to remove lock file: " + lockFile, e);
                    }
                }
            });
            return true;
        }
    } catch (Exception e) {
        log.error("Unable to create and/or lock file: " + lockFile, e);
    }
    return false;
}

Was sollte der Parameter "lockFile" für eine Desktop-Anwendung sein? der Name der Anwendungs-JAR-Datei? Wie wäre es, wenn es keine JAR-Datei gibt, nur einige Klassendateien?
5YrsLaterDBA

2
Ist es wirklich notwendig, die Dateisperre manuell aufzuheben und die Datei beim Herunterfahren zu schließen? Geschieht dies nicht automatisch, wenn der Prozess abbricht?
Natix

5
Aber was passiert, wenn die Stromversorgung ausfällt und der Computer herunterfährt, ohne den Abschalthaken auszuführen? Die Datei bleibt bestehen und die Anwendung kann nicht gestartet werden.
Petr Hudeček

6
@ PetrHudeček Es ist in Ordnung. Unabhängig davon, wie die Anwendung endet, wird die Dateisperre aufgehoben. Wenn es sich nicht um ein ordnungsgemäßes Herunterfahren handelte, hat dies sogar den Vorteil, dass die Anwendung dies beim nächsten Durchlauf erkennen kann. Auf jeden Fall: Auf die Sperre kommt es an, nicht auf das Vorhandensein der Datei. Wenn die Datei noch vorhanden ist, wird die Anwendung trotzdem gestartet.
Dreamspace Präsident

@ Robert: Danke für deine Lösung, ich benutze sie seitdem. Und gerade jetzt habe ich es erweitert, um auch mit der bereits vorhandenen Instanz zu kommunizieren, die eine andere Instanz zu starten versucht hat - mithilfe des Ordners WatchService! stackoverflow.com/a/36772436/3500521
Dreamspace President

9

Wenn die App. hat eine GUI, starte sie mit JWS und benutze die SingleInstanceService.

Aktualisieren

Das Java-Plug-In (sowohl für Applets als auch für JWS-Apps erforderlich) wurde von Oracle nicht mehr unterstützt und aus dem JDK entfernt. Browserhersteller hatten es bereits aus ihren Browsern entfernt.

Diese Antwort ist also nicht mehr gültig. Lassen Sie es nur hier, um Leute zu warnen, die sich alte Unterlagen ansehen.


2
Beachten Sie auch, dass die laufende Instanz anscheinend über neue Instanzen und deren Argumente informiert werden kann, was die Kommunikation mit einem solchen Programm erleichtert.
Thorbjørn Ravn Andersen

6

Ja, dies ist eine wirklich anständige Antwort für die Eclipse RCP Eclipse-Einzelinstanzanwendung unten ist mein Code

in application.java

if(!isFileshipAlreadyRunning()){
        MessageDialog.openError(display.getActiveShell(), "Fileship already running", "Another instance of this application is already running.  Exiting.");
        return IApplication.EXIT_OK;
    } 


private static boolean isFileshipAlreadyRunning() {
    // socket concept is shown at http://www.rbgrn.net/content/43-java-single-application-instance
    // but this one is really great
    try {
        final File file = new File("FileshipReserved.txt");
        final RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
        final FileLock fileLock = randomAccessFile.getChannel().tryLock();
        if (fileLock != null) {
            Runtime.getRuntime().addShutdownHook(new Thread() {
                public void run() {
                    try {
                        fileLock.release();
                        randomAccessFile.close();
                        file.delete();
                    } catch (Exception e) {
                        //log.error("Unable to remove lock file: " + lockFile, e);
                    }
                }
            });
            return true;
        }
    } catch (Exception e) {
       // log.error("Unable to create and/or lock file: " + lockFile, e);
    }
    return false;
}

5

Wir verwenden hierfür die Dateisperrung (eine exklusive Sperre für eine magische Datei im App-Datenverzeichnis des Benutzers), sind jedoch in erster Linie daran interessiert, zu verhindern, dass mehrere Instanzen jemals ausgeführt werden.

Wenn Sie versuchen, die zweite Instanz Befehlszeilenargumente usw. an die erste Instanz übergeben zu lassen, werden durch die Verwendung einer Socket-Verbindung auf localhost zwei Fliegen mit einer Klappe geschlagen. Allgemeiner Algorithmus:

  • Versuchen Sie beim Start, den Listener an Port XXXX auf localhost zu öffnen
  • Wenn dies fehlschlägt, öffnen Sie einen Writer für diesen Port auf localhost und senden Sie die Befehlszeilenargumente. Fahren Sie dann herunter
  • Andernfalls lauschen Sie Port XXXXX auf localhost. Wenn Sie Befehlszeilenargumente empfangen, verarbeiten Sie diese so, als ob die App mit dieser Befehlszeile gestartet wurde.

5

Ich habe eine Lösung gefunden, eine etwas karikaturistische Erklärung, funktioniert aber in den meisten Fällen immer noch. Es verwendet die einfache alte Sperrdatei, um Dinge zu erstellen, aber in einer ganz anderen Ansicht:

http://javalandscape.blogspot.com/2008/07/single-instance-from-your-application.html

Ich denke, es wird eine Hilfe für diejenigen mit einer strengen Firewall-Einstellung sein.


Ja, es ist ein guter Weg, da die Sperre freigegeben wird, wenn die Anwendung abstürzt oder so :)
LE GALL Benoît

5

Sie können die JUnique-Bibliothek verwenden. Es bietet Unterstützung für die Ausführung einer Java-Anwendung mit einer Instanz und ist Open Source.

http://www.sauronsoftware.it/projects/junique/

Die JUnique-Bibliothek kann verwendet werden, um zu verhindern, dass ein Benutzer gleichzeitig mehr Instanzen derselben Java-Anwendung ausführt.

JUnique implementiert Sperren und Kommunikationskanäle, die von allen JVM-Instanzen gemeinsam genutzt werden, die von demselben Benutzer gestartet wurden.

public static void main(String[] args) {
    String appId = "myapplicationid";
    boolean alreadyRunning;
    try {
        JUnique.acquireLock(appId, new MessageHandler() {
            public String handle(String message) {
                // A brand new argument received! Handle it!
                return null;
            }
        });
        alreadyRunning = false;
    } catch (AlreadyLockedException e) {
        alreadyRunning = true;
    }
    if (!alreadyRunning) {
        // Start sequence here
    } else {
        for (int i = 0; i < args.length; i++) {
            JUnique.sendMessage(appId, args[0]));
        }
    }
}

Unter der Haube werden Dateisperren im Ordner% USER_DATA% /. Junique erstellt und ein Server-Socket am zufälligen Port für jede eindeutige Anwendungs-ID erstellt, mit der Nachrichten zwischen Java-Anwendungen gesendet / empfangen werden können.


Kann ich damit mehrere Instanzen von Java-Anwendungen in einem Netzwerk verhindern? aka, nur eine Instanz meiner Anwendung ist in meinem gesamten Netzwerk erlaubt
Wuaner



2

Sie können versuchen, die Einstellungs-API zu verwenden. Es ist plattformunabhängig.


Ich mag diese Idee, da die API einfach ist, aber vielleicht möchten einige Virenscanner nicht, dass Sie die Registrierung ändern, sodass Sie ähnliche Probleme wie bei der Verwendung von RMI auf Systemen mit einer Software-Firewall haben ... nicht sicher.
Cal

@Cal Aber das gleiche Problem ist mit dem Ändern / Sperren von Dateien / etc ... meinst du nicht?
Alex

2

Eine allgemeinere Möglichkeit, die Anzahl der Instanzen auf einem einzelnen Computer oder sogar einem gesamten Netzwerk zu begrenzen, ist die Verwendung eines Multicast-Sockets.

Mithilfe eines Multicast-Sockets können Sie eine Nachricht an eine beliebige Anzahl von Instanzen Ihrer Anwendung senden, von denen sich einige auf physisch entfernten Computern in einem Unternehmensnetzwerk befinden können.

Auf diese Weise können Sie viele Arten von Konfigurationen aktivieren, um Dinge wie zu steuern

  • Eine oder mehrere Instanzen pro Maschine
  • Eine oder mehrere Instanzen pro Netzwerk (z. B. Steuern von Installationen auf einem Client-Standort)

Die Multicast-Unterstützung von Java erfolgt über das Paket java.net, wobei MulticastSocket & DatagramSocket die Hauptwerkzeuge sind.

Hinweis : MulticastSockets garantieren keine Zustellung von Datenpaketen. Verwenden Sie daher ein Tool, das auf Multicast-Sockets wie JGroups basiert . JGroups tut Garantie Lieferung aller Daten. Es ist eine einzelne JAR-Datei mit einer sehr einfachen API.

JGroups gibt es schon seit einiger Zeit und hat einige beeindruckende Anwendungen in der Industrie, zum Beispiel unterstützt es den Clustering-Mechanismus von JBoss, Daten an alle Instanzen eines Clusters zu senden.

Die Verwendung von JGroups und die Begrenzung der Anzahl der Instanzen einer App (auf einem Computer oder in einem Netzwerk, z. B. auf die Anzahl der Lizenzen, die ein Kunde gekauft hat) ist konzeptionell sehr einfach:

  • Beim Start Ihrer Anwendung versucht jede Instanz, einer benannten Gruppe beizutreten, z. B. "My Great App Group". Sie haben diese Gruppe so konfiguriert, dass 0, 1 oder N Mitglieder zugelassen werden
  • Wenn die Anzahl der Gruppenmitglieder größer ist als die, die Sie dafür konfiguriert haben, sollte Ihre App den Start verweigern.

1

Sie können eine Speicherzuordnungsdatei öffnen und dann prüfen, ob diese Datei bereits geöffnet ist. Wenn es bereits geöffnet ist, können Sie von main zurückkehren.

Andere Möglichkeiten sind die Verwendung von Sperrdateien (Standard-Unix-Praxis). Eine weitere Möglichkeit besteht darin, etwas in die Zwischenablage zu legen, wenn main startet, nachdem überprüft wurde, ob sich bereits etwas in der Zwischenablage befindet.

Andernfalls können Sie einen Socket in einem Listen-Modus (ServerSocket) öffnen. Versuchen Sie zunächst, eine Verbindung zum Socket herzustellen. Wenn Sie keine Verbindung herstellen können, öffnen Sie einen Serversocket. Wenn Sie eine Verbindung herstellen, wissen Sie, dass bereits eine andere Instanz ausgeführt wird.

So kann so ziemlich jede Systemressource verwendet werden, um zu wissen, dass eine App ausgeführt wird.

BR, ~ A.


Haben Sie Code für eine dieser Ideen? Was ist auch, wenn ich möchte, dass der Benutzer beim Starten einer neuen Instanz alle vorherigen schließt?
Android-Entwickler

1

Ich habe dafür Sockets verwendet und je nachdem, ob sich die Anwendung auf der Client- oder der Serverseite befindet, ist das Verhalten etwas anders:

  • Client-Seite: Wenn bereits eine Instanz vorhanden ist (ich kann einen bestimmten Port nicht abhören), übergebe ich die Anwendungsparameter und beende sie (möglicherweise möchten Sie in der vorherigen Instanz einige Aktionen ausführen), wenn nicht, starte ich die Anwendung.
  • Serverseite: Wenn bereits eine Instanz vorhanden ist, drucke ich eine Nachricht und beende sie. Wenn nicht, starte ich die Anwendung.

1
öffentliche Klasse SingleInstance {
    public static final String LOCK = System.getProperty ("user.home") + File.separator + "test.lock";
    public static final String PIPE = System.getProperty ("user.home") + File.separator + "test.pipe";
    privater statischer JFrame-Frame = null;

    public static void main (String [] args) {
        Versuchen {
            FileChannel lockChannel = new RandomAccessFile (LOCK, "rw"). GetChannel ();
            FileLock flk = null; 
            Versuchen {
                flk = lockChannel.tryLock ();
            } catch (Throwable t) {
                t.printStackTrace ();
            }}
            if (flk == null ||! flk.isValid ()) {
                System.out.println ("bereits ausgeführt, eine Nachricht an Pipe hinterlassen und beenden ...");
                FileChannel pipeChannel = null;
                Versuchen {
                    pipeChannel = new RandomAccessFile (PIPE, "rw"). getChannel ();
                    MappedByteBuffer bb = pipeChannel.map (FileChannel.MapMode.READ_WRITE, 0, 1);
                    bb.put (0, (Byte) 1);
                    bb.force ();
                } catch (Throwable t) {
                    t.printStackTrace ();
                } schließlich {
                    if (pipeChannel! = null) {
                        Versuchen {
                            pipeChannel.close ();
                        } catch (Throwable t) {
                            t.printStackTrace ();
                        }}
                    }} 
                }}
                System.exit (0);
            }}
            // Wir lösen die Sperre nicht und schließen den Kanal hier, 
            // Dies wird ausgeführt, nachdem die Anwendung normal abgestürzt oder geschlossen ist. 
            SwingUtilities.invokeLater (
                new Runnable () {
                    public void run () {
                        createAndShowGUI ();
                    }}
                }}
            );

            FileChannel pipeChannel = null;
            Versuchen {
                pipeChannel = new RandomAccessFile (PIPE, "rw"). getChannel ();
                MappedByteBuffer bb = pipeChannel.map (FileChannel.MapMode.READ_WRITE, 0, 1);
                while (wahr) {
                    Byte b = bb.get (0);
                    if (b> 0) {
                        bb.put (0, (Byte) 0);
                        bb.force ();
                        SwingUtilities.invokeLater (
                            new Runnable () {
                                public void run () {
                                    frame.setExtendedState (JFrame.NORMAL);
                                    frame.setAlwaysOnTop (true);
                                    frame.toFront ();
                                    frame.setAlwaysOnTop (false);
                                }}
                            }}
                        );
                    }}
                    Thread.sleep (1000);
                }}
            } catch (Throwable t) {
                t.printStackTrace ();
            } schließlich {
                if (pipeChannel! = null) {
                    Versuchen {
                        pipeChannel.close ();
                    } catch (Throwable t) {
                        t.printStackTrace ();
                    }} 
                }} 
            }}
        } catch (Throwable t) {
            t.printStackTrace ();
        }} 
    }}

    public static void createAndShowGUI () {

        frame = neuer JFrame ();
        frame.setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE);
        frame.setSize (800, 650);
        frame.getContentPane (). add (neues JLabel ("MAIN WINDOW", 
                    SwingConstants.CENTER), BorderLayout.CENTER);
        frame.setLocationRelativeTo (null);
        frame.setVisible (true);
    }}
}}


1

BEARBEITEN : Anstatt diesen WatchService-Ansatz zu verwenden, kann ein einfacher 1-Sekunden-Timer-Thread verwendet werden, um zu überprüfen, ob die Datei indicFile.exists () vorhanden ist. Löschen Sie es und bringen Sie die Anwendung zu Front ().

EDIT : Ich würde gerne wissen, warum dies abgelehnt wurde. Es ist die beste Lösung, die ich bisher gesehen habe. Beispielsweise schlägt der Server-Socket-Ansatz fehl, wenn eine andere Anwendung den Port bereits überwacht.

Laden Sie einfach Microsoft Windows Sysinternals TCPView herunter (oder verwenden Sie netstat), starten Sie es, sortieren Sie es nach "State", suchen Sie nach dem Zeilenblock "LISTENING", wählen Sie einen aus, dessen Remote-Adresse den Namen Ihres Computers enthält, und stecken Sie diesen Port in Ihren neuen Socket ()-Lösung. Bei meiner Implementierung kann ich jedes Mal einen Fehler verursachen. Und es ist logisch , weil es die Grundlage des Ansatzes ist. Oder was bekomme ich nicht darüber, wie ich das umsetzen soll?

Bitte informieren Sie mich, wenn und wie ich mich irre!

Meiner Ansicht nach - und ich bitte Sie, dies nach Möglichkeit zu widerlegen - wird den Entwicklern empfohlen, einen Ansatz im Produktionscode zu verwenden, der in mindestens einem von etwa 60000 Fällen fehlschlägt. Und wenn diese Ansicht richtig ist, kann es absolut nicht sein, dass eine vorgestellte Lösung, die dieses Problem nicht aufweist, wegen ihrer Codemenge herabgestuft und kritisiert wird.

Nachteile des Socket-Ansatzes im Vergleich:

  • Schlägt fehl, wenn der falsche Lottoschein (Portnummer) gewählt wird.
  • Fehler in einer Mehrbenutzerumgebung: Nur ein Benutzer kann die Anwendung gleichzeitig ausführen. (Mein Ansatz müsste leicht geändert werden, um die Datei (en) im Benutzerbaum zu erstellen, aber das ist trivial.)
  • Schlägt fehl, wenn die Firewall-Regeln zu streng sind.
  • Verdächtige Benutzer (die ich in freier Wildbahn getroffen habe) fragen sich, welche Spielereien Sie vorhaben, wenn Ihr Texteditor einen Server-Socket beansprucht.

Ich hatte gerade eine gute Idee, wie ich das Java-Kommunikationsproblem zwischen neuer Instanz und vorhandener Instanz so lösen kann, dass es auf jedem System funktioniert. Also habe ich diese Klasse in ungefähr zwei Stunden ausgepeitscht. Funktioniert wie ein Zauber: D.

Es basiert auf Roberts Dateisperrungsansatz (auch auf dieser Seite), den ich seitdem verwendet habe. Um der bereits laufenden Instanz mitzuteilen, dass eine andere Instanz versucht hat zu starten (aber nicht) ... wird eine Datei erstellt und sofort gelöscht, und die erste Instanz verwendet den WatchService, um diese Änderung des Ordnerinhalts zu erkennen. Ich kann nicht glauben, dass dies anscheinend eine neue Idee ist, wenn man bedenkt, wie grundlegend das Problem ist.

Dies kann leicht geändert werden, um nur die Datei zu erstellen und nicht zu löschen, und dann können Informationen darin eingefügt werden, die die richtige Instanz auswerten kann, z. B. die Befehlszeilenargumente - und die richtige Instanz kann dann das Löschen durchführen. Persönlich musste ich nur wissen, wann ich das Fenster meiner Anwendung wiederherstellen und an die Vorderseite senden musste.

Anwendungsbeispiel:

public static void main(final String[] args) {

    // ENSURE SINGLE INSTANCE
    if (!SingleInstanceChecker.INSTANCE.isOnlyInstance(Main::otherInstanceTriedToLaunch, false)) {
        System.exit(0);
    }

    // launch rest of application here
    System.out.println("Application starts properly because it's the only instance.");
}

private static void otherInstanceTriedToLaunch() {
    // Restore your application window and bring it to front.
    // But make sure your situation is apt: This method could be called at *any* time.
    System.err.println("Deiconified because other instance tried to start.");
}

Hier ist die Klasse:

package yourpackagehere;

import javax.swing.*;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileLock;
import java.nio.file.*;




/**
 * SingleInstanceChecker v[(2), 2016-04-22 08:00 UTC] by dreamspace-president.com
 * <p>
 * (file lock single instance solution by Robert https://stackoverflow.com/a/2002948/3500521)
 */
public enum SingleInstanceChecker {

    INSTANCE; // HAHA! The CONFUSION!


    final public static int POLLINTERVAL = 1000;
    final public static File LOCKFILE = new File("SINGLE_INSTANCE_LOCKFILE");
    final public static File DETECTFILE = new File("EXTRA_INSTANCE_DETECTFILE");


    private boolean hasBeenUsedAlready = false;


    private WatchService watchService = null;
    private RandomAccessFile randomAccessFileForLock = null;
    private FileLock fileLock = null;


    /**
     * CAN ONLY BE CALLED ONCE.
     * <p>
     * Assumes that the program will close if FALSE is returned: The other-instance-tries-to-launch listener is not
     * installed in that case.
     * <p>
     * Checks if another instance is already running (temp file lock / shutdownhook). Depending on the accessibility of
     * the temp file the return value will be true or false. This approach even works even if the virtual machine
     * process gets killed. On the next run, the program can even detect if it has shut down irregularly, because then
     * the file will still exist. (Thanks to Robert https://stackoverflow.com/a/2002948/3500521 for that solution!)
     * <p>
     * Additionally, the method checks if another instance tries to start. In a crappy way, because as awesome as Java
     * is, it lacks some fundamental features. Don't worry, it has only been 25 years, it'll sure come eventually.
     *
     * @param codeToRunIfOtherInstanceTriesToStart Can be null. If not null and another instance tries to start (which
     *                                             changes the detect-file), the code will be executed. Could be used to
     *                                             bring the current (=old=only) instance to front. If null, then the
     *                                             watcher will not be installed at all, nor will the trigger file be
     *                                             created. (Null means that you just don't want to make use of this
     *                                             half of the class' purpose, but then you would be better advised to
     *                                             just use the 24 line method by Robert.)
     *                                             <p>
     *                                             BE CAREFUL with the code: It will potentially be called until the
     *                                             very last moment of the program's existence, so if you e.g. have a
     *                                             shutdown procedure or a window that would be brought to front, check
     *                                             if the procedure has not been triggered yet or if the window still
     *                                             exists / hasn't been disposed of yet. Or edit this class to be more
     *                                             comfortable. This would e.g. allow you to remove some crappy
     *                                             comments. Attribution would be nice, though.
     * @param executeOnAWTEventDispatchThread      Convenience function. If false, the code will just be executed. If
     *                                             true, it will be detected if we're currently on that thread. If so,
     *                                             the code will just be executed. If not so, the code will be run via
     *                                             SwingUtilities.invokeLater().
     * @return if this is the only instance
     */
    public boolean isOnlyInstance(final Runnable codeToRunIfOtherInstanceTriesToStart, final boolean executeOnAWTEventDispatchThread) {

        if (hasBeenUsedAlready) {
            throw new IllegalStateException("This class/method can only be used once, which kinda makes sense if you think about it.");
        }
        hasBeenUsedAlready = true;

        final boolean ret = canLockFileBeCreatedAndLocked();

        if (codeToRunIfOtherInstanceTriesToStart != null) {
            if (ret) {
                // Only if this is the only instance, it makes sense to install a watcher for additional instances.
                installOtherInstanceLaunchAttemptWatcher(codeToRunIfOtherInstanceTriesToStart, executeOnAWTEventDispatchThread);
            } else {
                // Only if this is NOT the only instance, it makes sense to create&delete the trigger file that will effect notification of the other instance.
                //
                // Regarding "codeToRunIfOtherInstanceTriesToStart != null":
                // While creation/deletion of the file concerns THE OTHER instance of the program,
                // making it dependent on the call made in THIS instance makes sense
                // because the code executed is probably the same.
                createAndDeleteOtherInstanceWatcherTriggerFile();
            }
        }

        optionallyInstallShutdownHookThatCleansEverythingUp();

        return ret;
    }


    private void createAndDeleteOtherInstanceWatcherTriggerFile() {

        try {
            final RandomAccessFile randomAccessFileForDetection = new RandomAccessFile(DETECTFILE, "rw");
            randomAccessFileForDetection.close();
            Files.deleteIfExists(DETECTFILE.toPath()); // File is created and then instantly deleted. Not a problem for the WatchService :)
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    private boolean canLockFileBeCreatedAndLocked() {

        try {
            randomAccessFileForLock = new RandomAccessFile(LOCKFILE, "rw");
            fileLock = randomAccessFileForLock.getChannel().tryLock();
            return fileLock != null;
        } catch (Exception e) {
            return false;
        }
    }


    private void installOtherInstanceLaunchAttemptWatcher(final Runnable codeToRunIfOtherInstanceTriesToStart, final boolean executeOnAWTEventDispatchThread) {

        // PREPARE WATCHSERVICE AND STUFF
        try {
            watchService = FileSystems.getDefault().newWatchService();
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }
        final File appFolder = new File("").getAbsoluteFile(); // points to current folder
        final Path appFolderWatchable = appFolder.toPath();


        // REGISTER CURRENT FOLDER FOR WATCHING FOR FILE DELETIONS
        try {
            appFolderWatchable.register(watchService, StandardWatchEventKinds.ENTRY_DELETE);
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }


        // INSTALL WATCHER THAT LOOKS IF OUR detectFile SHOWS UP IN THE DIRECTORY CHANGES. IF THERE'S A CHANGE, ANOTHER INSTANCE TRIED TO START, SO NOTIFY THE CURRENT ONE OF THAT.
        final Thread t = new Thread(() -> watchForDirectoryChangesOnExtraThread(codeToRunIfOtherInstanceTriesToStart, executeOnAWTEventDispatchThread));
        t.setDaemon(true);
        t.setName("directory content change watcher");
        t.start();
    }


    private void optionallyInstallShutdownHookThatCleansEverythingUp() {

        if (fileLock == null && randomAccessFileForLock == null && watchService == null) {
            return;
        }

        final Thread shutdownHookThread = new Thread(() -> {
            try {
                if (fileLock != null) {
                    fileLock.release();
                }
                if (randomAccessFileForLock != null) {
                    randomAccessFileForLock.close();
                }
                Files.deleteIfExists(LOCKFILE.toPath());
            } catch (Exception ignore) {
            }
            if (watchService != null) {
                try {
                    watchService.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
        Runtime.getRuntime().addShutdownHook(shutdownHookThread);
    }


    private void watchForDirectoryChangesOnExtraThread(final Runnable codeToRunIfOtherInstanceTriesToStart, final boolean executeOnAWTEventDispatchThread) {

        while (true) { // To eternity and beyond! Until the universe shuts down. (Should be a volatile boolean, but this class only has absolutely required features.)

            try {
                Thread.sleep(POLLINTERVAL);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }


            final WatchKey wk;
            try {
                wk = watchService.poll();
            } catch (ClosedWatchServiceException e) {
                // This situation would be normal if the watcher has been closed, but our application never does that.
                e.printStackTrace();
                return;
            }

            if (wk == null || !wk.isValid()) {
                continue;
            }


            for (WatchEvent<?> we : wk.pollEvents()) {

                final WatchEvent.Kind<?> kind = we.kind();
                if (kind == StandardWatchEventKinds.OVERFLOW) {
                    System.err.println("OVERFLOW of directory change events!");
                    continue;
                }


                final WatchEvent<Path> watchEvent = (WatchEvent<Path>) we;
                final File file = watchEvent.context().toFile();


                if (file.equals(DETECTFILE)) {

                    if (!executeOnAWTEventDispatchThread || SwingUtilities.isEventDispatchThread()) {
                        codeToRunIfOtherInstanceTriesToStart.run();
                    } else {
                        SwingUtilities.invokeLater(codeToRunIfOtherInstanceTriesToStart);
                    }

                    break;

                } else {
                    System.err.println("THIS IS THE FILE THAT WAS DELETED: " + file);
                }

            }

            wk.reset();
        }
    }

}

Sie benötigen nicht Hunderte von Codezeilen, um dieses Problem zu lösen. new ServerSocket()mit einem Fangblock ist völlig ausreichend,
Marquis von Lorne

@EJP Beziehen Sie sich auf die akzeptierte Antwort oder wovon sprechen Sie? Ich habe ziemlich viel nach einer x-Plattform-Lösung ohne zusätzliche Bibliothek gesucht, die nicht fehlschlägt, z. B. weil ein Socket zufällig bereits von einer anderen Anwendung belegt war. Wenn es eine Lösung dafür gibt - besonders so einfach, wie Sie es meinen -, dann würde ich gerne davon erfahren.
Dreamspace Präsident

@EJP: Ich möchte noch einmal fragen , 1) welche triviale Lösung Sie meinen, dass Sie wie eine Karotte vor meinem Kopf baumeln, 2) falls es sich um die Socket-Lösung handelt, mit der die akzeptierte Antwort beginnt, wenn eine oder mehrere Es gelten meine Aufzählungspunkte "Nachteile des Socket-Ansatzes", und 3) wenn ja, warum würden Sie diesen Ansatz trotz dieser Mängel immer noch einem Ansatz wie meinem empfehlen?
Dreamspace Präsident

@EJP: Das Problem ist, dass Ihre Stimme, wie Sie sicher wissen, einiges an Gewicht hat, aber alle Beweise, die ich habe, zwingen mich zu der Überzeugung, dass Ihr Rat hier falsch ist. Ich bestehe nicht darauf, dass meine Lösung richtig ist und das alles, aber ich bin eine evidenzbasierte Maschine. Sehen Sie nicht, dass Ihre Position Ihnen gegenüber der Community die Verantwortung gibt, die fehlenden Puzzleteile dieser Kommunikation, die Sie begonnen haben , auszufüllen ?
Dreamspace Präsident

@EJP: Da Sie leider nicht reagiert haben, gehe ich als Tatsache davon aus: Die Wahrheit über die Server-Socket-Lösung ist, dass sie wirklich sehr fehlerhaft ist , und der Grund für die meisten, die sie gewählt haben, könnte "Die anderen" gewesen sein benutze dies auch. ", oder sie könnten von verantwortungslosen Menschen getäuscht worden sein, es zu benutzen. Ich denke , ein Teil des Grundes, warum Sie uns nicht mit den erforderlichen Erklärungen ausgezeichnet haben, ist, dass Sie das nicht verstehen können / warum Sie diesen Ansatz nie in Frage gestellt haben und keine öffentliche Erklärung abgeben möchten, die dies offenbart.
Dreamspace Präsident

0

Zu diesem Zweck habe ich eine Java-Bibliothek namens Unique4j geschrieben. Sie können es unter https://github.com/prat-man/unique4j sehen . Die Bibliothek ist auch in Maven Central für einfachen Zugriff und Integration verfügbar.

Es ist plattformübergreifend kompatibel und unterstützt Java 1.6+. Die Bibliothek ist unter Apache 2.0 lizenziert und eignet sich daher ideal für alle Open-Source- und Closed-Source-Projekte.

Es verwendet eine Kombination aus Dateisperren und dynamischen Port-Sperren, um Instanzen zu erkennen und zwischen ihnen zu kommunizieren, mit dem primären Ziel, nur eine Instanz ausführen zu lassen.

Im Folgenden finden Sie ein einfaches Beispiel für die Verwendung und die Funktionen von Unique4j. Die Bibliothek bietet mehr Anpassungen und Funktionen, die für mehr Features und Funktionen überschrieben werden können.

import tk.pratanumandal.unique4j.Unique;
import tk.pratanumandal.unique4j.exception.Unique4jException;

public class Unique4jDemo {

    // unique application ID
    public static String APP_ID = "tk.pratanumandal.unique4j-mlsdvo-20191511-#j.6";

    public static void main(String[] args) throws Unique4jException {
        // create unique instance
        Unique unique = new Unique(APP_ID) {
            @Override
            public void receiveMessage(String message) {
                // display received message from subsequent instance
                System.out.println(message);
            }

            @Override
            public String sendMessage() {
                // send message to first instance
                return "Hello World!";
            }
        };

        // try to obtain lock
        boolean lockFlag = unique.acquireLock();

        // perform long running tasks here
        ...

        // try to free the lock before exiting program
        boolean lockFreeFlag = unique.freeLock();
    }
}

Ich habe es für eines meiner eigenen Projekte erstellt und es dann als Bibliothek veröffentlicht. Hoffe, jemand findet es nützlich.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.