Accueil > Non classé > Applications Java extensibles avec les Service Provider Interfaces

Applications Java extensibles avec les Service Provider Interfaces

Introduction

Dans la lignée des plugins chargés au runtime, je vous propose cette fois-ci d’explorer une seconde voie d’ajout de fonctionnalités au runtime : les Service Provider Interfaces (SPI) couplées à l’utilisation du ServiceLoader du JDK6.

Une SPI ? Mais de quoi-t-est-ce qu’il cause-t-on ??

Les SPI, ou Service Provider Interfaces, sont des mécanismes permettant d’ajouter des composants à une application au runtime. Ils sont comparables à un design pattern pouvant par exemple se représenter sous cette forme simple :

Une SPI est donc un service prenant la forme d’une interface ou d’une classe (généralement abstraite) définissant un contrat particulier. Un ou plusieurs service providers implémentant ce contrat pourront ainsi être chargés au runtime et étendre le cœur de l’application. Ces providers sont souvent contenus dans jars placés dans le classpath.

C’est donc un pattern que vous avez très certainement utilisé dans bon nombre de vos projets, à quelques détails près :

  • Les interfaces et classes abstraites sont souvent définies dans un jar différent de votre application, et les providers sont eux-mêmes dans des jars séparés.
  • Votre application n’a aucune connaissance des implémentations (providers), il n’y a même pas de Factory ni d’injection via Spring.

Comment utiliser les providers, alors ?

C’est là qu’intervient le ServiceLoader. Cette classe est présente depuis Java 1.3, mais n’est une API publique que depuis Java SE 6. Ce ServiceLoader va se charger de chercher les différents providers d’une SPI dans le classpath de votre application, puis de les rendre accessibles à votre application.

Pour savoir quelles sont les classes faisant office de provider d’un service, le ServiceLoader va lire un fichier situé dans le répertoire META-INF/services de votre jar. Le nom de ce fichier est le fully qualified name de votre service, son contenu est la liste des fully qualified names des providers contenus dans le jar.

Voyons tout de suite un exemple pour mieux comprendre.

Du code ! Du code !

Imaginons un instant que log4j n’existe pas (ah, on me signale dans l’oreillette que 73.5% des projets Java actuels ne compilent plus à cause d’un log4j.jar manquant…). A la place, nous avons développé notre propre API de logging, basée sur l’interface suivante :


1
2
3
4
5
6
7
8
9
10
11
12
13
package com.excilys.blog.logger.spi;

/**
 * An interface describing a logging service, to be implemented by various
 * providers.
 *
 * @author bastien.jansen
 */

public interface Logger {
    void logInfo(String message);
    void logWarning(String message);
    void logError(String message, Throwable t);
}

Packageons cette interface dans un jar nommé LoggerSPI.jar. Notre équipe de développement a également codé deux classes implémentant cette interface : FileLogger, qui va écrire les logs dans un fichier, et SysoutLogger, qui va afficher les logs dans la console via System.out. Ces deux classes (que vous trouverez dans le source complet à la fin de l’article) sont nos providers, nous allons donc tenter de les charger via le ServiceLoader. Pour cela, nous allons les packager dans un second jar nommé LoggerProviders.jar, qui contiendra également le fichier META-INF/services/com.excilys.blog.logger.spi.Logger :


1
2
com.excilys.blog.logger.FileLogger
com.excilys.blog.logger.SysoutLogger

Le nom du fichier est donc le nom du service, son contenu est le nom des deux providers. Pour utiliser les implémentations de notre service, nous pourrons écrire le code suivant :


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.excilys.blog.logger.sampleapplication;

import com.excilys.blog.logger.spi.Logger;
import java.util.Iterator;
import java.util.ServiceLoader;

/**
 * Simple project that use our logging SPI.
 * @author bastien.jansen
 */

public class Main {

    public static void main(String[] args) {
        ServiceLoader loader = ServiceLoader.load(Logger.class);
        Iterator iterator = loader.iterator();

        while (iterator.hasNext()) {
            Logger logger = (Logger) iterator.next();

            logger.logWarning("Achtung, there may be a problem!");
            logger.logError("Oh no!", new Exception("Foo was here."));
        }
    }
}

Vous remarquerez que l’utilisation de ServiceLoader est très simple. Nous lui demandons de charger l’interface Logger.class, puis nous récupérons nos providers dans un Iterator que nous pouvons parcourir pour logger nos messages sur les différents loggers.

