Implémentation d’une annotation Lombok personnalisée

Implémentation d'une annotation Lombok personnalisée

1. Vue d'ensemble

Dans ce didacticiel,we’ll implement a custom annotation using Lombok to remove the boiler-plate around implementing Singletons in an application.

Lombok est une puissante bibliothèque Java qui vise à réduire le code de la chaudière en Java. Si vous ne le connaissez pas, vous trouverez icithe introduction to all the features of Lombok.

2. Lombok en tant que processeur d'annotation

Java permet aux développeurs d'applications de traiter les annotations pendant la phase de compilation. le plus important, pour générer de nouveaux fichiers basés sur une annotation. En conséquence, des bibliothèques telles que Hibernate permettent aux développeurs de réduire le code de la plaque de la chaudière et d’utiliser plutôt des annotations.

Le traitement des annotations est traité en détail dans cetutorial.

De la même manière,Project Lombok also works as an Annotation Processor. It processes the annotation by delegating it to a specific handler.

Lors de la délégation,it sends the compiler’s Abstract Syntax Tree (AST) of the annotated code to the handler. Par conséquent, il permet aux gestionnaires de modifier le code en étendant l'AST.

3. Implémentation d'une annotation personnalisée

3.1. Étendre Lombok

Étonnamment, Lombok n’est pas facile à étendre et à ajouter une annotation personnalisée.

En fait,the newer versions of Lombok use Shadow ClassLoader (SCL) to hide the .class files in Lombok as .scl files. Thus, it forces the developers to fork the Lombok source code and implement annotations there.

Du côté positif,it simplifies the process of extending custom handlers and AST modification using utility functions.

3.2. Annotation Singleton

Généralement, beaucoup de code est requis pour implémenter une classe Singleton. Pour les applications qui n’utilisent pas de cadre d’injection de dépendances, il s’agit simplement de choses standard.

Par exemple, voici une façon d’implémenter une classe Singleton:

public class SingletonRegistry {
    private SingletonRegistry() {}

    private static class SingletonRegistryHolder {
        private static SingletonRegistry registry = new SingletonRegistry();
    }

    public static SingletonRegistry getInstance() {
        return SingletonRegistryHolder.registry;
    }

    // other methods
}

En revanche, voici à quoi cela ressemblerait si nous implémentions une version annotée de celle-ci:

@Singleton
public class SingletonRegistry {}

Et, l'annotationSingleton:

@Target(ElementType.TYPE)
public @interface Singleton {}

Il est important de souligner icithat a Lombok Singleton handler would generate the implementation code we saw above by modifying the AST.

Comme l'AST est différent pour chaque compilateur, un gestionnaire personnalisé de Lombok est nécessaire pour chacun. Lombok allows custom handlers for javac (used by Maven/Gradle and Netbeans) and the Eclipse compiler.

Dans les sections suivantes, nous allons implémenter notre gestionnaire d'annotations pour chaque compilateur.

4. Implémentation d'un gestionnaire pourjavac

4.1. Dépendance Maven

Commençons par extraire les dépendances requises pourLombok:


    org.projectlombok
    lombok
    1.18.4

De plus, nous aurions également besoin dutools.jar shipped avec Java pour accéder et modifier lejavac AST. Cependant, il n'y a pas de référentiel Maven pour cela. Le moyen le plus simple de l'inclure dans un projet Maven est de l'ajouter àProfile:


    
        default-tools.jar
            
                
                    java.vendor
                    Oracle Corporation
                
            
            
                
                    com.sun
                    tools
                    ${java.version}
                    system
                    ${java.home}/../lib/tools.jar
                
            
    

4.2. Extension deJavacAnnotationHandler

Pour implémenter un gestionnairejavac personnalisé, nous devons étendre lesJavacAnnotationHandler: de Lombok

public class SingletonJavacHandler extends JavacAnnotationHandler {
    public void handle(
      AnnotationValues annotation,
      JCTree.JCAnnotation ast,
      JavacNode annotationNode) {}
}

Ensuite, nous allons mettre en œuvre la méthodehandle() . Ici, l'annotation AST est rendue disponible en tant que paramètre par Lombok.

4.3. Modification de l'AST

C'est là que les choses se compliquent. En règle générale, la modification d'un AST existant n'est pas aussi simple.

