Salut le forum,
Un petit article sous forme de post, en espérant avoir vos avis/retours.
L'objectif est de montrer une utilisation concrête des annotations et de l'introspection, afin de montrer leur intérêt. Pour moi, l'intérêt majeur est la possibilité d'ajouter des fonctionnalités dans un code source sans trop en altérer la structure.
Contexte
J'utilise la bibliothèque XStream pour sérialiser et désérialiser des objets Java. En particulier dans le cadre du développement d'un framework, l'utilisateur du framework spécialise des objets par héritage et ajoute des propriétés à ces objets. L'intérêt d'XStream est alors d'obtenir un fichier XML qui permet d'initialiser toutes ces propriétés qui sont ensuite utilisées dans le framework.
Limite d'Xstream
Xstream utilise le mot-clé transient pour exclure des attributs de classe du processus de sérialisation et désérialisation. C'est très pratique dans mon cas, puisque les attributs de classe définis par l'utilisateur peuvent des propriétés intéressantes pour la configuration du comportement global, mais ces attributs peuvent aussi n'être que fonctionnels et servir à stocker des objets liés au calcul par exemple.
Dans le processus de désérialisation, touts les attributs propriétés (non transient) sont donc correctement initialisés. En revanche, j'ai aussi souvent besoin d'initialiser les attributs fonctionnels (transient) avant toute exécution de mon processus global.
Exemple
Par exemple, si dans une de mes classes, j'utilise une distribution statistique pour générer des nombres pseudo-aléatoires qui a besoin de deux paramètres pour s'initialiser, j'aurai trois attributs dans ma classe, les deux paramètres de la distribution (non transient), et l'objet représentant ma distribution qui lui est purement fonctionnel et sera donc transient.
Ce qui donne quelquechose comme ça :
Une fois ma classe désérialisée, j'aurai mes attributs a et b correctement initialisés, par contre l'attribut maDistribution sera null.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4 class MaClasse { double a,b; transient MaDistribution maDistribution; }
Il faut donc que je crée une méthode qui effectuera cette initialisation, par exemple :
Oui mais ensuite comment appeler cette méthode au bon moment dans le processus de désérialisation ?
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4 public void initMaDistribution() { maDistribution = new MaDistribution(a,b); }
Une solution pourrait bien sûr de définir une interface avec une méthode qui serait appelée systématiquement. Il suffirait donc que MaClasse implémente cette interface, et définisse le corps de la fonction spécifié par l'interface. Ce serait simple.
Oui, mais. Cette étape d'initialisation n'est pas systématique, et obligerait donc à mettre la fonction de l'interface avec un corps vide.
Et c'est là que l'annotation est utile. Car elle va permettre de proposer une fonctionalité qui est très peu invasive dans le code.
L'annotation et son utilisation
Voilà l'annotation que je définis :
L'utilisation est simple, il suffit d'annoter la méthode que l'on veut exécuter à l'initialisation, comme cela :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public static @interface InitTransientParameters {}
Ensuite comment appeler les méthodes annotées ? Et bien, en plus de l'annotation, j'ai défini deux méthodes.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10 class MaClasse { double a,b; transient MaDistribution maDistribution; @InitTransientParameters public void initMaDistribution() { maDistribution = new MaDistribution(a,b); } }
La première permet de récupérer les méthodes annotées de la sorte dans une classe, en prenant soin de chercher ces méthodes dans toutes les super-classes :
Et la deuxième qui permet à l'aide de la première, d'exécuter ces méthodes sur un objet :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13 public static List<Method> fetchInitMethods(Class type) { List<Method> methods = new ArrayList<Method>(); Class inheritanceNavigator = type; while (inheritanceNavigator != null) { for (Method method : inheritanceNavigator.getDeclaredMethods()) { if (method.isAnnotationPresent(InitTransientParameters.class)) methods.add(method); } inheritanceNavigator = inheritanceNavigator.getSuperclass(); } return methods; }
À partir de là, je peux dans mon framework, après avoir désérialiser un objet, appeler la méthode callAnnotedMethods sur cet objet pour effectuer les éventuels traitement d'initialisation.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7 public static void callAnnotedMethods(Object object) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { for (Method method : TransientParameters.fetchInitMethods(object.getClass())) { method.setAccessible(true); method.invoke(object); } }
Remarques
- La méthode annotée ne doit pas avoir d'argument, sinon une exception est levée
- L'introspection nécessaire pour obtenir les méthodes annotées a un certain coût à l'exécution. Dans mon contexte, il ne s'agit que de la phase d'initialisation qui est négligeable par rapport à l'exécution complète.
- Il y avait peut-être d'autres solutions possibles, mais je n'ai pas trop cherché. Cette solution me paraissait simple et séduisante.
Merci de m'avoir lu
Partager