Le lancement de notre application se fera de la manière suivante :

1
>: java -classpath "lib\LoggerSPI.jar;lib\LoggerProviders.jar" -jar dist/SampleApplication.jar

Résultat de la console lors de l'exécution depuis Netbeans

[WARNING] Achtung, there may be a problem!
[ERROR] Oh no!: Foo was here.
[com.excilys.blog.logger.sampleapplication.Main.main(Main.java:21)]

Contenu du fichier créé par FileLogger

Utilisation des SPI dans des cas concrets

Dans JavaEE

La stack JavaEE utilise des SPI dans de nombreux cas. Pour avoir un aperçu, faites une recherche sur “spi” dans la liste des packages de l’API JavaEE 6. Un exemple que vous avez dû utiliser sans le savoir est la SPI de JNDI :

SPI dans JNDI

SPI dans JNDI

Votre application utilise une API haut niveau, mais sous le capot se trouve une SPI dont les implémentations vont taper dans du LDAP, NIS, RMI etc. En ajoutant les jars adéquats à votre serveur d’application, vous pouvez ainsi facilement ajouter le support de CORBA. Le provider à utiliser va être déduit à partir de l’InitialContext que vous allez créer :


1
2
3
Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
Context ctx = new InitialContext(env);

Ici, le service provider LDAP sera utilisé. En changeant d’INITIAL_CONTEXT_FACTORY, nous pouvons ainsi facilement remplacer le provider utilisé au runtime. En fouinant dans le code source de LdapCtxFactory, nous remarquons que -oh magie- la classe est une implémentation de deux SPI :


1
2
3
4
import javax.naming.spi.ObjectFactory;
import javax.naming.spi.InitialContextFactory;
// ...
final public class LdapCtxFactory implements ObjectFactory, InitialContextFactory {

A noter que l’on retrouve également des SPIs dans JDBC, JCE ou encore JAXP.

Pour les annotation processors

Décidément, on les retrouve partout ces annotations:D Il est possible de spécifier au compilateur qu’un annotation processor est un provider de javax.annotation.processing.Processor en créant un fichier du même nom dans META-INF/services.

Toujours dans le mix annotations / SPI, je suis tombé par hasard sur un projet permettant d’éviter de déclarer à la main ses providers dans META-INF/services en annotant les providers avec un @ProviderFor(BlahBlahBlah.class). Bien évidemment, il y a un autre annotation processor qui génère automatiquement ces fichiers. L’histoire ne dit pas si l’annotation processor pour @ProviderFor a réussi à se déclarer lui-même comme un provider de javax.annotation.processing.Processor ;)

Dans Java Sound

Dans le temps jadis de Java 1.2, seuls les formats WAV, AIFF, MIDI et AU étaient supportés par le système audio du JDK. Java 1.3 a introduit une nouvelle API, Java Sound (en même temps que les SPI, donc). Cette API est extensible de manière à supporter de nouveaux formats.

Pour cela, il suffit de créer un provider pour le service javax.sound.sampled.spi.AudioFileReader. L’avantage est que vous pouvez par exemple ajouter le support des MP3 à un player utilisant Java Sound, sans avoir besoin de modifier ou recompiler l’application ! Plus de précisions sont disponibles dans un (vieil) article sur JavaWorld : Add MP3 capabilities to Java Sound with SPI.

Conclusion

Pour les fans de découplage, de modularisation ou d’API claires, les SPI peuvent être le rêve. D’un côté des interfaces qui n’exposent que ce qui doit l’être, de l’autre un ou plusieurs providers qui peuvent être remplacés à tout moment sans avoir à toucher au code. Et si ça ne vous suffit pas, il ne vous reste plus qu’à attendre Java 8 et son célèbre Jigsaw !

Pour aller plus loin

Share
  1. 19/05/2011 à 14:03 | #1

    Merci pour cet article. Il faudra que je les essaye !

  2. 06/03/2015 à 16:52 | #2

    Avoid Enum a été enlevé de la doc. C’est donc correct à utiliser maintenant! :)

  3. daniel
    18/07/2017 à 07:14 | #3

    bonjour s il vous plait
    pourquoi on ne peux pas faire ceci java.lang.Math n = new java.lang.Math();
    pourquoi sa ne marche pas

  1. 10/01/2011 à 10:52 | #1