M2 TIW : Intergiciels et Services

TP conteneurs d'objets et inversion de contrôle

Objectifs pédagogiques

Mettre en pratique les patterns IoC, Annuaire et Contexte afin de mieux comprendre le fonctionnement d'un framework. Mettre en place un outil configurable et capable de gérer le cycle de vie de ses composants.

Rendu du TP

Vous créerez un projet forge dans lequel vous travaillerez pour tout ce TP. N'oubliez pas de nous mettre (Emmanuel Coquery et moi) reporters de votre projet. Pour le rendu, vous indiquerez l'URL de base, complète, de votre projet forge sur la page Tomuss de l'UE (case "Rendu_TP_conteneurs").

Application source

Vous allez travailler sur une application de gestion de calendriers électroniques, dont une implémentation basique est accessible sur la forge, à : https://forge.univ-lyon1.fr/LIONEL.MEDINI/tiw-is-2017-container-base. La fonction main de l'application est située dans la classe d'interface utilisateur (CalendarUI), qui instancie une classe contrôleur (Calendar), laquelle instancie à son tour les autres classes de l'application, et notamment la classe d'accès aux données (DAO).

Clonez ce projet et modifiez, dans les classes CalendarUI et TestCalendarBuilder, le chemin du calendar où vous allez enregistrer les données.

Compilez et exécutez le projet. Logiquement, il doit donctionner avec un DAO en XML. Au besoin, corrigez les problèmes de persistance, et lancez les tests. Vous pouvez aussi tester "à la main", en lançant CalendarUI. Cela vous permettra de regarder, en cours d'exécution, le comportement de vos supports de persistance.

Durant ce TP, vous allez modifier la structure de cette application pour qu'elle implémente les principes de l'inversion de contrôle, du contexte, de l'injection de dépendances et de la gestion du cycle de vie.

Lien vers la dernière version de PicoContainer.

0. Premières modifications

  1. L'application doit être considérée comme une application client-serveur sur un réseau. Le client est l'interface utilisateur, qui échange des requêtes et des réponses avec le serveur (le calendar).
    Vous allez tout d'abord créer une arborescence de trois packages "tp1", "serveur" et "client", le premier étant le père des suivants. Créez ensuite une classe contenant la fonction main, créant un serveur (Calendar) et un client (CalendarUI) et passant à ce dernier une référence sur le serveur. Placez l'ensemble des classes de l'application dans les packages appropriés.
  2. Actuellement, il existe une classe de configuration statique qui contient le format de date commun à plusieurs classes de l'application (CalendarUI et Event). Faites en sorte que seul Event dépende de cette configuration. Pour cela :
    • Supprimez l'utilisation de cette classe de CalendarUI, et déportez le parsing des entrées utilisateur dans Calendar
    • Profitez-en pour mettre en place un pattern DTO (qui encapsule les données qui décrivent un événement) pour transporter les données entre le client et le serveur
    • Profitez-en pour factoriser le code de la méthode main de CalendarUI
  3. Supprimez l'aspect statique de la configuration, pour que chaque utilisateur puisse configurer l'application selon ses préférences. Injectez la configuration (cela peut être juste une String) dans l'Calendar, qui l'injectera à son tour dans les Events

1. Inversion de contrôle

Vous allez maintenant mettre en place un conteneur. Pour cela, créez une classe Serveur qui masque au client l'ensemble des objets côté serveur en lien avec le calendar et placez ces objets dans un conteneur.

Indications

Ressortez les objets métiers du package serveur de l'application et rangez-les dans des packages métier, à votre guise. Modifiez enfin les classes main et cliente pour que le client puisse contacter le serveur et appeler la méthode de service.

À ce stade, vous avez inversé le contrôle de vos objets métier en les plaçant dans un serveur (i.e. un framework) qui se charge d'instancier, de gérer et d'utiliser ces objets.

2. Isolation et uniformisation des objets côté serveur

2.1. Isolation

Bien entendu, vous ne pouvez pas laisser le client accéder directement à l'instance du calendar créée dans le conteneur. Pour cela, vous allez implémenter le paradigme requête-réponse :

