«1. Обзор

В этом уроке мы реализуем пользовательскую аннотацию, используя Lombok, чтобы удалить шаблон вокруг реализации синглтонов в приложении.

Lombok — это мощная библиотека Java, целью которой является сокращение шаблонного кода на Java. Если вы не знакомы с ним, здесь вы можете найти введение во все функции Ломбока.

Важное примечание: Lombok 1.14.8 — это последняя совместимая версия, которую мы можем использовать для выполнения этого руководства. Начиная с версии 1.16.0, Lombok скрыл свой внутренний API, и больше невозможно создавать собственные аннотации так, как мы представляем здесь.

2. Lombok как обработчик аннотаций

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

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

Точно так же Project Lombok также работает как обработчик аннотаций. Он обрабатывает аннотацию, делегируя ее определенному обработчику.

При делегировании он отправляет обработчику абстрактное синтаксическое дерево компилятора (AST) аннотированного кода. Следовательно, он позволяет обработчикам изменять код, расширяя AST.

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

3.1. Расширение Lombok

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

Фактически, более новые версии Lombok используют Shadow ClassLoader (SCL), чтобы скрыть файлы .class в Lombok как файлы .scl. Таким образом, это заставляет разработчиков разветвлять исходный код Lombok и внедрять туда аннотации.

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

3.2. Аннотация Singleton

Обычно для реализации класса 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 {}

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

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

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

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

4.1. Зависимость Maven

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

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.14.8</version>
</dependency>

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

<profiles>
    <profile>
        <id>default-tools.jar</id>
            <activation>
                <property>
                    <name>java.vendor</name>
                    <value>Oracle Corporation</value>
                </property>
            </activation>
            <dependencies>
                <dependency>
                    <groupId>com.sun</groupId>
                    <artifactId>tools</artifactId>
                    <version>${java.version}</version>
                    <scope>system</scope>
                    <systemPath>${java.home}/../lib/tools.jar</systemPath>
                </dependency>
            </dependencies>
    </profile>
</profiles>

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

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

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

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

4.3. Модификация AST

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

К счастью, Lombok предоставляет множество служебных функций в JavacHandlerUtil и JavacTreeMaker для генерации кода и внедрения его в AST. Имея это в виду, давайте воспользуемся этими функциями и создадим код для нашего SingletonRegistry:

public void handle(
  AnnotationValues<Singleton> 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);
}

Важно отметить, что методы deleteAnnotationIfNeccessary() и deleteImportFromCompilationUnit(), предоставляемые Lombok, используются для удаления аннотаций и любого импорта. для них.

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

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("<init>"),
        null, nil(), nil(), nil(), block, null);

    JavacHandlerUtil.injectMethod(singletonClass, constructor);
}

«

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

«Далее, внутренний класс SingletonHolder:

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

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

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<JCTree.JCStatement> 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 также поддерживает список обработчиков таким же образом. Кроме того, он использует платформу SPI для автоматического обновления списка обработчиков.

<dependency>
    <groupId>org.kohsuke.metainf-services</groupId>
    <artifactId>metainf-services</artifactId>
    <version>1.8</version>
</dependency>

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

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

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

Это сгенерирует файл lombok.javac.JavacAnnotationHandler при компиляции. время. Такое поведение характерно для всех фреймворков SPI.

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

5.1. Зависимость Maven

<dependency>
    <groupId>org.eclipse.jdt</groupId>
    <artifactId>core</artifactId>
    <version>3.3.0-v_771</version>
</dependency>

Подобно tools.jar, который мы добавили для доступа к AST для javac, мы добавим eclipse jdt для Eclipse IDE:

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

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

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

Вместе с аннотацией SPI, MetaInfServices, этот обработчик действует как процессор для нашей аннотации Singleton. Следовательно, всякий раз, когда класс компилируется в Eclipse IDE, обработчик преобразует аннотированный класс в одноэлементную реализацию.

5.3. Модификация AST

public void handle(
  AnnotationValues<Singleton> 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);
}

Теперь, когда наш обработчик зарегистрирован в SPI, мы можем приступить к редактированию AST для компилятора Eclipse:

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

Наконец, фабричный метод:

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

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

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

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

И наоборот, IntelliJ не поддерживает обработчик Lombok. Вместо этого он обеспечивает поддержку Lombok через плагин.

В связи с этим любая новая аннотация должна явно поддерживаться плагином. Это также относится к любой аннотации, добавленной на Ломбок.

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

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