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
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.
Hallo Illuvatar,
vielen Dank für deinen Beitrag. Ich habe die
tryLockMethode der KlasseFileChannelnicht angesprochen, da das Verhalten vonFileLocksehr 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
Super Tutorial!
Sowas hätte ich denke ich kaum zu Stande gebracht, zumindest nicht so genau
Gruß,
pcworld
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.
Hi Stefan,
ein solches Problem ist mir nicht bekannt. Ich habe den Quellcode aber einmal ausgebessert. Anstatt von
getCanonicalHostName().equals("localhost")wird jetzt einfachisLoopbackAddress()geschrieben.Über eine Rückmeldung, ob es jetzt funktioniert oder nicht, würde ich mich freuen.
Gruß
Stefan
Hi Stefan,
ja. Das klappt jetzt auch unter Vista.
Danke und schöne Grüße,
Stefan
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)
Hallo Stefan,
ich kann leider gerade nicht nachvollziehen, wovon Sie reden. Das
System.exit(0);in derExampleKlasse kann auch ohne Probleme weggelassen werden.Grüße
Stefan
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).
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