L'injection dans Eclipse 4

Image non disponible

Le but de cet article est de démystifier le mécanisme d'injection et de décrire les annotations associées et utilisées dans Eclipse 4.

Le comportement implicite de ces annotations est également décrit.

Pour réagir au contenu de cet article, un espace de dialogue vous est proposé sur le forum 4 commentaires Donner une note à l'article (5).

Article lu   fois.

L'auteur

Site personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

Dans tous les tutoriels que l'on peut trouver sur Eclipse 4, beaucoup considèrent que le lecteur manipule depuis longtemps les annotations et l'injection, et peu d'informations sont données sur le fonctionnement interne de tous ces mécanismes.

Le but de ce premier article est de démystifier les annotations et leur fonctionnement, et notamment celles qui concernent le mécanisme de base de l'injection.

Je détaillerai ensuite dans un autre article comment utiliser les autres annotations.

J'espère qu'il vous aidera à mieux comprendre la puissance de ce concept et l'intérêt de les utiliser dans Eclipse 4.

II. Qu'est ce qu'une annotation ?

L'annotation est définie par le langage Java dès sa version 1.5. Il s'agit simplement d'un élément du langage qui peut être associé à une classe, une méthode, un constructeur, un champ ou un paramètre.

Elle se définit dans un fichier java du nom de l'annotation et se déclare avec la notation @interface. L'annotation doit aussi définir sa cible d'application directement dans sa définition. Par exemple l'annotation d'injection est définie de la manière suivante :

 
Sélectionnez
@Target({ METHOD, CONSTRUCTOR, FIELD })
@Retention(RUNTIME)
@Documented
public @interface Inject {}

Cette définition se fait également par des annotations réservées pour les annotations !

Il est également possible de définir des annotations avec des paramètres, en créant à l'intérieur des méthodes qui donnent le type de la valeur et leur valeur par défaut. Par exemple, l'annotation de préférence d'Eclipse 4 est définie de la manière suivante :

 
Sélectionnez
@Qualifier
@Documented
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface Preference {
    String value() default ""; // key in the node

    String nodePath() default "";
}

Je ne développerai pas plus la déclaration des annotations en Java, de nombreux articles le font déjà, l'essentiel étant de retenir qu'une annotation s'applique à une cible et peut donc être utilisée par la suite, en utilisant l'introspection Java.

III. Le rôle de l'introspection java.

Les annotations n'ont aucun comportement dynamique associé dans le langage. Pour leur donner un sens, il faut qu'à un moment donné, la classe contenant les annotations soit introspectée pour analyser ses annotations et pour lui appliquer un traitement particulier.

Ce traitement peut, par exemple, intervenir lors de la compilation de la classe pour générer du code complémentaire. Dans ce cas, c'est le compilateur qui analyse le code et effectue le comportement.

Il faut donc bien comprendre que les annotations ne sont rien sans un traitement derrière qu'il faut effectuer.

C'est donc, lors du runtime, qu'Eclipse 4 va exploiter les classes annotées qu'il va traiter au moment voulu !

Toute la difficulté est donc de connaître ces moments et de savoir à quoi vous avez accès. Eclipse 4 dispose d'un renderer qui permet d'afficher votre application décrite dans le modèle d'application.

C'est donc ce renderer qui va analyser la classe traitée à un moment donné (par exemple un Part est analysé au moment de l'affichage d'une perspective), et qui va ensuite appeler les méthodes possédant telle ou telle annotation.