Heureusement,Lombok provides many utility functions in JavacHandlerUtil and JavacTreeMaker for generating code and injecting it in the AST. Dans cet esprit, utilisons ces fonctions et créons le code pour nosSingletonRegistry:

public void handle(
  AnnotationValues annotation,
  JCTree.JCAnnotation ast,
  JavacNode annotationNode) {
    Context context = annotationNode.getContext();
    Javac8BasedLombokOptions options = Javac8BasedLombokOptions
      .replaceWithDelombokOptions(context);
    options.deleteLombokAnnotations();
    JavacHandlerUtil
      .deleteAnnotationIfNeccessary(annotationNode, Singleton.class);
    JavacHandlerUtil
      .deleteImportFromCompilationUnit(annotationNode, "lombok.AccessLevel");
    JavacNode singletonClass = annotationNode.up();
    JavacTreeMaker singletonClassTreeMaker = singletonClass.getTreeMaker();
    addPrivateConstructor(singletonClass, singletonClassTreeMaker);

    JavacNode holderInnerClass = addInnerClass(singletonClass, singletonClassTreeMaker);
    addInstanceVar(singletonClass, singletonClassTreeMaker, holderInnerClass);
    addFactoryMethod(singletonClass, singletonClassTreeMaker, holderInnerClass);
}

Il est important de souligner quethedeleteAnnotationIfNeccessary() and the deleteImportFromCompilationUnit() methods provided by Lombok are used for removing annotations and any imports for them.

Voyons maintenant comment d’autres méthodes privées sont mises en œuvre pour générer le code. Tout d'abord, nous allons générer le constructeur privé:

private void addPrivateConstructor(
  JavacNode singletonClass,
  JavacTreeMaker singletonTM) {
    JCTree.JCModifiers modifiers = singletonTM.Modifiers(Flags.PRIVATE);
    JCTree.JCBlock block = singletonTM.Block(0L, nil());
    JCTree.JCMethodDecl constructor = singletonTM
      .MethodDef(
        modifiers,
        singletonClass.toName(""),
        null, nil(), nil(), nil(), block, null);

    JavacHandlerUtil.injectMethod(singletonClass, constructor);
}

Ensuite, la classe interneSingletonHolder :

private JavacNode addInnerClass(
  JavacNode singletonClass,
  JavacTreeMaker singletonTM) {
    JCTree.JCModifiers modifiers = singletonTM
      .Modifiers(Flags.PRIVATE | Flags.STATIC);
    String innerClassName = singletonClass.getName() + "Holder";
    JCTree.JCClassDecl innerClassDecl = singletonTM
      .ClassDef(modifiers, singletonClass.toName(innerClassName),
      nil(), null, nil(), nil());
    return JavacHandlerUtil.injectType(singletonClass, innerClassDecl);
}

Maintenant, nous allons ajouter une variable d'instance dans la classe de support:

private void addInstanceVar(
  JavacNode singletonClass,
  JavacTreeMaker singletonClassTM,
  JavacNode holderClass) {
    JCTree.JCModifiers fieldMod = singletonClassTM
      .Modifiers(Flags.PRIVATE | Flags.STATIC | Flags.FINAL);

    JCTree.JCClassDecl singletonClassDecl
      = (JCTree.JCClassDecl) singletonClass.get();
    JCTree.JCIdent singletonClassType
      = singletonClassTM.Ident(singletonClassDecl.name);

    JCTree.JCNewClass newKeyword = singletonClassTM
      .NewClass(null, nil(), singletonClassType, nil(), null);

    JCTree.JCVariableDecl instanceVar = singletonClassTM
      .VarDef(
        fieldMod,
        singletonClass.toName("INSTANCE"),
        singletonClassType,
        newKeyword);
    JavacHandlerUtil.injectField(holderClass, instanceVar);
}

Enfin, ajoutons une méthode de fabrique pour accéder à l'objet singleton:

private void addFactoryMethod(
  JavacNode singletonClass,
  JavacTreeMaker singletonClassTreeMaker,
  JavacNode holderInnerClass) {
    JCTree.JCModifiers modifiers = singletonClassTreeMaker
      .Modifiers(Flags.PUBLIC | Flags.STATIC);

    JCTree.JCClassDecl singletonClassDecl
      = (JCTree.JCClassDecl) singletonClass.get();
    JCTree.JCIdent singletonClassType
      = singletonClassTreeMaker.Ident(singletonClassDecl.name);

    JCTree.JCBlock block
      = addReturnBlock(singletonClassTreeMaker, holderInnerClass);

    JCTree.JCMethodDecl factoryMethod = singletonClassTreeMaker
      .MethodDef(
        modifiers,
        singletonClass.toName("getInstance"),
        singletonClassType, nil(), nil(), nil(), block, null);
    JavacHandlerUtil.injectMethod(singletonClass, factoryMethod);
}

Clairement, la méthode factory renvoie la variable d'instance de la classe holder. Mettons cela en œuvre également:

private JCTree.JCBlock addReturnBlock(
  JavacTreeMaker singletonClassTreeMaker,
  JavacNode holderInnerClass) {

    JCTree.JCClassDecl holderInnerClassDecl
      = (JCTree.JCClassDecl) holderInnerClass.get();
    JavacTreeMaker holderInnerClassTreeMaker
      = holderInnerClass.getTreeMaker();
    JCTree.JCIdent holderInnerClassType
      = holderInnerClassTreeMaker.Ident(holderInnerClassDecl.name);

    JCTree.JCFieldAccess instanceVarAccess = holderInnerClassTreeMaker
      .Select(holderInnerClassType, holderInnerClass.toName("INSTANCE"));
    JCTree.JCReturn returnValue = singletonClassTreeMaker
      .Return(instanceVarAccess);

    ListBuffer statements = new ListBuffer<>();
    statements.append(returnValue);

    return singletonClassTreeMaker.Block(0L, statements.toList());
}

En conséquence, nous avons l'AST modifié pour notre classe Singleton.

4.4. Enregistrement du gestionnaire avec SPI

Jusqu'à présent, nous n'avons implémenté qu'un gestionnaire Lombok pour générer un AST pour notreSingletonRegistry.  Ici, il est important de répéter que Lombok fonctionne comme un processeur d'annotations.

Habituellement, les processeurs d'annotations sont découverts viaMETA-INF/services. Lombok maintient également une liste de gestionnaires de la même manière. De plus,it uses a framework named SPI for automatically updating the handler list.

Pour nos besoins, nous utiliserons lesmetainf-services:


    org.kohsuke.metainf-services
    metainf-services
    1.8

Maintenant, nous pouvons enregistrer notre gestionnaire avec Lombok:

@MetaInfServices(JavacAnnotationHandler.class)
public class SingletonJavacHandler extends JavacAnnotationHandler {}

This will generate a lombok.javac.JavacAnnotationHandler file at compile time.  Ce comportement est commun à tous les frameworks SPI.

5. Implémentation d'un gestionnaire pour Eclipse IDE

5.1. Dépendance Maven

Semblable àtools.jar we ajouté pour accéder à l'AST pourjavac, nous ajouteronseclipse jdt pour Eclipse IDE:


    org.eclipse.jdt
    core
    3.3.0-v_771

5.2. Extension deEclipseAnnotationHandler

Nous allons maintenant étendreEclipseAnnotationHandler  pour notre gestionnaire Eclipse:

@MetaInfServices(EclipseAnnotationHandler.class)
public class SingletonEclipseHandler
  extends EclipseAnnotationHandler {
    public void handle(
      AnnotationValues annotation,
      Annotation ast,
      EclipseNode annotationNode) {}
}

Avec l'annotation SPI,MetaInfServices, ce gestionnaire agit comme un processeur pour notre annotationSingleton . Par conséquent,whenever a class is compiled in Eclipse IDE, the handler converts the annotated class into a singleton implementation.

5.3. Modification de l'AST

Avec notre gestionnaire enregistré auprès de SPI, nous pouvons maintenant commencer à éditer le compilateur AST pour Eclipse:

public void handle(
  AnnotationValues annotation,
  Annotation ast,
  EclipseNode annotationNode) {
    EclipseHandlerUtil
      .unboxAndRemoveAnnotationParameter(
        ast,
        "onType",
        "@Singleton(onType=", annotationNode);
    EclipseNode singletonClass = annotationNode.up();
    TypeDeclaration singletonClassType
      = (TypeDeclaration) singletonClass.get();

    ConstructorDeclaration constructor
      = addConstructor(singletonClass, singletonClassType);

    TypeReference singletonTypeRef
      = EclipseHandlerUtil.cloneSelfType(singletonClass, singletonClassType);

    StringBuilder sb = new StringBuilder();
    sb.append(singletonClass.getName());
    sb.append("Holder");
    String innerClassName = sb.toString();
    TypeDeclaration innerClass
      = new TypeDeclaration(singletonClassType.compilationResult);
    innerClass.modifiers = AccPrivate | AccStatic;
    innerClass.name = innerClassName.toCharArray();

    FieldDeclaration instanceVar = addInstanceVar(
      constructor,
      singletonTypeRef,
      innerClass);

    FieldDeclaration[] declarations = new FieldDeclaration[]{instanceVar};
    innerClass.fields = declarations;

    EclipseHandlerUtil.injectType(singletonClass, innerClass);

    addFactoryMethod(
      singletonClass,
      singletonClassType,
      singletonTypeRef,
      innerClass,
      instanceVar);
}

Ensuite, le constructeur privé:

private ConstructorDeclaration addConstructor(
  EclipseNode singletonClass,
  TypeDeclaration astNode) {
    ConstructorDeclaration constructor
      = new ConstructorDeclaration(astNode.compilationResult);
    constructor.modifiers = AccPrivate;
    constructor.selector = astNode.name;

    EclipseHandlerUtil.injectMethod(singletonClass, constructor);
    return constructor;
}

Et pour la variable d'instance:

private FieldDeclaration addInstanceVar(
  ConstructorDeclaration constructor,
  TypeReference typeReference,
  TypeDeclaration innerClass) {
    FieldDeclaration field = new FieldDeclaration();
    field.modifiers = AccPrivate | AccStatic | AccFinal;
    field.name = "INSTANCE".toCharArray();
    field.type = typeReference;

    AllocationExpression exp = new AllocationExpression();
    exp.type = typeReference;
    exp.binding = constructor.binding;

    field.initialization = exp;
    return field;
}

Enfin, la méthode d'usine:

private void addFactoryMethod(
  EclipseNode singletonClass,
  TypeDeclaration astNode,
  TypeReference typeReference,
  TypeDeclaration innerClass,
  FieldDeclaration field) {

    MethodDeclaration factoryMethod
      = new MethodDeclaration(astNode.compilationResult);
    factoryMethod.modifiers
      = AccStatic | ClassFileConstants.AccPublic;
    factoryMethod.returnType = typeReference;
    factoryMethod.sourceStart = astNode.sourceStart;
    factoryMethod.sourceEnd = astNode.sourceEnd;
    factoryMethod.selector = "getInstance".toCharArray();
    factoryMethod.bits = ECLIPSE_DO_NOT_TOUCH_FLAG;

    long pS = factoryMethod.sourceStart;
    long pE = factoryMethod.sourceEnd;
    long p = (long) pS << 32 | pE;

    FieldReference ref = new FieldReference(field.name, p);
    ref.receiver = new SingleNameReference(innerClass.name, p);

    ReturnStatement statement
      = new ReturnStatement(ref, astNode.sourceStart, astNode.sourceEnd);

    factoryMethod.statements = new Statement[]{statement};

    EclipseHandlerUtil.injectMethod(singletonClass, factoryMethod);
}

De plus, nous devons connecter ce gestionnaire au chemin de classe de démarrage Eclipse. Généralement, cela se fait en ajoutant le paramètre suivant auxeclipse.ini:

-Xbootclasspath/a:singleton-1.0-SNAPSHOT.jar

6. Annotation personnalisée dans IntelliJ

De manière générale, un nouveau gestionnaire Lombok est nécessaire pour chaque compilateur, comme les gestionnairesjavac et Eclipse que nous avons implémentés auparavant.

Inversement, IntelliJ ne prend pas en charge le gestionnaire Lombok. It provides Lombok support through a plugin instead.

Pour cette raison,any new annotation must be supported by the plugin explicitly. This also applies to any annotation added to Lombok.

7. Conclusion

Dans cet article, nous avons implémenté une annotation personnalisée à l'aide des gestionnaires Lombok. Nous avons également brièvement examiné la modification AST pour notre annotationSingleton dans différents compilateurs, disponibles dans divers IDE.

Le code source complet est disponibleover on Github.