Implementieren einer benutzerdefinierten Lombok-Anmerkung

Implementieren einer benutzerdefinierten Lombok-Anmerkung

1. Überblick

In diesem Tutorial werdenwe’ll implement a custom annotation using Lombok to remove the boiler-plate around implementing Singletons in an application.

Lombok ist eine leistungsstarke Java-Bibliothek, die darauf abzielt, den Kessel-Code in Java zu reduzieren. Wenn Sie damit nicht vertraut sind, finden Sie hierthe introduction to all the features of Lombok.

2. Lombok als Anmerkungsprozessor

Mit Java können Anwendungsentwickler Anmerkungen während der Kompilierungsphase verarbeiten. vor allem, um neue Dateien basierend auf einer Anmerkung zu generieren. Bibliotheken wie Hibernate ermöglichen es den Entwicklern daher, den Code der Kesselplatte zu reduzieren und stattdessen Anmerkungen zu verwenden.

Die Verarbeitung von Anmerkungen wird intutorial ausführlich behandelt.

In gleicher Weise istProject Lombok also works as an Annotation Processor. It processes the annotation by delegating it to a specific handler.

Beim Delegieren vonit sends the compiler’s Abstract Syntax Tree (AST) of the annotated code to the handler. können die Handler daher den Code durch Erweitern des AST ändern.

3. Implementieren einer benutzerdefinierten Anmerkung

3.1. Lombok erweitern

Überraschenderweise ist es nicht einfach, Lombok zu erweitern und eine benutzerdefinierte Anmerkung hinzuzufügen.

Tatsächlich istthe 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.

Positiv zu vermerken ist, dassit simplifies the process of extending custom handlers and AST modification using utility functions.

3.2. Singleton-Anmerkung

Im Allgemeinen ist viel Code zum Implementieren einer Singleton-Klasse erforderlich. Für Anwendungen, die kein Abhängigkeitsinjektionsframework verwenden, ist dies nur ein Boilerplate-Material.

Hier ist beispielsweise eine Möglichkeit, eine Singleton-Klasse zu implementieren:

public class SingletonRegistry {
    private SingletonRegistry() {}

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

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

    // other methods
}

Im Gegensatz dazu würde es so aussehen, wenn wir eine Annotation-Version davon implementieren:

@Singleton
public class SingletonRegistry {}

Und die Annotation vonSingleton:

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

Es ist wichtig, hierthat a Lombok Singleton handler would generate the implementation code we saw above by modifying the AST. hervorzuheben

Da der AST für jeden Compiler unterschiedlich ist, wird für jeden ein benutzerdefinierter Lombok-Handler benötigt. Lombok allows custom handlers for javac (used by Maven/Gradle and Netbeans) and the Eclipse compiler.

In den folgenden Abschnitten implementieren wir unseren Annotation-Handler für jeden Compiler.

4. Implementieren eines Handlers fürjavac

4.1. Maven-Abhängigkeit

Lassen Sie uns zuerst die erforderlichen Abhängigkeiten fürLombok ziehen:


    org.projectlombok
    lombok
    1.18.4

Zusätzlich benötigen wirtools.jar , das mit Java geliefert wird, um auf den AST vonjavaczuzugreifen und ihn zu ändern. Es gibt jedoch kein Maven-Repository dafür. Der einfachste Weg, dies in ein Maven-Projekt aufzunehmen, besteht darin, es zuProfile: hinzuzufügen


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

4.2. JavacAnnotationHandler verlängern

Um einen benutzerdefiniertenjavac-Handler zu implementieren, müssen wir LomboksJavacAnnotationHandler: erweitern

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

Als Nächstes implementieren wir diehandle() -Smethodik. Hier wird die Annotation AST von Lombok als Parameter zur Verfügung gestellt.

4.3. Ändern des AST

Hier wird es schwierig. Im Allgemeinen ist das Ändern eines vorhandenen AST nicht so einfach.

