Accueil > Non classé > La réponse à la Grande Question sur la Vie, les Annotation Processors et le reste…

La réponse à la Grande Question sur la Vie, les Annotation Processors et le reste…

Introduction

Ne vous êtes-vous jamais demandé si vous utilisiez correctement une annotation ? Certes, pour certains cas comme @Deprecated, il faut vraiment en vouloir pour se tromper en l’utilisant. Mais quid d’un @Transactional ou d’un @AccessRole ? La plupart du temps, on s’attendra à les trouver sur des classes d’un package particulier (*.service), sur des méthodes ayant des paramètres précis, etc.

L'apocalypse des développeurs

Les annotation processors : l'apocalypse des développeurs ?

A l’inverse, on pourrait vouloir savoir comment sont utilisées certaines annotations, pour faire du reporting : quels sont les services auxquels aura accès un utilisateur disposant du droit "admin" ? Dans quelles classes mon framework doit-il injecter mon instance de FooBarPlopDAOImpl ? Brian va-t-il demander Penelope en mariage dans l’épisode 43 de la saison 28 d’Amour, Gloire et Clean Code ?

Autant de problématiques pouvant être résolues (en partie) grâce aux Annotation Processors, fonctionnalité assez peu connue apparue dans le JDK5. Je vous propose dans cet article de pousser un peu l’utilisation des annotations, dans la continuité des présentations faites par Olivier Croisier et d’un non moins génialissime article paru sur le blog Excilys récemment [1].

Où en étions-nous ?

Dans la précédente partie, je vous avais montré comment créer votre propre annotation, et comment s’en servir par la suite au runtime. L’annotation @ServerCallbackHandler définie pour notre remote controller Trackmania permet de mettre en place de manière automatique une sorte de pattern observer dans les plugins. Les méthodes sont ensuite appelées au moment adéquat avec un paramètre de type ServerCallbackEvent. Le problème est que la syntaxe des annotations ne permet pas de forcer leur utilisation sur une méthode ayant une signature particulière. Si la méthode annotée ne prend pas les bons paramètres, on devra lancer une exception lors du chargement du plugin (au runtime) pour manifester notre mécontentement.

Le gentil développeur voulant créer son propre plugin devra donc lire le pavé de 800 pages de specs sur la création de plugins (que je n’ai pas encore écrit, soit dit en passant), pour maîtriser toutes les contraintes de codage qui ne sont pas vérifiables par le compilateur. Vraiment pas vérifiables ? En fait si, il est possible "d’étendre" les règles du compilateur Java pour y inclure vos propres règles dictatoriales de codage. Ceci est possible en utilisant…

Les Annotation Processors

Depuis sa version 5, le JDK propose de traiter des annotations lors de la compilation d’un fichier Java. Ces annotation processor peuvent ainsi réaliser diverses vérifications / opérations sur les annotations gérées et ainsi compléter les règles de compilation standards de javac. Autrefois appelés via l’outil apt du JDK, les processeurs d’annotations sont utilisés via l’option -processor de javac depuis le JDK 6. Comme je suis jeune et moderne (et flemmard), je vais uniquement parler de leur utilisation en Java 6.

Pour créer notre propre processeur, on va devoir étendre la classe abstraite AbstractProcessor :


1
2
3
4
5
6
7
8
public abstract class AbstractProcessor implements Processor {
    protected ProcessingEnvironment processingEnv;

    //...
    public abstract boolean process(Set<? extends TypeElement> annotations,
                    RoundEnvironment roundEnv);
    //...
}

Dans cette version réduite d’AbstractProcessor, on remarque :

  • une variable membre processingEnv, un environnement de traitement permettant notamment d’avoir accès à des instances de Messager (pour afficher une erreur ou une info dans la console) et de Filer (permettant de créer à la volée de nouvelles ressources, comme des fichiers Java ou XML)
  • une méthode abstraite process(Set<? extends TypeElement>, RoundEnvironment), dans laquelle on va faire le traitement proprement dit des annotations supportées par le processeur
  • un paramètre roundEnv, représentant le round de traitement d’annotations en cours

La phase de traitement des annotations à la compilation est donc divisée en rounds. Les technotes de javac expliquent comment sont gérés ces rounds :

