Instance unique d'application en java
Par
Romain Vimont (®om) (home)
I. Introduction
II. Solutions
II-A.
II-A-1. Fichier existant
II-A-2. Fichier verrouillé
II-B. Socket
III. Mise en œuvre
III-A. Besoins
III-B. Code source
III-C. Exemple d'utilisation
I. Introduction
Comment empêcher de lancer plusieurs fois simultanément une application java?
Par exemple, vous venez de programmer un serveur, vous voulez vous assurer qu'une seule instance est lancée, pour éviter de provoquer des comportements inattendus (une instance reçoit une requête d'un client, et l'autre reçoit son identifiant).
En général, quelque soit le programme, il peut être pratique d'interdire plusieurs lancements de la même application...
C'est à cette question que cet article se propose de répondre.
 |
Pour ceux qui se fichent du fonctionnement, mais qui veulent pouvoir utiliser la solution mise en œuvre, allez directement prendre le code source à la fin de ce tutoriel...
|
 |
Il ne faut pas confondre instance unique d'une application et instance unique d'une classe, cela n'a rien à voir. Une instance unique de classe peut être assurée en utilisant le design pattern Singleton :
|
public final class MonSingleton {
private static final MonSingleton instance = new MonSingleton();
private MonSingleton() {
}
public static MonSingleton getMonSingleton() {
return instance;
}
} |
II. Solutions
Reformulons le problème : pour empêcher plusieurs instances d'être lancées simultanément, il suffit, lors du démarrage, de vérifier qu'aucune autre est déjà lancée. Le problème est alors "comment vérifier qu'aucune autre n'est déjà lancée?" ; pour cela, il leur faut un moyen de communication.
 |
Nous n'aborderons pas ici d'éventuelles solutions qui ne seraient pas multiplateforme : notre solution doit fonctionner aussi bien sous Windows que sous Linux et Mac...
|
II-A.
II-A-1. Fichier existant
La première solution qui vient à l'esprit, c'est de laisser une empreinte (par exemple un fichier) lorsque l'application démarre, et de la supprimer lorsque l'application se termine. Cette solution semble intéressante, mais elle pose un problème : peut-on supprimer l'empreinte quelque soit la manière dont l'application est quittée?
Si l'utilisateur ferme la fenêtre, pas de problèmes
a priori, il suffit de lancer une méthode qui supprime l'empreinte. Pourtant, sous MacOS, cela pose un problème : selon la manière dont l'utilisateur ferme la fenêtre, soit cela fonctionne correctement, soit l'application est tout simplement tuée. Et lorsque l'application est tuée (quelque soit le système d'exploitation), et bien on n'exécute pas du code grâce à un écouteur de fenêtre...
Vous allez me dire, et vous aurez raison, on n'a qu'à ajouter un
hook (voir
ici) pour exécuter du code lorsque l'application est tuée (à condition que ce code soit rapide à exécuter).
Mais bon, l'argument qui balaye tout, c'est
"et s'il y a une panne de courant?",
"et si l'ordinateur plante lamentablement (blue screen sous Windows) ?". Là, vous pouvez mettre un
hook ou tout ce que vous voulez, aucun code ne sera exécuté. Vous me direz, ça n'arrive pas si souvent... Mais lorsque ça arrive, il ne vous sera plus jamais possible de lancer l'application (une autre est toujours "lancée"). Si c'est pour vous, ça va, vous savez qu'il suffit de supprimer le fichier. Si c'est pour distribuer à des clients...
II-A-2. Fichier verrouillé
La solution en utilisant l'existence d'un fichier n'est donc pas sûre. Cependant, on peut utiliser un fichier différemment pour résoudre notre problème : poser un verrou dessus. Cette solution fonctionne très bien, elle a l'avantage d'être extrêmement simple.
Même si ça n'est pas la méthode retenue dans cet article, elle peut très bien répondre à vos besoins.
Voici comment tester l'unicité de l'application en utilisant cette solution :
public boolean isUnique() {
boolean unique;
try {
unique = new FileOutputStream("lock").getChannel().tryLock() != null;
} catch(IOException ie) {
unique = false;
}
return unique;
} |
II-B. Socket
Si c'est la première fois que vous entendez parler de cette solution, vous allez sans doute bondir de votre siège (j'exagère), en pensant "une socket, pour faire ça !?!".
Et pourtant... Cette méthode est, d'après moi, la meilleure. Pourquoi ?
D'abord, il n'y a pas 36 façons pour faire communiquer deux programmes totalement dissociés tout en gardant la portabilité : a priori, les deux méthodes décrites dans cet article.
Ensuite, en utilisant des sockets, on peut effectuer une vraie communication. Par exemple, lorsqu'une deuxième instance de l'application essaye de démarrer, elle peut communiquer avec la première, pour lui demander de passer au premier-plan, ce que ne permet pas de faire la solution utilisant un verrou sur un fichier.
Par contre, cette méthode a bien sûr un inconvénient, elle utilise un port de communication, et si ce port est utilisé par une autre application, cela ne fonctionnera pas. Mais, si vous prenez au hasard un port entre 10000 et 65535, il y a très peu de chances qu'il soit déjà utilisé (et au pire, on peut implanter une configuration de ce port grâce à un fichier de configuration).
III. Mise en œuvre
Nous allons donc mettre en œuvre la méthode basée sur des sockets.
III-A. Besoins
De quoi a-t-on besoin pour cela?
Pour répondre à cette question, voyons ce que nous désirons obtenir. Nous voudrions pouvoir utiliser le gestionnaire d'instance unique très simplement, par exemple, dans un premier temps :
UniqueInstance uniqueInstance = new UniqueInstance(PORT);
if(uniqueInstance.uneMethodeIndiquantSiAucuneAutreInstanceEstDejaLancee()) {
application = new MonApplication();
} |
On a vu qu'avec des sockets, il était possible de signaler à l'instance déjà lancée qu'une autre avait essayé de démarrer. Donc on aimerait pouvoir passer en paramètres le message de communication (message à envoyer par l'application qui essaye de démarrer vers l'application déjà lancée) et les actions à effectuer en cas de réception de ce message.
Runnable actions = new Runnable() {
public void run() {
}
}
UniqueInstance uniqueInstance = new UniqueInstance(PORT, "monApplication", actions);
if(uniqueInstance.uneMethodeIndiquantSiAucuneAutreInstanceEstDejaLancee()) {
application = new MonApplication();
} |
III-B. Code source
Maintenant que l'on a vu ce que l'on désirait obtenir, je vous donne l'implantation que j'ai faite de la méthode basée sur des sockets, bien sûr totalement libre de droits. L'astuce principale de la méthode est que l'ouverture d'un ServerSocket sur un port déjà utilisé lève une exception.
 |
