Dans cette section, vous apprendrez à créer un service. Un service est composé d’une interface, de toutes les classes que l’interface référence, et d’un moyen de rechercher les implémentations de l’interface. Les implémentations ne font pas partie du service.
Nous utiliserons une application de visite guidée dans la section services. Elle comporte quatre modules illustrés dans la Figure 12.13. Dans cet exemple, les modules zoo.tours.api
et zoo.tours.reservations
constituent le service puisqu’ils sont composés de l’interface et de la fonctionnalité de recherche.
Déclarer l’Interface du Fournisseur de Service
Tout d’abord, le module zoo.tours.api
définit un objet Java appelé Souvenir
. Il est considéré comme faisant partie du service car il sera référencé par l’interface.
// Souvenir.java
package zoo.tours.api;
public record Souvenir(String description) { }
Ensuite, le module contient un type d’interface Java. Cette interface est appelée l’interface du fournisseur de service car elle spécifie quel comportement notre service aura. Dans ce cas, c’est une API simple avec trois méthodes.
// Tour.java
package zoo.tours.api;
public interface Tour {
String nom();
int duree();
Souvenir getSouvenir();
}
Les trois méthodes utilisent le modificateur public
implicite. Puisque nous travaillons avec des modules, nous devons également créer un fichier module-info.java
pour que notre définition de module exporte le package contenant l’interface.
// module-info.java
module zoo.tours.api {
exports zoo.tours.api;
}
Maintenant que nous avons les deux fichiers, nous pouvons compiler et empaqueter ce module.
javac -d serviceProviderInterfaceModule
serviceProviderInterfaceModule/zoo/tours/api/*.java
serviceProviderInterfaceModule/module-info.java
jar -cvf mods/zoo.tours.api.jar -C serviceProviderInterfaceModule/ .
Une interface du fournisseur de service peut être une classe abstraite plutôt qu’une interface réelle.
Pour résumer, le service comprend l’interface du fournisseur de service et les classes de support qu’il référence. Le service comprend également la fonctionnalité de recherche, que nous définissons ensuite.
Créer un Localisateur de Service
Pour compléter notre service, nous avons besoin d’un localisateur de service. Un localisateur de service peut trouver n’importe quelle classe qui implémente une interface de fournisseur de service.
Heureusement, Java fournit une classe ServiceLoader
pour aider à cette tâche. Vous passez le type d’interface du fournisseur de service à sa méthode load()
, et Java renverra toutes les implémentations de services qu’il peut trouver. La classe suivante le montre en action :
// TourFinder.java
package zoo.tours.reservations;
import java.util.*;
import zoo.tours.api.*;
public class TourFinder {
public static Tour trouverUneTour() {
ServiceLoader<Tour> loader = ServiceLoader.load(Tour.class);
for (Tour tour : loader)
return tour;
return null;
}
public static List<Tour> trouverToutesTours() {
List<Tour> tours = new ArrayList<>();
ServiceLoader<Tour> loader = ServiceLoader.load(Tour.class);
for (Tour tour : loader)
tours.add(tour);
return tours;
}
}
Comme vous pouvez le voir, nous avons fourni deux méthodes de recherche. La première est une méthode de commodité si vous vous attendez à ce qu’exactement un Tour
soit renvoyé. L’autre renvoie une List
, ce qui permet d’accueillir n’importe quel nombre de fournisseurs de services. Au moment de l’exécution, il peut y avoir de nombreux fournisseurs de services (ou aucun) qui sont trouvés par le localisateur de service.
L’appel à ServiceLoader
est relativement coûteux. Si vous écrivez une vraie application, il est préférable de mettre en cache le résultat.
Notre définition de module exporte le package avec la classe de recherche TourFinder
. Elle nécessite le package d’interface du fournisseur de service. Elle a également la directive uses
puisqu’elle recherchera un service.
// module-info.java
module zoo.tours.reservations {
exports zoo.tours.reservations;
requires zoo.tours.api;
uses zoo.tours.api.Tour;
}
N’oubliez pas que les directives requires
et uses
sont toutes deux nécessaires, l’une pour la compilation et l’autre pour la recherche. Enfin, nous compilons et empaquetage le module.
javac -p mods -d serviceLocatorModule
serviceLocatorModule/zoo/tours/reservations/*.java
serviceLocatorModule/module-info.java
jar -cvf mods/zoo.tours.reservations.jar -C serviceLocatorModule/ .
Maintenant que nous avons l’interface et la logique de recherche, nous avons terminé notre service.
Utilisation de ServiceLoader
Il y a deux méthodes dans ServiceLoader
que vous devez connaître. La déclaration est la suivante, sans l’implémentation complète :
public final class ServiceLoader<S> implements Iterable<S> {
public static <S> ServiceLoader<S> load(Class<S> service) { … }
public Stream<Provider<S>> stream() { … }
// Méthodes supplémentaires
}
Comme nous l’avons déjà vu, appeler ServiceLoader.load()
renvoie un objet que vous pouvez parcourir normalement. Cependant, demander un Stream
vous donne un type différent. La raison en est qu’un Stream
contrôle quand les éléments sont évalués. Par conséquent, un ServiceLoader
renvoie un Stream
d’objets Provider
. Vous devez appeler get()
pour récupérer la valeur que vous vouliez de chaque Provider
, comme dans cet exemple :
ServiceLoader.load(Tour.class)
.stream()
.map(Provider::get)
.mapToInt(Tour::duree)
.max()
.ifPresent(System.out::println);
Invocation depuis un Consommateur
Vient ensuite l’appel au localisateur de service par un consommateur. Un consommateur (ou client) fait référence à un module qui obtient et utilise un service. Une fois que le consommateur a acquis un service via le localisateur de service, il est en mesure d’invoquer les méthodes fournies par l’interface du fournisseur de service.
// Touriste.java
package zoo.visitor;
import java.util.*;
import zoo.tours.api.*;
import zoo.tours.reservations.*;
public class Touriste {
public static void main(String[] args) {
Tour tour = TourFinder.trouverUneTour();
System.out.println("Tour unique : " + tour);
List<Tour> tours = TourFinder.trouverToutesTours();
System.out.println("# tours : " + tours.size());
}
}
Notre définition de module n’a pas besoin de connaître quoi que ce soit sur les implémentations puisque le module zoo.tours.reservations
s’occupe de la recherche.
// module-info.java
module zoo.visitor {
requires zoo.tours.api;
requires zoo.tours.reservations;
}
Cette fois, nous exécutons un programme après la compilation et l’empaquetage.
javac -p mods -d consumerModule
consumerModule/zoo/visitor/*.java consumerModule/module-info.java
jar -cvf mods/zoo.visitor.jar -C consumerModule/ .
java -p mods -m zoo.visitor/zoo.visitor.Touriste
Le programme affiche ce qui suit :
Tour unique : null
# tours : 0
Eh bien, cela a du sens. Nous n’avons pas encore écrit de classe qui implémente l’interface.
Ajouter un Fournisseur de Service
Un fournisseur de service est l’implémentation d’une interface de fournisseur de service. Comme nous l’avons dit précédemment, à l’exécution, il est possible d’avoir plusieurs classes ou modules d’implémentation. Nous nous en tiendrons à un seul ici pour plus de simplicité.
Notre fournisseur de service est le package zoo.tours.agency
car nous avons externalisé la gestion des visites à un tiers.
// TourImpl.java
package zoo.tours.agency;
import zoo.tours.api.*;
public class TourImpl implements Tour {
public String nom() {
return "Dans les Coulisses";
}
public int duree() {
return 120;
}
public Souvenir getSouvenir() {
return new Souvenir("peluche");
}
}
Encore une fois, nous avons besoin d’un fichier module-info.java
pour créer un module.
// module-info.java
module zoo.tours.agency {
requires zoo.tours.api;
provides zoo.tours.api.Tour with zoo.tours.agency.TourImpl;
}
La déclaration du module nécessite le module contenant l’interface comme dépendance. Nous n’exportons pas le package qui implémente l’interface car nous ne voulons pas que les appelants y fassent référence directement. Au lieu de cela, nous utilisons la directive provides
. Cela nous permet de spécifier que nous fournissons une implémentation de l’interface avec une classe d’implémentation spécifique. La syntaxe ressemble à ceci :
provides interfaceName with className;
Nous n’avons pas exporté le package contenant l’implémentation. Au lieu de cela, nous avons rendu l’implémentation disponible pour un fournisseur de service utilisant l’interface.
Enfin, nous compilons et empaquetage.
javac -p mods -d serviceProviderModule
serviceProviderModule/zoo/tours/agency/*.java
serviceProviderModule/module-info.java
jar -cvf mods/zoo.tours.agency.jar -C serviceProviderModule/ .
Maintenant vient la partie intéressante. Nous pouvons exécuter le programme Java à nouveau.
java -p mods -m zoo.visitor/zoo.visitor.Touriste
Cette fois, nous voyons la sortie suivante :
Tour unique : zoo.tours.agency.TourImpl@1936f0f5
# tours : 1
Remarquez comment nous n’avons pas recompilé les packages zoo.tours.reservations
ou zoo.visitor
. Le localisateur de service a pu observer qu’il y avait maintenant une implémentation de fournisseur de service disponible et la trouver pour nous.
Ceci est utile lorsque vous avez une fonctionnalité qui change indépendamment du reste de la base de code. Par exemple, vous pourriez avoir des rapports personnalisés ou de la journalisation.
Dans le développement logiciel, le concept de séparation de différents composants en pièces autonomes est appelé couplage faible. Un avantage du code faiblement couplé est qu’il peut être facilement échangé ou remplacé avec des changements minimaux (ou nuls) dans le code qui l’utilise. S’appuyer sur une structure faiblement couplée permet aux modules de service d’être facilement extensibles à l’exécution.
Révision des Directives et Services
Le tableau suivant résume ce que nous avons couvert dans la section sur les services. Nous recommandons d’apprendre vraiment bien ce qui est nécessaire lorsque chaque artefact est dans un module séparé. Cela garantira que vous comprenez les concepts.
Artefact | Partie du service | Directives requises |
---|---|---|
Interface du fournisseur de service | Oui | exports |
Fournisseur de service | Non | requires provides |
Localisateur de service | Oui | exports requires uses |
Consommateur | Non | requires |
Directive | Description |
---|---|
exports package; exports packageto module; | Rend le package disponible en dehors du module |
requires module; requires transitive module; | Spécifie un autre module comme dépendance |
opens package; opens packageto module; | Permet au package d’être utilisé avec la réflexion |
provides serviceInterfacewith implName; | Rend le service disponible |
uses serviceInterface; | Référence le service |