Tout se passe donc par introspection et la logique séquentielle de votre programme va en être un peu bouleversée (la recherche dans la pile d'appel lors du debug aussi !).

Prenons l'exemple de l'annotation @PostConstruct, que l'on peut poser sur une méthode. Il faut savoir que cette méthode ne sera appelée qu'une fois et après que tous les comportements d'injection aient été appliqués (j'y reviendrai tout à l'heure).

Donc si on ne connaît pas la logique du moteur qui exploite les annotations, il va être difficile de comprendre le séquencement d'appel et les endroits où annoter.

Détaillons donc maintenant un peu plus le fonctionnement de l'annotation d'injection, qui est la base de tout le système, et qui est également implicitement utilisée par toutes les autres annotations ensuite.

IV. L'annotation @Inject

Le mécanisme d'injection consiste à déléguer l'instanciation des objets à un injecteur qui aura pour rôle ensuite d'injecter les instances créées aux endroits voulus.

Comme je l'ai dit précédemment, cette annotation ne peut fonctionner que si elle est exploitée dans un contexte de création par un injecteur.

Donc si vous avez écrit une classe avec des annotations @Inject, n'espérez pas que le mécanisme soit pris en charge, si vous instanciez votre classe avec un « new » traditionnel en java !

Il faudra appeler l'injecteur (Cf ContextInjectionFactory) pour construire chaque objet afin que ses annotations soient traitées. Le contexte d'injection peut alors être reçu par injection ! Le schéma suivant résume la situation :

Image non disponible

Concernant la logique d'appel, l'annotation @Inject fonctionne de la manière suivante :

  • Appel du constructeur,
  • Initialisation des champs,
  • Appel des méthodes injectées.

On obtient donc la situation :

Image non disponible

Il faut faire attention, car la logique est encore subtile pour chacune des parties :

IV-A. Appel du constructeur

Il se fait directement par l'injecteur. Mais quel constructeur choisir si plusieurs ont été définis puisqu'on ne l'appelle pas explicitement ?

Pour le constructeur, l'injecteur va choisir celui qui possède une annotation d'injection et qui possède le maximum de paramètres injectables. Ainsi si vous avez un constructeur avec 3 paramètres, dont 2 seulement peuvent être injectés, ce constructeur ne sera pas appelé.

Autre petite caractéristique qui a son importance, le constructeur n'a jamais accès aux champs injectés de la classe, car ils seront initialisés après son appel. Par contre, il peut manipuler les champs qui ne possèdent pas d'annotation @Inject.

IV-B. Injection des champs

Chaque champ de la classe, précédé d'une annotation @Inject sera initialisé, une fois le constructeur appelé et ayant marché (s'il y a eu une exception, tout s'arrête).

Si un champ ne peut pas être injecté (valeur nulle), l'injecteur génère une exception. Si ce cas peut se produire, il faut précéder le champ de l'annotation @Optional.

Si votre objet a des champs hérités injectés, ces derniers seront initialisés avant.

IV-C. Appel des méthodes injectées

L'instanciation de la classe possédant les annotations va se terminer par l'appel de chaque méthode possédant une annotation @Inject.

Tous les paramètres attendus par ces méthodes doivent pouvoir être injectés (à moins qu'ils soient précédés d'une annotation @Optional) sous peine d'exception.

L'ordre d'appel des méthodes injectées est indéterminé. Toutefois, les méthodes injectées héritées sont appelées avant celles de la classe.

Et voilà votre objet a été initialisé automatiquement et intégralement… mais ce n'est pas tout !

Un comportement supplémentaire à connaître :

C'est aussi là que l'injection apporte sa magie, car chaque méthode sera rappelée automatiquement, si l'un de ses paramètres change et ce à tout moment. De même un attribut injecté sera remis à jour automatiquement si la dernière valeur injectée a changé.

Ainsi si vous définissez une méthode @Inject dont l'un des paramètres est par exemple la sélection courante, cette méthode sera appelée à chaque modification de la sélection courante.

Ce mécanisme fonctionne avec tous les objets reçus et remplace partiellement le mécanisme des listeners, plus traditionnel en Eclipse 3.

Ainsi dans l'exemple suivant :

 
Sélectionnez
@Inject
public void setSelection(@Optional @Named(IServiceConstants.ACTIVE_SELECTION) Object o, Adapter adapter)
{
    Rental r = adapter.adapt(o, Rental.class);
    setRental(r);
}

La méthode sera appelée lors de la construction de l'objet qui la contient, mais également à chaque moment où un des deux objets passés en paramètre change de valeur dans l'injecteur. Pour ce cas, l'Adapter ne changera pas, car c'est un singleton dans Eclipse 4, par contre, la sélection active (passée avec une annotation @Named) changera régulièrement et sera automatiquement réinjectée dans cette méthode !

Remarque : On notera dans cet exemple, que le paramètre injecté étant de type « Object », il doit être récupéré par un nom unique, qui, dans notre cas de sélection, est fourni par l'interface IServiceConstants d'Eclipse 4. Il serait aussi possible de remplacer le type Object attendu, par le type Rental. Dans ce cas, la méthode ne sera appelée que si la sélection courante est une instance de Rental.

Voilà donc l'une des subtilités de l'injection qui est aussi utilisée dans Eclipse 4 ! Et attention, là nous n'avons décrit que le mécanisme d'injection, nous n'avons pas encore parlé des annotations spécifiques utilisées par le renderer (objet d'un autre article).