Ce code est aussi disponible en page html ici, et la javadoc est disponible ici.
|
import java.io.IOException;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.logging.Logger;
public class UniqueInstance {
private int port;
private String message;
private Runnable runOnReceive;
public UniqueInstance(int port, String message, Runnable runOnReceive) {
if (port == 0 || (port & 0xffff0000) != 0)
throw new IllegalArgumentException("Le port doit être compris entre 1 et 65535 : " + port + ".");
if (runOnReceive != null && message == null)
throw new IllegalArgumentException("runOnReceive != null ==> message == null.");
this.port = port;
this.message = message;
this.runOnReceive = runOnReceive;
}
public UniqueInstance(int port) {
this(port, null, null);
}
public boolean launch() {
boolean unique;
try {
final ServerSocket server = new ServerSocket(port);
unique = true;
if(runOnReceive != null) {
Thread portListenerThread = new Thread("UniqueInstance-PortListenerThread") {
{
setDaemon(true);
}
@Override public void run() {
while(true) {
try {
final Socket socket = server.accept();
new Thread("UniqueInstance-SocketReceiver") {
{
setDaemon(true);
}
@Override public void run() {
receive(socket);
}
}.start();
} catch(IOException e) {
Logger.getLogger("UniqueInstance").warning("Attente de connexion de socket échouée.");
}
}
}
};
portListenerThread.start();
}
} catch(IOException e) {
unique = false;
if(runOnReceive != null) {
send();
}
}
return unique;
}
private void send() {
PrintWriter pw = null;
try {
Socket socket = new Socket("localhost", port);
pw = new PrintWriter(socket.getOutputStream());
pw.write(message);
} catch(IOException e) {
Logger.getLogger("UniqueInstance").warning("Écriture sur flux de sortie de la socket échouée.");
} finally {
if(pw != null)
pw.close();
}
}
private synchronized void receive(Socket socket) {
Scanner sc = null;
try {
socket.setSoTimeout(5000);
sc = new Scanner(socket.getInputStream());
String s = sc.nextLine();
if(message.equals(s)) {
runOnReceive.run();
}
} catch(IOException e) {
Logger.getLogger("UniqueInstance").warning("Lecture du flux d'entrée de la socket échoué.");
} finally {
if(sc != null)
sc.close();
}
}
} |
III-C. Exemple d'utilisation
Voici un exemple typique d'utilisation (celui présent dans la javadoc) :
final int PORT = 32145;
final String MESSAGE = "nomDeMonApplication";
final Runnable RUN_ON_RECEIVE = new Runnable() {
public void run() {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
if(mainFrame != null) {
if(!mainFrame.isVisible())
mainFrame.setVisible(true);
mainFrame.toFront();
}
}
});
}
});
UniqueInstance uniqueInstance = new UniqueInstance(PORT, MESSAGE, RUN_ON_RECEIVE);
if(uniqueInstance.launch()) {
new MonApplication();
} |
Dans cet exemple, mainFrame est la fenêtre principale de l'application (l'accès à cette fenêtre peut être fait différemment selon la manière dont vous avez organisé votre application).


Ce document est issu de http://www.developpez.com et reste la propriété exclusive de son auteur.
La copie, modification et/ou distribution par quelque moyen que ce soit est soumise à l'obtention préalable de l'autorisation de l'auteur.