M2TI : méthodes
de conception de SI distribués
TP 1 : conteneurs d'objets et inversion
de contrôle
Application source
Vous allez travailler sur une application de gestion de
bookmarks de
sites web dont le diagramme de classes UML est disponible ici. Pour gagner du
temps, une
implémentation basique de cette appication est accessible là.
Intégrez les sources de cette application dans un projet
Eclipse et exécutez-la.
Dans son état actuel, la fonction main de l'application est
située dans la classe d'interface utilisateur (AnnuaireUI), qui
instancie une classe contrôleur (Annuaire), laquelle instancie
à son tour les autres classes de l'application, et notamment la
classe d'accès aux données (SiteXMLDAO). Vous allez
remanier la structure de cette application pour qu'elle soit conforme
à quelques patrons de conception de base.
1. Mise en place d'un conteneur
Pour cela, vous allez tout d'abord créer une arborescence de
trois packages "annuaire", "serveur" et "client", le premier étant le
père des des suivants. Créez ensuite deux classes supplémentaires :
- une classe Serveur, qui masque l'ensemble des objets
en lien
avec l'annuaire, à l'exception du client (AnnuaireUI), et qui
instanciera un conteneur pour gérer le cycle de vie et
l'accès à ces objets,
- une classe contenant la fonction main, créant un serveur
et un client et passant à ce dernier une référence
sur l'annuaire créé par le serveur.
Indications
- Le conteneur côté serveur sera basé sur l'outil PicoContainer.
Vous téléchargerez sur le site la dernière version du jar ou la version
disponible en local ici. La Javadoc est disponible en local là
Commencez par lire la première page d'introduction à l'utilisation de
cet outil, située ici.
- Modifications nécessaires de la classe Annuaire :
- la première chose à faire est de modifier
l'instanciation en chaîne d'une liste et d'un objet SiteXMLDAO dans le
constructeur ; modifiez ce constructeur pour que les dépendances se
fassent sur des instances des objets ArrayList et SiteDAO (et non
sur une valeur de nom de fichier permettant de créer un DAO)
Les dépendances seront alors les
suivantes :
Annuaire -> ArrayList
Annuaire -> SiteXMLDAO
SiteXMLDAO -> valeur du nom du fichier XML (String)
- faites en sorte que la classe Annuaire implémente
l'interface Startable et ajoutez-y les méthodes requises ; dans la
méthode start, appelez la méthode initSites() d'Annuaire et rajoutez un affichage indiquant que le serveur a démarré
et le nom du fichier XML où sont stockées les données
- Méthodes de la classe Serveur :
- constructeur
: il instanciera un ContainerBuilder, un
DefaultPicoContainer, puis y rajoutera quatre composants avec des
dépendances entre eux, et appellera ensuite la méthode start() de
l'annuaire
- méthode
(provisoire) de service : getAnnuaire(), renvoyant au
client une référence vers le composant annuaire instancié par le
conteneur.
A ce stade, vous avez réalisé une
inversion de contrôle et créé un framework permettant d'instancier et
de gérer vos objets côté serveur.
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 de l'annuaire créée dans le conteneur. Pour
cela, vous allez implémenter le paradigme requête-réponse :
- Modifiez la méthode de service du serveur pour qu'elle soit
plus générique ; par exemple :
public String traiteRequete(String commande, HashMap<String,
String> parametres);
où les éléments de la HashMap représentent les paires nom / valeur des
paramètres des requêtes
- Dans l'annuaire, passez les trois méthodes add, remove et
listSites en privé, et créez une méthode publique "process", qui
appellera l'une de ces trois méthodes en fonction de la commande
- Modifiez le code du client pour qu'il n'appelle plus que la
méthode traiteRequete() du serveur
Remarquez que la classe Serveur masque désormais complètement
l'implémentation du traitement des requêtes dans le programme principal
et dans le client. Il suffit de connaître son API pour utiliser
l'annuaire.
2.2. Uniformisation
Plutôt que d'avoir un objet Annuaire 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 :
- Commencez par définir une interface et une classe abstraite
reprenant les principales caractéristiques de l'annuaire : dépendances,
implémentation de Startable et méthode de service process()
- Créez trois classes implémentant ce modèle et correspondant
aux trois méthodes de service add(), remove() et listeSites()
- Modifiez le constructeur du serveur pour que votre
conteneur crée les composants correspondants aux instances de vos trois
nouvelles classes
- Faites l' "aiguillage" vers les instances de chacune de ces
classes au niveau de la méthode de service du serveur : la commande
correspond au nom de la classe à appeler, comme un nom de ressource sur
un serveur Web
A ce stade, vous avez réalisé un outil
équivalent à un conteneur de servlets.
3. Création d'un contexte applicatif
L'objet SiteXMLDAO est commun pour tous les Site dans le
conteneur que vous venez de créer. Vous allez implémenter une classe
SiteContext
qui permettra à chaque instance de Site créée d'accéder à ce DAO en
respectant le pattern Context présenté en cours. Pour cela, vous allez
en fait rajouter un niveau d'indirection
entre Site et le DAO, qui vous permettra par exemple de modifier par
configuration le DAO utilisé pour gérer la persistence des données de
l'application.
3.1. Création du contexte
Pour rajouter une indirection entre le conteneur (et
ses composants) et les objets de type SiteDAO, vous allez créer
une interface "SiteContext" et sa classe d'implémentation, de
façon à ce que :
- l'instanciation du DAO soit paramétrable en fonction d'un
"type" de DAO et d'un nom de
fichier (ici, en XML, mais on peut aussi imaginer d'utiliser une BD ou
un
ORM pour la persistance),
- le contexte permette aux différents composants d'obtenir
une référence sur
l'objet DAO instancié, via
une méthode publique getSiteDao().
Exemple de code pour ces deux éléments :
public interface SiteContext {
//Méthodes de configuration du contexte :
//Spécification d'un type de DAO et du
fichier (si XML) ou du port (si SGDB) qui va avec
public void setDaoByType (String type,
String FileOrPort) throws SiteDAOCreationException;
//Utilisation d'une instance de DAO
existante
public void setSiteDAO(SiteDAO dao);
//Méthode de service du contexte
public SiteDAO getSiteDao();
}
public class SiteContextImpl
implements SiteContext {
SiteDAO dao;
public void setDaoByType (String type, String FileOrPort) throws
SiteDAOCreationException {
if (type.equals("XML")) {
dao = new SiteXMLDAO (FileOrPort);
System.out.println("SiteContext : DAO initialisé : type = " + type + "
; valeur = " + FileOrPort);
} else { // rajouter ici d'autres types éventuels de DAO
throw new SiteDAOCreationException ("Unrecognized DAO type : " + type);
}
}
public void setSiteDAO(SiteDAO dao) {
this.dao = dao;
}
public SiteDAO getSiteDao() {
return this.dao;
}
}
public class SiteDAOCreationException
extends Exception {
public
SiteDAOCreationException (String message) {
super(message);
}
}
3.2. Modification de l'arbre de dépendances
L'objectif est ici d'intégrer l'objet SiteContext dans votre
conteneur et de modifier les composants du conteneur ayant
une dépendance sur un objet SiteDAO pour qu'ils dépendent de
SiteContext
- Supprimez
la création d'un objet SiteXMLDAO dans le conteneur de votre serveur et
créez à la place une instance de ContextImpl en tant que composant de
ce conteneur.
Attention :
il faut créer ce composant avec une propriété de type "caching", pour
que la valeur de la variable globale "dao" de la classe ne soit pas
perdue à chaque réutilisation de l'instance de SiteContextImpl par les
différents composants du conteneur.
container.as(org.picocontainer.Characteristics.CACHE).addComponent("Context",
SiteContextImpl.class);
- Modifiez les constructeurs de vos classes d'implémentation
d'Annuaire (abstraites ou non), de façon à ce qu'ils prennent en
paramètre un SiteContext et non plus un SiteDAO. Cela pour que le
conteneur soit capable de résoudre les dépendances entre ses composants.
- Si vos objets Site ont également un constructeur de ce
type, faites de même.
3.3. Initialisation et utilisation du contexte
Enfin, initialisez la variable dao dans votre contexte avant
le démarrage des différents composants Annuaire (méthode "start()"), en
utilisant l'une des méthodes de configuration (tant qu'à faire, la
première) :
try {
contexte.setDaoByType("XML", "test.xml");
} catch (SiteDAOCreationException e) {
e.printStackTrace();
}
Dans la méthode start de Site, appelez la méthode
getDao() du contexte pour positionner la variable "dao" avant
l'utilisation du DAO.
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 de l'objet DAO (si la classe
SiteContextImpl est implémentée de façon plus approfondie).
4. Gestion du cycle de vie des objets
Actuellement, l'annuaire gère une liste de Site, qu'il
augmente à chaque nouvel enregistrement. Tous n'ont pourtant pas besoin
d'être instanciés en même temps. Vous allez remplacer cette liste par
un conteneur (tant qu'à faire, utilisant un autre type d'injection de
dépendances que le premier), modifier vos objets Site pour qu'ils
soient compatible avec cette nouvelle implémentation, et en gérer le
cycle de vie. Pour cela, vous allez suivre les étapes suivantes :
- Vous allez remplacer
cette structure de stockage par un
nouveau conteneur, fils du premier (voir partie "Container hierarchies"
de l'introduction sur le site picocontainer) dans la classe Serveur.
Pour cela :
Reprenez le code de votre serveur pour que celui-ci
déclare un
deuxième
conteneur. Vous passerez au constructeur de ce conteneur les paramètres
suivants :
- une ComponentFactory "new AnnotatedFieldInjection()"
(c'est-à-dire qu'il permettra de résoudre les
dépendances exprimées sous forme d'annotations de champs ; ici, une
variable globale),
- une référence au conteneur parent, pour permettre la
résolution de dépendances entre ses composants et ceux du père.
//Création d'un
conteneur fils de "container", de type AnnotatedFieldInjection : 2
méthodes
//Méthode utilisant un builder
PicoBuilder builder = new
PicoBuilder(container);
MutablePicoContainer sites =
(DefaultPicoContainer)
builder.withAnnotatedFieldInjection().build();
//Méthode utilisant un constructeur
spécifique
//MutablePicoContainer sites = new
DefaultPicoContainer(new AnnotatedFieldInjection(), container);
//Rajout en tant que composant du
conteneur principal (pour la propagation des dépendances)
container.addComponent(sites);
Supprimez la référence au DAO dans le constructeur de Site.
En revanche, rajoutez à
Site une variable globale de type "SiteDAO", et précédez sa déclaration
d'une annotation @Inject
(de org.picocontainer.annotations).
En
tant que composant, Site doit implémenter la méthode start() de
l'interface Startable, dont vous vous servirez plus tard.
-
Faites en sorte que votre
serveur instancie dans ce deuxième conteneur autant
de
composants Site qu'il y en a dans l'annuaire. Pour cela, il faut :
- créer
une méthode d'initialisation du conteneur fils dans vos annuaires qui
utilise la méthode listSites() du DAO pour peupler le conteneur et qui
sera appelée (une seule fois !) depuis la méthode start() de votre
annuaire,
public void initListe() {
Vector<Site> v =
dao.getAllSites();
Site s;
int i=0;
for(Iterator<Site> iter =
v.iterator() ; iter.hasNext() ;) {
s =
iter.next();
s.setContext((SiteContextImpl) sites.getComponent("Context"));
String
name = "Site" + i;
sites.addComponent(name, s);
i++;
}
}
Remarque :
ici, la gestion des composants dans le conteneur fils est faite en
fournissant à chaque instance créée un nom incluant son numéro d'index.
Ce nom doit rester synchronisé avec l'ordre dans la liste, notamment
lors de la suppression de sites. D'où le code un peu complexe de la
méthode removeSite(), plus bas... - modifier la méthode add de l'annuaire pour qu'elle
rajoute effectivement un nouveau site dans le conteneur fils,
private boolean addSite(String desc, String url) {
String name = "Site" +
sites.getComponents().size();
Site s = new Site(desc, url);
s.setContext((SiteContext)
sites.getComponent("Context"));
sites.addComponent(name, s);
try {
s.start();
s.save();
s.stop();
return
true;
} catch (Exception e) {
e.printStackTrace();
return
false;
}
} - modifier
la méthode remove pour qu'elle supprime la référence aux sites effacés
du conteneur fils (et qu'elle décale les suivants s'ils sont gérés avec
un nom qui dépend de leur numéro d'ordre).
private int removeSite(String desc, String url) {
//Remarque : on construit un Site sans
contexte,
uniquement pour utiliser sa méthode equals()
Site s = new Site(desc, url);
int result = 0;
List<java.lang.Object>
siteslist = sites.getComponents();
Site temp;
int i=0;
try {
for(Iterator<java.lang.Object> iter =
siteslist.iterator();
iter.hasNext() ;) {
temp = (Site) iter.next();
if (s.equals(temp)) {
temp.start();
temp.delete();
sites.removeComponent("Site" + i);
//On coupe le lien entre le conteneur et
le Site
// Plus aucune référence n'existe sur
cet objet
// On attend que le garbage
collecting passe...
sites.removeComponentByInstance(temp);
temp.stop();
result++;
} else {
//il faut changer les noms de référence
des
composants suivant ceux qu'on a enlevés,
//sans quoi on aura un problème pour en
rajouter
d'autres...
if
(result>0) {
sites.addComponent("Site" +
(i-result), sites.getComponent("Site"+i));
sites.removeComponent("Site"+i);
}
}
i++;
}
s.stop();
} catch (Exception e) {
e.printStackTrace();
result =
-1;
}
return result;
}
A
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. Dans la question suivante, vous devrez
détruire une partie de ce travail. En conséquence, quand votre
application fonctionnera, sauvegardez-la et continuez dans un autre
projet.
- Votre application n'accède jamais à toutes les instances de
Site en
même temps. A condition de les synchroniser avec votre support de
persistance, vous pouvez vous passer de les avoir toutes à disposition.
Pour cela :
- modifiez votre conteneur fils pour qu'il
n'instancie qu'un seul objet Site (plus besoin, donc, de de spécifier
de nom pour ce composant) ; vous affecterez à ce composant un comportement (par
exemple "Caching" ou
"ThreadCaching"), permettant
de conserver les valeurs des variables qui restent stables pour tous
les sites, comme la référence au contexte ou au DAO,
- créez
deux composants de type String (nommé par exemple "Description" et
"URL") du conteneur parent, et injectez-en les dépendances dans Site,
Attention :
vous devrez paramétrer l'injecteur à la création du composant pour que
le conteneur arrive à résoudre les dépendances, Site dépendant
désormais de deux objets de même type. - modifiez le code des
différents objets de l'application
pour l'adapter au nouveau fonctionnement : une instance de Site existe
dans le conteneur, et tous les objets y accèdent tour à tour en
positionnant les valeurs de la description et d'url du site dans les
composants ad hoc.
5. 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 utilisez ces données dans la classe Serveur lors de l'instanciation
des éléments des conteneurs et du contexte.