V. Quatre annotations complémentaires pour finir.

Mais l'annotation @Inject seule ne suffit pas à couvrir tous les cas. En effet, l'ordre d'appel des méthodes annotées avec @Inject n'étant pas déterminé, il peut être difficile de s'y retrouver. Certains paramètres également peuvent correspondre à plusieurs instances possibles dans l'injecteur, ou peuvent être optionnels. Le développeur dispose donc d'annotations complémentaires permettant de traiter ces différents cas.

V-A. @PostConstruct

Cette annotation ne s'applique que sur une méthode qui sera appelée une fois toutes les injections terminées. Cette annotation permet ainsi de finaliser l'initialisation d'un objet. Elle est utilisée par exemple pour créer le contenu d'une vue dans Eclipse 4, pour initialiser un Addon, pour brancher des listeners, etc.

V-B. @PreDestroy

Comme @PostConstruct, elle ne s'applique que sur une méthode qui sera appelée juste avant la destruction d'un objet. On peut se servir de cette annotation pour indiquer une méthode qui enlèvera les éventuels listeners ou qui gérera les subtilités de désallocation.

V-C. @Optional

À mettre devant un paramètre de méthode ou un champ de classe pour indiquer que l'on gère le fait qu'il n'y ait pas de valeur.  N'oubliez pas que si aucune valeur ne peut être injectée, une exception sera levée.

V-D. @Named

Permet, associé à un nom passé en paramètre de l'annotation, de retrouver un objet nommé stocké dans l'injecteur. On utilise cette annotation quand le type recherché peut avoir plusieurs instances possibles.

V-E. Que peut on injecter ?

Là, il faut aussi être intuitif et on peut arriver à s'en sortir presque à chaque fois avec un peu de réflexion (c'est le cas de le dire !).

Déjà il faut bien comprendre qu'Eclipse va gérer pour vous des contextes d'injection (IEclipseContext) hiérarchiques initialisés au fur et à mesure des traitements.

Par exemple, quand l'application démarre, un contexte global est créé. Ce contexte va contenir tous les services OSGi utilisés par Eclipse 4. Puis quand le renderer va s'exécuter, il va créer un contexte pour l'application model (fils du contexte principal). Ensuite, une perspective se crée, elle aura aussi son propre contexte relié au contexte parent. Idem pour le Part.

Donc, logiquement, le renderer va vous fournir les instances nécessaires pour que votre méthode puisse fonctionner.

C'est pour cela que si on injecte une instance de Component dans la méthode de création du contenu d'une vue, ce Component est forcément le parent dans lequel la vue doit se créer ! Ce n'est pas magique. C'est le comportement dynamique du renderer qui à un moment donné l'initialise pour vous.

Si vous arrivez ensuite dans un autre part, le Component injecté sera bien sur différent !

On peut donc se dire, que si on a besoin de quelque chose à un endroit, normalement il est disponible !

Plus précisément, dans Eclipse 4, les objets fournissant un nouveau contexte dans le cadre de l'application model héritent de l'interface MContext. On trouve par exemple : MApplication, MWindow, MPerspective, MPart, MPopupMenu. Sur chacun de ces objets, on pourra récupérer le contexte en appelant la méthode getContext().

Dans un prochain article je vous détaillerai les autres annotations d'Eclipse 4, et surtout le moment où celles-ci sont utilisées.

VI. Conclusion

Comme on peut le constater, l'injection est un mécanisme très puissant, avec des comportements implicites qu'il faut bien maitriser. Une fois cette connaissance acquise, on découvre une nouvelle manière de programmer, plus souple et plus puissante, qui est la base du développement d'application Eclipse RCP version 4.

VII. Remerciements

Cet article a été publié avec l'aimable autorisation de la société OPCoachOPCoach.

Cet article n'aurait pas pu voir le jour sans l'aide de djibril, dont le support pour les outils perl était indispensable, mais aussi de Mickaël BARON, responsable de la rubrique Eclipse et Java, ainsi que de Vincent (zoom61) pour la correction orthographique. Merci à vous tous.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2012 Olivier Prouvost. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.