Remarquez que la classe ServeurImpl masque désormais complètement l'implémentation du traitement des requêtes par les objets métier. Il suffit au client de connaître son API pour utiliser le calendar.

2.2. Uniformisation

Plutôt que d'avoir un objet Calendar qui répond à différentes requêtes, vous allez créer plusieurs objets sur le même modèle, mais traitant chacun un type de requête spécifique. Pour cela :

Cycle de vie des composants : Normalement, votre application ne doit pas fonctionner et vous renvoie une liste d'événements vide à chaque opération. Vous constatez également que les instances du DAO sont différentes dans les messages d'initialisation des calendars des méthodes de gestion du cycle de vie.
Cela vient du fait que bien que les classes CalendarXMLDAO et ArrayList soient des dépendances communes de tous les calendars du conteneur, par défaut, celui-ci résout les dépendances en instanciant un objet différent pour chaque instance de Calendar. Toutefois, vous pouvez indiquer que vous souhaitez procéder autrement, c'est-à-dire qu'il "cache" les instances. Vous pouvez résoudre ce problème à deux niveaux :

  1. Au niveau du composant : en spécifiant la caractéristique "Cache" des composants que vous voulez cacher. Le plus simple est d'utiliser la méthode as() du conteneur, comme spécifié ici.
  2. Au niveau du conteneur : en spécifiant un comportement global de type "Caching" pour tous les composants du conteneur dans le constructeur de celui-ci.

Si vous choisissez la seconde solution, les calendars seront cachés également, et vous n'aurez plus besoin dus stocker dans une variable globale. Par ailleurs, comme indiqué dans le warning du début de la page sur la gestion du cycle de vie des composants, les méthodes start(), stop(), etc. sont conçues pour fonctionner avec des composants cachés, et vous pourrez appelez directement la méthode start() du conteneur pour qu'il démarre tous les composants qui implémentent Startable en même temps...