If any processors generate any new source files, another round of annotation processing will occur: any newly generated source files will be scanned, and the annotations processed as before. Any processors invoked on previous rounds will also be invoked on all subsequent rounds. This continues until no new source files are generated. After a round occurs where no new source files are generated, the annotation processors will be invoked one last time, to give them a chance to complete any work they may need to do. Finally, unless the -proc:only option is used, the compiler will compile the original and all the generated source files.

Un exemple de génération d’une classe grâce au Filer est disponible dans la documentation de Netbeans. Puisqu’une nouvelle classe aura été générée, un second round d’annotation processing aura lieu pour traiter cette nouvelle classe. Enfin, un troisième et dernier round aura lieu, comme spécifié plus haut.

Mon premier annotation processor

Définissons tout d’abord une annotation :


1
2
3
4
5
6
7
8
9
10
11
12
package test;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAnnotation {
    String value() default "";
}

L’annotation est donc préservée au runtime, et appliquable sur des méthodes. Sa valeur par défaut est une chaine vide. Voici un exemple d’annotation processor traitant cette annotation :


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package test;

import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;

@SupportedAnnotationTypes({"test.MyAnnotation"})
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class MyAnnotationProcessor extends AbstractProcessor {

    public MyAnnotationProcessor() {
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

        for (TypeElement annotation : annotations) {
            if (annotation.getQualifiedName().toString().equals(MyAnnotation.class.getName())) {
                for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) {
                    MyAnnotation myAnnotation = element.getAnnotation(MyAnnotation.class);

                    if (myAnnotation != null && "".equals(myAnnotation.value())) {
                        processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Please specify a value for MyAnnotation!", element);
                    }
                }
            }
        }
        return false;
    }
}

Les points importants que l’on peut noter en lisant ce code :

  • Les annotations que le processeur souhaite gérer sont définies grâce à l’annotation @SupportedAnnotationTypes.
  • Le source level supporté est défini grâce à @SupportedSourceVersion.
  • La récupération des méthodes annotées avec un @MyAnnotation n’est pas directe, il faut tout d’abord boucler sur les annotations supportées par le processeur, sélectionner celle qui nous intéresse, et enfin récupérer une liste d’Element (classe, package, méthode, …) possédant l’annotation.
  • L’auteur de cette classe n’est pas logique, puisqu’il a défini une valeur par défaut à "" alors que par la suite il interdit cette valeur grâce au processeur.
  • Il est possible de générer ses propres erreurs de compilation grâce au Messager.

En effet, ce Messager possède une méthode printMessage(Diagnostic.Kind kind, CharSequence msg, Element e) permettant d’afficher des erreurs ou des infos à la compilation. Le résultat en pratique est le suivant (notez l’utilisation du paramètre -processor pour l’activation de notre processeur) :


1
2
3
4
$ javac -processor test.MyAnnotationProcessor ClassToCompile.java
...\Tests\src\test\ClassToCompile.java:15: Please specify a value for MyAnnotation!
    public void theMethod() {
1 error

Le code compilé étant :


1
2
3
4
5
6
7
8
9
package test;

public class ClassToCompile {

    @MyAnnotation
    public void theMethod() {
        System.out.println("Foo");
    }  
}

Application : compilation rules enforcement

Revenons à nos moutons pour faire un processeur un peu plus utile. Nous allons vérifier que toutes les méthodes annotées avec @ServerCallbackHandler prennent exactement un paramètre de type ServerCallbackEvent :


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.excilys.blog.annotations;

import java.util.List;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.ElementKindVisitor6;
import javax.tools.Diagnostic.Kind;

@SupportedAnnotationTypes({"com.excilys.blog.annotations.ServerCallbackHandler"})
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class ServerCallbackHandlerAnnotationProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (TypeElement annotationType : annotations) {
            processAnnotatedMethods(roundEnv.getElementsAnnotatedWith(annotationType));
        }
        return false;
    }

    public void processAnnotatedMethods(Set<? extends Element> methods) {
        HasOneParameterMethodTypeVisitor visitor = new HasOneParameterMethodTypeVisitor();

        for (Element method : methods) {
            if (!method.accept(visitor, null)) {
                processingEnv.getMessager().printMessage(Kind.ERROR, String.format("Methods annotated with "
                        + " %s should have exactly one parameter of type %s ",
                        ServerCallbackHandler.class.getName(), ServerCallbackEvent.class.getName()), method);
            }
        }
    }
}