Zum GlückLombok provides many utility functions in JavacHandlerUtil and JavacTreeMaker for generating code and injecting it in the AST. Verwenden Sie in diesem Sinne diese Funktionen und erstellen Sie den Code für unsereSingletonRegistry:

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);
}

Es ist wichtig darauf hinzuweisen, dassthedeleteAnnotationIfNeccessary() and the deleteImportFromCompilationUnit() methods provided by Lombok are used for removing annotations and any imports for them.

Lassen Sie uns nun sehen, wie andere private Methoden zum Generieren des Codes implementiert werden. Zuerst generieren wir den privaten Konstruktor:

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);
}

Als nächstes die innereSingletonHolder -Klasse:

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);
}

Jetzt fügen wir der Inhaberklasse eine Instanzvariable hinzu:

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);
}

Fügen wir abschließend eine Factory-Methode für den Zugriff auf das Singleton-Objekt hinzu:

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);
}

Es ist klar, dass die Factory-Methode die Instanzvariable aus der Holder-Klasse zurückgibt. Lassen Sie uns das auch implementieren:

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());
}

Als Ergebnis haben wir den modifizierten AST für unsere Singleton-Klasse.

4.4. Registrieren des Handlers bei SPI

Bisher haben wir nur einen Lombok-Handler zum Generieren eines AST für unserSingletonRegistry.  implementiert. Hier ist es wichtig zu wiederholen, dass Lombok als Annotationsprozessor arbeitet.

Normalerweise werden Anmerkungsprozessoren überMETA-INF/services erkannt. Auf die gleiche Weise führt Lombok auch eine Liste von Handlern. Zusätzlichit uses a framework named SPI for automatically updating the handler list.

Für unseren Zweck verwenden wir diemetainf-services:


    org.kohsuke.metainf-services
    metainf-services
    1.8

Jetzt können wir unseren Handler bei Lombok registrieren:

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

This will generate a lombok.javac.JavacAnnotationHandler file at compile time. Dieses Verhalten ist für alle SPI-Frameworks gleich.

5. Implementieren eines Handlers für die Eclipse-IDE

5.1. Maven-Abhängigkeit

Ähnlich wietools.jar we für den Zugriff auf den AST fürjavac hinzugefügt wurde, fügen wireclipse jdt für die Eclipse-IDE hinzu:


    org.eclipse.jdt
    core
    3.3.0-v_771

5.2. EclipseAnnotationHandler verlängern

Wir werden jetztEclipseAnnotationHandler für unseren Eclipse-Handler erweitern:

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

Zusammen mit der SPI-AnnotationMetaInfServices fungiert dieser Handler als Prozessor für unsereSingleton -Sannotation. Daher istwhenever a class is compiled in Eclipse IDE, the handler converts the annotated class into a singleton implementation.

5.3. AST ändern

Mit unserem bei SPI registrierten Handler können wir nun den AST for Eclipse-Compiler bearbeiten:

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);
}

Als nächstes der private Konstruktor:

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;
}

Und für die Instanzvariable:

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;
}

Schließlich die Fabrikmethode:

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);
}

Darüber hinaus müssen wir diesen Handler in den Eclipse-Startklassenpfad einbinden. Im Allgemeinen erfolgt dies durch Hinzufügen des folgenden Parameters zueclipse.ini:

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

6. Benutzerdefinierte Anmerkung in IntelliJ

Im Allgemeinen wird für jeden Compiler ein neuer Lombok-Handler benötigt, wie die zuvor implementiertenjavac- und Eclipse-Handler.

Umgekehrt unterstützt IntelliJ den Lombok-Handler nicht. It provides Lombok support through a plugin instead.

Aufgrund dessen istany new annotation must be supported by the plugin explicitly. This also applies to any annotation added to Lombok.

7. Fazit

In diesem Artikel haben wir eine benutzerdefinierte Annotation mit Lombok-Handlern implementiert. Wir haben uns auch kurz die AST-Modifikation für unsereSingleton -Sannotation in verschiedenen Compilern angesehen, die in verschiedenen IDEs verfügbar sind.

Der vollständige Quellcode ist verfügbarover on Github.