Développer des services en Java

Yann D'Isanto

Présentation du développement de services en Java avec le ServiceLoader. Commentez ♪

- Titre : Développer des services en Java

- Auteur : Yann D'Isanto

- Parution : 6 juillet 2011

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2011 Yann D'Isanto. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Déclaration d'un service

- III. Récupération d'une implémentation du service

- IV. Implémentation du service

- V. Conclusion

- VI. Remerciements

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.

Authenticator.java

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 :

App.main()

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 ServiceLoader qui permet d'effectuer une recherche (un lookup en littérature anglaise) des implémentations disponibles pour un service donné.

App.getAuthenticator()

/**
* @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 :

StubAuthenticator.java

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 :

META-INF/services/org.myapp.Authenticator

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.