static class HasOneParameterMethodTypeVisitor extends ElementKindVisitor6<Boolean, Void> {

    @Override
    public Boolean visitExecutableAsMethod(ExecutableElement t, Void p) {
        List<? extends VariableElement> parameters = t.getParameters();

        if (parameters.size() != 1) {
            return Boolean.FALSE;
        }

        VariableElement param = parameters.get(0);

        return param.asType().toString().equals(ServerCallbackEvent.class.getName());
    }

}

Ici, je pense qu’une petite explication s’impose. La méthode processAnnotatedMethods() va être chargée de traiter tous les éléments annotés gérés par le processeur, dans notre cas cela se restreint donc à des méthodes @ServerCallbackHandler. En bouclant sur ces Element, on va vérifier les paramètres des méthodes grâce à un design pattern visiteur fourni par le JDK. Le visiteur que j’ai créé ici est une sous-classe de ElementKindVisitor6, qui facilite la visite d’élements exécutables (méthodes et constructeurs). Les paramètres typés de la classe permettent de spécifier un type de retour personnalisé, ainsi qu’un type de paramètre éventuel à passer à la méthode visitExecutableAsMethod(). J’ai choisi un retour booléen (annotation correcte ou pas) et aucun paramètre de visite (pas besoin).

C’est dans la méthode visitExecutableAsMethod() que va réellement se faire la vérification. Si la liste de paramètre contient un seul paramètre de type ServerCallbackEvent (la vérification se fait sur le FQN), alors on renvoie true, sinon on renvoie false et une erreur sera générée par le compilateur.

Autre application : génération de ressources

Autre exemple, la génération d’un fichier HTML listant les évènements gérés par plugin :


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@SupportedAnnotationTypes({"com.excilys.blog.annotations.ServerCallbackHandler"})
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class ReportingAnnotationProcessor extends AbstractProcessor {

    private Writer reportWriter;

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        if (roundEnv.processingOver()) {
            return false;
        }
        processingEnv.getMessager().printMessage(Kind.NOTE, "Generating report for ServerCallbackHandler usage.");
        try {
            beginReport();

            for (TypeElement annotationType : annotations) {
                processAnnotatedMethods(roundEnv.getElementsAnnotatedWith(annotationType));
            }

            endReport();
        } catch (IOException ex) {
            Logger.getLogger(ReportingAnnotationProcessor.class.getName()).log(Level.SEVERE, null, ex);
        }
        return true;
    }

    private void beginReport() throws IOException {
        FileObject report = processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", "report.html");
        reportWriter = report.openWriter();
        reportWriter.write("<html><body><h1>Usages of annotation " + ServerCallbackHandler.class.getName() + "</h1>\n");
    }

    public void processAnnotatedMethods(Set<? extends Element> methods) throws IOException {
        for (Element method : methods) {
            ServerCallbackHandler annotation = method.getAnnotation(ServerCallbackHandler.class);
            reportWriter.append("<hr/><b>Method " + method.toString() + "</b><br/>\n");
            reportWriter.append("Callbacks: " + Arrays.toString(annotation.callbacks()) + "\n");
        }
    }

    private void endReport() throws IOException {
        reportWriter.write("</body></html>");
        reportWriter.close();
    }

}

Le résultat sera une page HTML ressemblant à ceci :

Rapport HTML généré

Rapport HTML généré

Intégration aux IDEs récents

Les principaux IDE du marché gèrent les annotation processors à la volée, en affichant les erreurs directement dans l’éditeur (pour toi, Etienne !). Exemple sous Netbeans :

Outre le surlignage des erreurs, il est également possible de prendre en compte les nouvelles classes générées par les processeurs. Par contre, je ne garantis pas la stabilité et/ou la fiabilité de ces fonctionnalités :D.

Si vous souhaitez savoir comment configurer vos projets dans les IDE Netbeans, IntelliJ et Eclipse, vous pouvez faire un tour sur la documentation des validateurs Hibernate qui a un chapitre dédié aux annotation processors.