Mise en place des couches de sérialisation : Maintenant, vous avez plusieurs classes terminales Calendar, et vous ne pouvez pas garder les mappings JAXB sur la classe abstraite. Les classes dérivées de celle-ci sont des composants métier, et il n'est pas intéressant dus sérialiser (et cela conduirait à reproduire les mappings dans toutes ces classes, ce qui contredit le pattern DRY). Vous devez donc séparer les données à sérialiser des traitements métier. Pour cela, créez une classe CalendarEntity qui représente la partie données du calendar et encapsule les données à persister.
Si vous appliquez les annotations JAXB directement sur cette classe, celle-ci sera dépendante de la méthode de sérialisation choisie (ici XML). Pour rendre CalendarEntity générique par rapport à sa méthode de sérialisation, vous pouvez :

  1. créer une nouvelle classe qui récupère un CalendarEntity et est sérialisée à l'aide d'annotations JAXB
  2. définir la sérialisation JAXB du calendar dans un fichier XML (et non dans des annotations de l'entité)
  3. mettre en place la sérialisation XML programmatiquement (en utilisant le DOM par exemple)

À ce stade, vous avez réalisé un outil équivalent à un conteneur de servlets. Il pourra fonctionner avec n'importe quelle classe d'implémentation correspondant à l'API du calendar, à partir du moment où celle-ci est déclarée dans le serveur et correspond à une commande reconnue.

3. Création d'un contexte applicatif

Dans cette partie, vous allez rajouter un niveau d'indirection entre le conteneur (et ses composants) et les objets de type CalendarDAO. Pour cela, vous allez implémenter une classe qui permettra à chaque instance de Event créée d'accéder à ce DAO en respectant le pattern Context présenté en cours. Cela permettra par exemple d'utiliser un DAO instancié à l'extérieur du serveur (cas d'une connexion à un SGBD), ou de modifier par configuration le DAO utilisé pour gérer la persistence des données de l'application.

3.1. Création du contexte

Créez une interface "CalendarContext", de façon à ce que :

En clair, il s'agit d'un objet qui stocke une référence sur un DAO et possède deux accesseurs sur ce champ.

Créez une classe d'implémentation "CalendarContextImpl" de cette interface, instanciez-la dans votre serveur et ajoutez-la en tant que composant de votre conteneur.

3.2. Modification de l'arbre de dépendances

Vous allez modifier les composants du conteneur ayant une dépendance sur un objet CalendarDAO pour qu'ils dépendent de CalendarContext.

3.3. Initialisation et utilisation du contexte

Enfin, dans le serveur, mettez en place les méthodes correspondantes du contexte de façon à ce que ses clients (calendars) puissent définir et récupérer une référence sur le DAO (setDAO(), getDAO() ?) .

Faites tourner et testez votre application. Vous pouvez ensuite par exemple vous servir du contexte pour filtrer les appels au DAO et ne renvoyer la bonne référence que si la méthode est appelée par un calendar (voir ici ou pour des exemples de code sur comment trouver la classe appelant une méthode).
Remarque : dans ce cas, supprimez l'appel à la méthode toString() de l'instance du DAO dans l'affichage de la méthode start() des calendars.

3.4 Généralisation du contexte

Actuellement, votre contexte n'est capable que de gérer un DAO. Modifiez-en l'API pour qu'il puisse stocker des références à tous les types d'objets et que ces objets soient accessibles par un nom (String).

Testez ce contexte générique en lui injectant aussi le calendar entity et en forçant le passage par le contexte pour permettre aux calendars de communiquer avec cette liste.

Votre serveur a désormais une responsabilité supplémentaire : en plus de fournir un conteneur de composants métier, il gère un contexte pour l'isolation des composants du conteneur. Le contexte est accessible à l'intérieur du conteneur pour permettre et contrôler l'accès par les composants aux éléments externes tels que le DAO. Vous avez mis en place les principaux éléments d'un framework applicatif, que vous allez perfectionner dans la suite.

4. Création d'un Annuaire

Encapsulez le contexte dans une structure de type annuaire (cf. cours). Un annuaire sera une hiérarchie de contextes, spécifiques à différents éléments de votre conteneur et de vos applications. Ces éléments correspondront aux différents types d'objets à isoler (les calendars, la liste d'événements et le DAO). Faites en sorte que votre annuaire soit capable de décomposer les noms de la manière suivante : nom de contexte + "/" + nom d'objet stocké.

Vous allez maintenant commencer à remplir votre annuaire :

Remarque : les bindings dans l'annuaire s'effectuent avec des références à des instances déjà créées. Ces instances peuvent donc être créées par le conteneur, l'annuaire étant uniquement un moyen de permettre les accès entre ces objets.

Modifiez les classes principales de votre application pour que votre Main démarre séparément l'annuaire, le serveur et le client, et passe aux deux derniers une référence sur l'annuaire. Faites en sorte qu'au démarrage, le client "découvre" le serveur en passant par l'annuaire.

Vous venez de construire quelque chose de similaire à un annuaire JNDI, qui pernet aux composants d'accéder à des références sur des objets interne au conteneur ou distants. L'avantage de cette méthode est qu'elle fonctionne quelles que soient les implémentations du conteneur et du composant, et qu'elle permet d'utiliser plusieurs implémentations différentes d'un objet pour une même interface.

Aspects dynamiques de l'annuaire

Actuellement, vos objets interrogent l'annuaire pour récupérer des références à d'autres objets. Cependant, il peut arriver qu'une référence sur un objet change, par exemple parce qu'un objet n'est plus disponible ou qu'une nouvelle version a été implémentée. Vous allez donc mettre en place un système à base d'événements qui permettra aux objets clients de s'abonner aux changements des références dans l'annuaire et de donc faire une nouvelle requête à l'annuaire à chaque notification.

Pour cela, modifiez l'implémentation de votre annuaire :

  1. mettez en place un pattern Observer
  2. faites en sorte que les objets puissent s'abonner aux événements "changement de référence sur le nom X" pour réagir en conséquence
  3. déclenchez cet événement à chaque rebind d'un objet sur un nom existant

Cette stratégie de mise à jour dynamique d'une référence sur un objet est celle utilisée dans les frameworks à composants dynamiques, comme OSGi. Bien entendu, cette stratégie fonctionnera d'autant mieux si le client s'attend à trouver une implémentation d'une interface et non une instance d'une classe.

5. Mise en place d'un serveur d'applications

Dans cette partie, vous allez rendre votre serveur générique et permettre de lui faire exécuter diféfrentes applications.

5.1 Configuration de l'application

Écrivez un fichier de configuration en XML et stockez-y les dépendances de valeurs (type d'objet DAO, nom du fichier de stockage) et les types d'objets Calendar correspondant à chaque commande (à la manière des fichiers web.xml utilisés dans un container de servlets). Ci-dessous un exemple de fichier de configuration :

<? xml version="1.0" ?> <config> <application name="Calendar"> <business> <component> <class-name>monPackage.CalendarAdd</class-name> </component> <component> <class-name>monPackage.CalendarRemove</class-name> </component> <component> <class-name>monPackage.CalendarInit</class-name> </component> <component> <class-name>monPackage.CalendarList</class-name> </component> <component> <class-name>monAutrePackage.AnnuaireImpl</class-name> </component> <component> <class-name>java.util.ArrayList</class-name> </component> </business> <persistence> <dao> <class-name>monTroisiemePackage.CalendarXMLDAO</class-name> <param>test.xml</param> </dao> </persistence> </application> </config>

Remarque : dans le fichier de configuration, vous pouvez également indiquer :

Utilisez ces données dans la classe Serveur lors de l'instanciation des éléments des conteneurs et du contexte. Vous devrez utiliser l'API Reflection (et probablement Class.forName()) pour récupérer le .class défini par une chaîne de caractères.

5.2 Spécialisation des composants (facultatif)

Éventuellement, vous pouvez spécialiser vos classes Calendar et définir différents types de composants :

Pour mettre en oeuvre cette spécialisation, vous pouvez soit faire hériter chaque type d'une interface spécifique, soit les annoter en fonction d'annotations définies en conséquence.

De la même manière, pour prendre en compte cette spécialisation au niveau du serveur, vous pouvez soit modifier ce serveur (et son mode de configuration) pour que les fichiers XML mentionnent la nature des composants, et que le serveur la "comprenne", soit rajouter un composant intermédiaire qui intercepte toutes les requêtes et s'appuie sur son propre mode de configuration pour instancier et rediriger les requêtes sur ces composants.

À ce stade, vous avez réalisé un serveur d'applications, composé d'un serveur et d'un framework capable de mettre en place et de faire tourner différents types d'applications. Si vous avez réalisé la partie 5.2 en modifiant le serveur, vous avez créé un serveur qui fonctionne d'une manière proche des serveurs Java EE. Si vous l'avez réalisée par ajout d'une couche supplémentaire entre le serveur de la question 4 et l'application, votre serveur se rapproche plus d'un conteneur Spring inclus dans un conteneur de servlets.

6. Hiérarchie de conteneurs

Dans cette partie, vous allez changer l'implémentation de la liste de Events gérée par les calendars en remplaçant le composant ArrayList du conteneur par une classe implémentant l'interface List mais dérivant la classe DefaultPicoContainer. Par conséquent, cette liste sera à la fois un composant et un conteneur fils du conteneur existant. D'autre part, vos objets Event deviendront des composants de ce conteneur fils : il va y injecter les dépendances et gérer leur cycle de vie. Vous devrez donc éventuellement modifier la classe Event pour qu'elle soit compatible avec cette nouvelle implémentation.

6.1. Création d'une hiérarchie de conteneurs

Dans la classe Serveur, remplacez l'ArrayList qui contient les objets Event par un nouveau conteneur, fils du premier (voir partie "Container hierarchies" de l'introduction sur le site picocontainer). Tant qu'à faire, le conteneur fils utilisera un autre type d'injection de dépendances que le premier : l'injection par annotation de champs. Vous pouvez utiliser deux méthodes pour créer le conteneur fils :

  1. spécifier les factories dans le constructeur
    MutablePicoContainer fils = new DefaultPicoContainer(new Caching().wrap(new AnnotatedFieldInjection()), pere);
  2. utiliser un builder
    PicoBuilder builder = new PicoBuilder(pere);
    MutablePicoContainer fils = (DefaultPicoContainer) builder.withAnnotatedFieldInjection().build();

Dans tous les cas, il faut construire le fils en lui passant une référence sur le père (fait dans les exemples ci-dessus) ET ajouter le fils comme composant du père : voir ici (à faire).
Remarque : si l'on veut que le conteneur père puisse résoudre les dépendances des calendars vers le conteneur fils, il faut ajouter ce dernier en tant que component et non en tant que child container dans le père. Contrairement à ce qui est marqué dans la doc, l'appel à la méthode start() du fils sera cascadée correctement.

En tant que composant, faites en sorte que Event implémente l'interface Startable (vous vous servirez de la méthode start() plus tard). Pour cela, il faut que le conteneur fils possède également le comportement "Caching". Modifiez la création du fils en conséquence.

Dans Event, supprimez le constructeur prenant les différents champs en paramètre et précédez la déclaration des champs de la classe Event d'une annotation @Inject (de org.picocontainer.annotations).

6.2. Instanciation des composants

Dans cette question vous allez faire en sorte que votre conteneur fils reproduise le même comportement que celui de l'ArrayList utilisée dans les questions précédentes. Par conséquent, le serveur instanciera et gèrera dans ce deuxième conteneur autant de composants Event qu'il y en a dans le calendar. En plus des modifications de la classe Event, il faut modifier les méthodes de service des différents calendars en fonction des règles suivantes :

  1. Comme tous les composants du conteneur fils sont du même type, il faut utiliser le nommage des composants et gérer un String qui indique leur numéro d'ordre pour les désigner. Pour simplifier cela, il est conseillé de dériver le type de conteneur que vous allez utiliser comme fils. Cela permettra de lui rajouter les comportements désirés (par exemple la gestion de l'indice des événements) tout en conservant celui du conteneur.
  2. L'injection de dépendances étant faite au moment de l'appel des composants par le conteneur (getComponent()), il faut que le(s) composant(s) qui est/sont injecté(s) dans chaque instance d'Event représente(nt) les données à injecter. En d'autres termes, créez
    • soit des composants "title" (String), "description" (String), "start" (Date), "end" (Date)
    • soit un composant "dto" (EventDTO)
    et faites en sorte qu'il(s) possèdent des données à jour au moment où vous appellez la méthode getComponent() du conteneur fils.
  3. Il faut arrêter le conteneur fils avant de supprimer un événement parmi ses composants. Veillez à bien utiliser les méthodes stop() puis start() du fils autour des traitements effectués par le calendar (si vous ne le "restartez" pas, il ne pourra pas faire l'injection des dépendances dans les événements).

Ensuite, il faut faire attention à des problèmes spécifiques pour chaque calendar :

7. Gestion du cycle de vie des objets (pooling d'instances)

Actuellement, votre application gère une liste (conteneur fils) d'événements, qui crée des instances de Event et les rajoute dans le conteneur en fonction des besoins de l'application. Cependant, votre application n'accède jamais à toutes les instances de Event en même temps. Par conséquent, chaque objet Event est instancié, passe la majeure partie de sa vie dans la liste en étant peu utilisé, et est garbage collecté une fois supprimé de la liste. Dans cette partie, vous allez améliorer votre framework pour lui permettre de mieux gérer le cycle de vie des instances de Event pour qu'elles représentent alternativement différents événements stockées sur le support de persistance et se synchronisent avec celui-ci quand c'est nécessaire. Pour cela, vous allez utiliser les méthodes de gestion du cycle de vie des composants pour leur injecter les valeurs issues du support de persistance avant chaque utilisation, et les sauvegarder ensuite.

  1. Modifiez votre conteneur fils pour qu'il prenne en compte un nombre max d'instances déployables dans le conteneur. Ce nombre sera défini dans le fichier de configuration et injecté dans le conteneur fils par le serveur.
  2. Découplez la classe Event du DAO : comme le conteneur fils va devoir injecter les valeurs issues du support de persistence dans les instances de Event, il ne faut pas qu'il ait besoin d'accéder à des instances pour pouvoir obtenir ces valeurs... Faites en sorte que le conteneur fils dépende du DAO (qui lui sera par exemple injecté par le conteneur père), qu'il sache l'utiliser (i.e. qu'il possèdus méthodes nécessaires pour récupérer / envoyer des données au support de persistence), qu'il les injecte dans les instances de Event avant leur utilisation et qu'il re-synchronise les données ensuite. Pour ces deux derniers points, plusieurs méthodes sont possibles :
    1. Créez des composants avec des types appropriés (nommé par exemple "title", "description", "startDate" et "endDate") du conteneur parent, et injectez-en les dépendances dans Event
      Attention : vous devrez utiliser une des méthodes d'injection fournies par PicoContainer permettant à l'injecteur de résoudre les dépendances ambiguës, Event dépendant désormais de plusieurs objets de même type.
      Autre remarque : Il est conseillé de tester sur un exemple à part l'injection de dépendances dans une hiérarchie de conteneurs, pour un composant du conteneur fils dépendant d'un composant du conteneur père, tout d'abord sur des conteneurs identiques, puis de choisir les types de conteneurs qui correspondent aux besoins de votre application.
    2. Créez vous-même une annotation et son processeur pour injecter en même temps les trois strings (et toujours le contexte) dans une instance de Event, et utilisez le builder du conteneur fils pour lui dire d'utiliser cette annotation pour résoudre les dépendances de Event.
  3. Dans tous les cas, vous devrez utiliser les méthodes de gestion du cycle de vie du composant Event pour que l'injection soit faite au moment où on doit l'utiliser et la sauvegarde ensuite. Remarquez que maintenant que la gestion de la persistance et l'appel au DAO est faite dans le conteneur, le code de Event est beaucoup plus simple et uniquement lié au métier de l'application.
  4. Rajoutez explicitement une opération de fermeture de session utilisateur (passivation des instances en cours d'utilisation)
  5. Mettez en place un mécanisme qui supprime une instance du conteneur fils si celle-ci n'a pas été utilisée depuis un certain temps.

À ce stade, vous avez dans votre application une arborescence de conteneurs gérant des dépendances entre leurs composants, en utilisant plusieurs types d'injection. Le conteneur père est plutôt destiné aux objets métier de l'application et le conteneur fils aux objets de données. Le conteneur fils tire parti des méthodes de gestion du cycle de vie des composants pour les sérialiser / désérialiser quand ils ne sont pas utilisés. Ainsi, il est capable d'économiser les ressources en n'instanciant qu'un faible nombre d'objets de données et en les réutilisant, plutôt que d'en créer et d'en détruire inutilement.
Si vous avez utilisé la méthode d'injection utilisant le pattern DTO, votre conteneur fils doit être suffisamment générique pour pouvoir gérer des pools d'instances d'une autre classe que Event, pour peu que le DTO injecté soit approprié. À quelques détails près, les éléments qui composent votre serveur sont donc tous génériques. Si le travail de gestion des types de composants, duur dépendances et duur persistance dans un fichier de configuration a été fait jusqu'au bout, vous devriez pouvoir vous resservir de votre framework pour une toute autre application que le calendar.

8. Gestionnaire d'entités

Vous allez à présent faire de votre conteneur fils un gestionnaire d'entités (similaire à un EntityManager de l'API JPA, en version simplifiée), dédié aux événements. On considérera que le titre d'un événement permet de l'identifier. Pour cela, créez une classe GestionnaireEntite (qui dérive de PicoContainer), et remplacez votre conteneur fils par une instance de cette classe. Il aura accès au DAO (En fonction de votre implémentation du contexte, pensez à lui donner les droits pour appeler la méthode getDao()...) et exposera les méthodes suivantes :

Remarques:

Enfin, modifier les méthodes métier du calendar pour utiliser le gestionnaire d'entités.

Bonus : RMI

Utilisez vos résultats du TP2 pour faire en sorte que votre serveur et votre client communiquent en RMI...

Licence Creative Commons
Valid XHTML 1.0 Strict