I. Introduction▲
Depuis sa version 6 Java SE offre des outils pour gérer simplement des services au sein des applications Java.
L'utilisation des services permet d'apporter une modularité et un meilleur découplage à votre application. En effet, l'application utilisera les services au travers d'interfaces ce qui permet de s'abstraire de leur implémentation.
Imaginons une application nécessitant une authentification de la part de l'utilisateur. Du point de vue de l'application (couche présentation), la seule chose importante est de savoir si un couple login/password est valide, peu importe comment est effectué cette validation (couche business). L'application ne s'intéresse pas à l'implémentation du mécanisme d'authentification qui doit lui rester « invisible ». Ainsi même si ce mécanisme d'authentification change cela n'impactera pas le code de l'application.
II. Déclaration d'un service▲
La première étape consiste à déclarer le service ce qui revient tout simplement à écrire l'interface ou la classe abstraite qui sera utilisée par l'application.
package
org.myapp;
public
interface
Authenticator {
/**
* Renvoie true si le couple login/password spécifié est valide.
*
@param
login
le login
*
@param
password
le mot de passe
*
@return
true en cas d'authentification réussie, false dans le cas contraire.
*/
boolean
authenticate
(
String login, char
[] password);
}
Voici un exemple d'application qui utilise ce service :
Console console =
System.console
(
);
if
(
console ==
null
) {
System.err.println
(
"no console available"
);
System.exit
(
1
);
}
Authenticator authenticator =
getAuthenticator
(
);
if
(
authenticator ==
null
) {
System.err.println
(
"Service d'authentification introuvable"
);
System.exit
(
1
);
}
String login =
console.readLine
(
"login:"
);
char
[] password =
console.readPassword
(
"password:"
);
if
(
authenticator.authenticate
(
login,password)) {
System.out.println
(
"Authentification réussie, bienvenue "
+
login +
" :-)"
);
}
else
{
System.out.println
(
"Login ou mot de passe invalide !"
);
}
La méthode getAuthenticator(), qui permet de récupérer une implémentation de l'interface Authenticator, est détaillée dans la partie suivante.
III. Récupération d'une implémentation du service▲
Java 6 a introduit la classe ServiceLoaderServiceLoader Javadoc qui permet d'effectuer une recherche (un lookup en littérature anglaise) des implémentations disponibles pour un service donné.
/**
*
@return
la première implémentation de l'interface Authenticator trouvée.
*/
public
static
Authenticator getAuthenticator
(
) {
Authenticator authenticator =
null
;
// Récupération d'un ServiceLoader pour l'interface Authenticator
ServiceLoader<
Authenticator>
serviceLoader =
ServiceLoader.load
(
Authenticator.class
);
// Iterator des implémentations trouvées par le ServiceLoader
Iterator<
Authenticator>
iterator =
serviceLoader.iterator
(
);
// On récupère la première implémentation trouvée
if
(
iterator.hasNext
(
)) {
authenticator =
iterator.next
(
);
}
return
authenticator;
}
Chose importante, ce code ne fait toujours référence qu'à l'interface et reste donc là encore totalement abstrait de l'implémentation.
Le ServiceLoader utilise un cache et un système d'initialisation à la demande. En effet, l'implémentation est instanciée lors de l'appel à la méthode next() de l'Iterator puis mise en cache. Ainsi, en itérant une deuxième fois, on récupère la même instance du service. Il est possible de vider le cache (et ainsi forcer la réinstanciation du service) en appelant la méthode reload() ou en créant un deuxième ServiceLoader avec ServiceLoader.load().
IV. Implémentation du service▲
Le code de l'application est maintenant terminé, il ne reste plus qu'à implémenter et publier le service d'authentification. La seule restriction imposée est d'avoir un constructeur par défaut qui sera appelé lors du chargement. En voici un exemple simple :
package
org.myapp;
public
class
StubAuthenticator implements
Authenticator {
@Override
public
boolean
authenticate
(
String login, char
[] password) {
return
"admin"
.equals
(
login) &&
"unsecure"
.equals
(
new
String
(
password));
}
}
Afin que notre implémentation puisse être récupérée par le ServiceLoader, il faut la « publier ». Pour ce faire, il suffit de créer un fichier dont le nom est celui du service (nom complet de l'interface ou de la classe abstraite) dans le répertoire ressource « META-INF/services ». Dans ce fichier on écrit le nom complet de l'implémentation.
Par exemple, on crée un fichier META-INF/services/org.myapp.Authenticator dans lequel on écrit la ligne suivante :
org.myapp.StubAuthenticator
Tout est maintenant en place pour exécuter l'application qui utilisera le service Authenticator en toute transparence. Il est ainsi possible de créer une autre implémentation (par exemple un LdapAuthenticator qui utiliserait un annuaire LDAP) et il suffit de placer son nom dans le fichier META-INF/services/org.myapp.Authenticator à la place du StubAuthenticator pour modifier l'implémentation de l'authentification de l'application sans toucher une seule ligne de code de celle-ci.
À noter qu'il est possible d'indiquer plusieurs implémentations dans le fichier en allant simplement à la ligne entre leurs noms. L'Iterator renvoyé par le ServiceLoader permet de toutes les récupérer. On peut aussi utiliser une boucle for étendue avec le ServiceLoader qui implémente Iterable.
ServiceLoader<
Authenticator>
serviceLoader =
ServiceLoader.load
(
Authenticator.class
);
for
(
Authenticator authenticator : serviceLoader) {
// CODE
}
Il est également possible d'écrire des commentaires dans le fichier en utilisant le caractère '#' (tout ce qui suit le '#' sur la ligne est ignoré).
V. Conclusion▲
Bien que le ServiceLoader offre une certaine ressemblance aux frameworks d'injection de dépendances, il n'est pas vraiment fait pour répondre à cette problématique. Alors qu'un framework d'injection de dépendances, comme Spring ou Guice, s'occupe de charger une implémentation donnée et précise d'une dépendance, le ServiceLoader est conçu pour récupérer un ensemble extensible d'implémentations d'un service.
Il est donc particulièrement indiqué pour le développement de plugin/modules (on peut facilement imaginer une interface Plugin dont on chargerait les instances avec le ServiceLoader).
Pour résumer, le ServiceLoader est un moyen standard assez simple de facilement :
- découpler son application ;
- la rendre plus flexible/modulaire.
VI. Remerciements▲
Je remercie keulkeul, ram-0000 et ClaudeLELOUP pour leurs remarques et corrections.