Conclusion

Malgré leur apparence un peu fourbe et compliquée, les annotation processors peuvent être vos amis dans beaucoup de cas. Si vous êtes architecte / tech lead par exemple, vous serez heureux de pouvoir vérifier automatiquement que vos standards de structure ou de codage sont respectés. Des documentations un peu hors-normes, propres à votre projet, pourront être générées à chaque mvn:site, éventuellement en ayant recours à des annotations "informatives" créées pour l’occasion.

La contrepartie de ces avantages est une relative complexité de mise en œuvre (la première fois ça fait un peu mal, mais après on s’y habitue). De plus, il ne faut pas oublier d’invoquer ces processeurs ! Je doute que beaucoup d’entre vous appellent encore javac à la main, donc il suffit d’intégrer le -processor MonProc à Maven/Ant, voire même d’utiliser la Service Provider Interface (SPI) adéquate du JDK 6 (sujet d’un prochain article, qui sait ? :D).

Et vous, que feriez-vous d’intéressant avec un annotation processor ? Lâchez vos comms !


[1] Et je ne dis pas ça parce que je connais personnellement l’auteur !

Share
  1. 21/12/2010 à 15:30 | #1

    article très interessant.

    J’ai écrit une abstraction pour la reflection java runtime / java compiletime (reflext.googlecode.com). Cette API vise à gommer les différences entre les deux mais aussi avant à fournir une API plus oo-ish pour la reflection APT qui est très “brute”.

  2. 25/07/2014 à 05:19 | #2

    You Can Buy Polo Clothes Online ,You Can Buy Them With Biggest Discount Here.
    tiffany wedding rings http://www.outlet-tiffany.net/tiffany-co-rings-c-7.html

  3. 26/01/2017 à 01:34 | #3

    Simply want to say your article is as astonishing. The clarity in your post is simply cool and that i can assume you’re
    knowledgeable on this subject. Fine along with your permission let me to clutch your RSS feed to
    stay up to date with forthcoming post. Thank you one million and please
    keep up the rewarding work.

  4. 22/04/2017 à 21:40 | #4

    Enquanto matéria é chocolate varias dúvidas brotam, especialmente se chocolate engorda e qual é
    a sua quantidade de calorias. http://rigginsphoto.com/gmedia/img_9112-jpg/

  5. 15/05/2017 à 20:13 | #5

    I don’t know whether it’s just me or if perhaps everybody else experiencing issues with your blog.

    It appears as if some of the text in your posts are running off the screen. Can somebody else please comment and let me know if this is happening to them too?
    This might be a problem with my web browser because I’ve had this happen previously.
    Cheers

  6. 01/09/2018 à 16:35 | #6

    I am not certain where you’re getting your info, however great topic.
    I must spend some time finding out much more or figuring out more.
    Thanks for excellent info I was on the lookout for this info for my mission.

  7. 10/09/2018 à 05:21 | #7

    It’s remarkable for me to have a web site, which is valuable in favor of my know-how.
    thanks admin

  8. 01/11/2018 à 08:21 | #8

    Tremendous things here. I am very glad to
    see your article. Thank you so much and I am taking a look forward to contact
    you. Will you kindly drop me a e-mail?

  1. 21/12/2010 à 15:16 | #1
  2. 14/03/2011 à 11:00 | #2
  3. 18/08/2014 à 14:51 | #3
  4. 15/04/2015 à 04:12 | #4
  5. 16/05/2015 à 02:46 | #5
  6. 09/07/2015 à 19:31 | #6
  7. 09/08/2015 à 17:13 | #7
  8. 31/08/2015 à 16:00 | #8
  9. 02/09/2015 à 07:36 | #9
  10. 26/09/2015 à 09:21 | #10
  11. 10/10/2015 à 00:27 | #11
  12. 21/10/2015 à 10:27 | #12
  13. 26/10/2015 à 06:34 | #13
  14. 27/11/2015 à 00:57 | #14
  15. 24/12/2015 à 02:03 | #15
  16. 17/02/2016 à 04:41 | #16
  17. 24/02/2016 à 13:04 | #17
  18. 12/04/2017 à 02:26 | #18