Strates et conteneurs

Une application peut avoir besoin d’utiliser des logithèques complexes qui sont elles-mêmes des "briques" constitutives adaptables à des logiciels divers.

On aura ainsi par exemple une intégration d’un code de rémanence qui assurera une transposition objet/relationnel et des liens avec une autre logithèque d’accès à une base de données relationnelle (citons Hibernate pour gérer ces transitions). On pourrait aussi avoir une gestion de l’informatique répartie avec couche de transfert (par exemple service fiable sur UDP comme jgroups ) et couche authentification/sécurité, etc…

Pour fonctionner ces codes ont besoin de leurs propres versions des services et même parfois de leur propre version de classes Java (standard ou d’autres logithèques).

Pour éviter des conflits et isoler ces éléments on peut alors gérer ces composants dans des environnements qui assurent l'étanchéité entre contextes. Le premier constituant architectural est le ClassLoader. Toutefois l’architecture modulaire est un système complémentaire d’isolation des composants (on a des contrôles de compile-time)

On pourra donc combiner les deux outils pour créer des architectures complexes: tout dépend des contraintes (étanchéité entre modules contre duplications -apparentes- de packages tolérées entre ClassLoaders)

Les ClassLoaders

Chaque ClassLoader charge des modules et leur contenu de manière cohérente.

  • Il sait trouver les binaires (dans le système de fichier, sur le réseau, etc…) et sait trouver des ressources dans l’organisation hiérarchique des désignations.

    Par exemple un URLClassLoader saura trouver ses codes dans une série d’URL.

  • Il est lié à un ClassLoader parent. Lorsqu’on demande une classe ou une ressource (ou une library binaire) il va d’abord la demander à son parent avant d'éventuellement la charger localement. On a ainsi une hiérarchie des ClassLoaders dont le sommet est le ClassLoader de bootstrap.

    Par exemple les constructeurs d'URLClassLoader prennent en paramètre le ClassLoader parent.

  • Une "même" classe peut être chargée (et initialisée) par différents ClassLoader. Les données statiques peuvent alors évoluer indépendamment (on peut même avoir la même constante qui a des valeurs différentes selon le loader hôte!). Les instances de ces classes sont incompatibles entre elles (instanceof répondra false si on traverse une frontière de loader!). Ce peut être un avantage (contexte ou versions isolées) ou un inconvénient!

On peut toutefois assurer la collaboration de codes issus de différents loaders. Par exemple en définissant une interface dans un ClassLoader parent et en invoquant une recherche de service par ServiceLoader.load(classService, classloaderCible)

Les strates de modules

images/blackbelt.png 2° dan!

Une "strate" (ModuleLayer) permet, par exemple, d’organiser une série de plugins associés à une application:

images/layers/plugins.png

Dans l’exemple suivant:

  • on recherche un ensemble de modules stockés dans un élément du Path
  • parmi ces modules certains seront des "points d’entrée": c’est à dire les modules à partir desquels le système cherchera à résoudre les graphes de dépendances (ils seront appelés racines dans l’exemple). Cette résolution créera un objet Configuration (à partir de la Configuration de la strate courante) qui vérifie les graphes de dépendances entre modules.
  • On crée un graphe de dépendance à partir de chaque module "racine" et il y aura un nouveau ClassLoader (fils) pour gérer l’ensemble des modules concernés (ici invocation avec OneLoader mais une invocation avec ManyLoaders est possible: voir la documentation)
// Le code est ici uniquement à titre de démonstration
// le paramètre Module est là pour simplifier l'explication
 public  static ModuleLayer créerStrate(Module module, Path path, String... racines){
    // trouve les modules dans le Path
     ModuleFinder finder = ModuleFinder.of(path);
     Set<String> ensembleRacines =  Set.of(racines) ;
     // la strate courante
     ModuleLayer parent = module.getLayer();
     // résolution du graphe des modules
     // à partir des modules "racines"
     Configuration config = parent.configuration().resolve(finder,
              // pas de recherche d'autres modules = ModuleFinder.of()ne rend plus rien
             ModuleFinder.of(), ensembleRacines) ;
     ClassLoader currentLoader = module.getClassLoader() ;
     // création de la strate "fille"
     ModuleLayer result = parent.defineModulesWithOneLoader(config, currentLoader);
     return result ;
 }

Pour une recherche de service on pourra invoquer un ServiceLoader par: ServiceLoader.load(classService, strate)

Les conteneurs

Une architecture en "Container" est un peu l’inverse d’une architecture avec des plugins.

images/layers/container.png

Ici on a une application "hôte" qui fournit un certains nombres de service de base à des applications qui lui sont connectées.

La création de la strate peut se faire par: ModuleLayer.Controller defineModulesWithOneLoader (Configuration cf, List<ModuleLayer> parentLayers, ClassLoader parentLoader) Car il peut y avoir plusieurs strates "parentes" de la strate à créer. La strate du container devra utiliser le ModuleLayer.Controller pour réaliser, par exemple, des addOpens sur les codes d’initialisations des applications.