Реализация пользовательской аннотации Lombok

Реализация пользовательской аннотации Lombok

1. обзор

В этом руководствеwe’ll implement a custom annotation using Lombok to remove the boiler-plate around implementing Singletons in an application.

Lombok - это мощная библиотека Java, целью которой является сокращение стандартного кода в Java. Если вы не знакомы с ним, здесь вы можете найтиthe introduction to all the features of Lombok.

2. Ломбок как процессор аннотаций

Java позволяет разработчикам приложений обрабатывать аннотации на этапе компиляции; самое главное, чтобы генерировать новые файлы на основе аннотации. В результате такие библиотеки, как Hibernate, позволяют разработчикам сокращать стандартный код и использовать вместо этого аннотации.

Обработка аннотаций подробно рассматривается в этомtutorial.

Таким же образомProject Lombok also works as an Annotation Processor. It processes the annotation by delegating it to a specific handler.

При делегированииit sends the compiler’s Abstract Syntax Tree (AST) of the annotated code to the handler. Таким образом, он позволяет обработчикам изменять код, расширяя AST.

3. Реализация пользовательской аннотации

3.1. Расширение Ломбок

Удивительно, но Lombok нелегко расширить и добавить собственную аннотацию.

Фактически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.

С положительной стороны,it simplifies the process of extending custom handlers and AST modification using utility functions.

3.2. Синглтон аннотации

Как правило, для реализации класса Singleton требуется много кода. Для приложений, которые не используют фреймворк внедрения зависимостей, это просто шаблонный материал.

Например, вот один из способов реализации класса 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
}

Напротив, вот как это будет выглядеть, если мы реализуем его аннотационную версию:

@Singleton
public class SingletonRegistry {}

И аннотацияSingleton:

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

Здесь важно подчеркнутьthat a Lombok Singleton handler would generate the implementation code we saw above by modifying the AST.

Поскольку AST различается для каждого компилятора, для каждого из них требуется собственный обработчик Lombok. Lombok allows custom handlers for javac (used by Maven/Gradle and Netbeans) and the Eclipse compiler.с

В следующих разделах мы реализуем наш обработчик аннотаций для каждого компилятора.

4. Реализация обработчика дляjavac

4.1. Maven Dependency

Давайте сначала извлечем необходимые зависимости дляLombok:


    org.projectlombok
    lombok
    1.18.4

Кроме того, нам также понадобитсяtools.jar , поставляемый с Java, для доступа и изменения ASTjavac. Однако для него нет репозитория Maven. Самый простой способ включить это в проект Maven - добавить его вProfile:


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

4.2. РасширениеJavacAnnotationHandler

Чтобы реализовать собственный обработчикjavac, нам нужно расширить LombokJavacAnnotationHandler:

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

Затем мы реализуем методhandle() . Здесь аннотация AST доступна как параметр Lombok.

4.3. Модификация АСТ

Здесь все становится сложнее. Как правило, изменение существующего AST не так просто.

К счастью,Lombok provides many utility functions in JavacHandlerUtil and JavacTreeMaker for generating code and injecting it in the AST. Имея это в виду, давайте воспользуемся этими функциями и создадим код для нашегоSingletonRegistry:

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

Важно отметить, чтоthedeleteAnnotationIfNeccessary() and the deleteImportFromCompilationUnit() methods provided by Lombok are used for removing annotations and any imports for them.

Теперь давайте посмотрим, как реализованы другие частные методы для генерации кода. Сначала мы сгенерируем частный конструктор:

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

Затем внутренний классSingletonHolder :

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

Теперь мы добавим переменную экземпляра в класс-держатель:

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

Наконец, давайте добавим фабричный метод для доступа к одноэлементному объекту:

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

Ясно, что фабричный метод возвращает переменную экземпляра из класса владельца. Давайте реализуем и это:

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

В результате у нас есть модифицированный AST для нашего класса Singleton.

4.4. Регистрация обработчика в SPI

До сих пор мы реализовали только обработчик Lombok для генерации AST для нашегоSingletonRegistry. . Здесь важно повторить, что Lombok работает как процессор аннотаций.

Обычно процессоры аннотаций обнаруживаются черезMETA-INF/services. Lombok также поддерживает список обработчиков таким же образом. Дополнительноit uses a framework named SPI for automatically updating the handler list.

Для нашей цели мы будем использоватьmetainf-services:


    org.kohsuke.metainf-services
    metainf-services
    1.8

Теперь мы можем зарегистрировать наш обработчик в Lombok:

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

This will generate a lombok.javac.JavacAnnotationHandler file at compile time. Это поведение является общим для всех платформ SPI.

5. Реализация обработчика для Eclipse IDE

5.1. Maven Dependency

Аналогичноtools.jar we, добавленному для доступа к AST дляjavac, мы добавимeclipse jdt для Eclipse IDE:


    org.eclipse.jdt
    core
    3.3.0-v_771

5.2. РасширениеEclipseAnnotationHandler

Теперь мы расширимEclipseAnnotationHandler  для нашего обработчика Eclipse:

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

Вместе с аннотацией SPI,MetaInfServices, этот обработчик действует как процессор для нашей саннотацииSingleton . Следовательно,whenever a class is compiled in Eclipse IDE, the handler converts the annotated class into a singleton implementation.

5.3. Модификация АСТ

Теперь, когда наш обработчик зарегистрирован в SPI, мы можем начать редактирование AST для компилятора 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);
}

Далее приватный конструктор:

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

И для переменной экземпляра:

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

Наконец, заводской метод:

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

Кроме того, мы должны подключить этот обработчик к пути загрузки Eclipse. Обычно это делается путем добавления следующего параметра кeclipse.ini:

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

6. Пользовательская аннотация в IntelliJ

Вообще говоря, новый обработчик Lombok необходим для каждого компилятора, как и обработчикиjavac и Eclipse, которые мы реализовали ранее.

И наоборот, IntelliJ не поддерживает обработчик Lombok. It provides Lombok support through a plugin instead.с

Благодаря этомуany new annotation must be supported by the plugin explicitly. This also applies to any annotation added to Lombok.

7. Заключение

В этой статье мы реализовали пользовательскую аннотацию с использованием обработчиков Lombok. Мы также вкратце рассмотрели модификацию AST для нашей саннотацииSingleton в разных компиляторах, доступных в различных IDE.

Доступен полный исходный кодover on Github.