1. Обзор
В этом руководстве мы реализуем пользовательскую аннотацию с помощью Lombok, чтобы удалить шаблон вокруг реализации синглтонов в приложении.
Lombok — это мощная библиотека Java, целью которой является сокращение шаблонного кода на Java. Если вы не знакомы с ним, здесь вы можете найти введение во все функции Lombok .
Важное примечание: Lombok 1.14.8 — это последняя совместимая версия, которую мы можем использовать для выполнения этого руководства. Начиная с версии 1.16.0, Lombok скрыл свой внутренний API, и больше невозможно создавать собственные аннотации так, как мы представляем здесь.
2. Ломбок как процессор аннотаций
Java позволяет разработчикам приложений обрабатывать аннотации на этапе компиляции; самое главное, генерировать новые файлы на основе аннотации. В результате такие библиотеки, как Hibernate, позволяют разработчикам сократить стандартный код и вместо этого использовать аннотации.
В этом руководстве подробно рассматривается обработка аннотаций .
Точно так же Project Lombok также работает как обработчик аннотаций. Он обрабатывает аннотацию, делегируя ее определенному обработчику.
При делегировании он отправляет обработчику абстрактное синтаксическое дерево (AST) аннотированного кода компилятора. Следовательно, он позволяет обработчикам изменять код, расширяя AST.
3. Внедрение пользовательской аннотации
3.1. Расширение Ломбока
Удивительно, но Lombok непросто расширить и добавить пользовательскую аннотацию.
Фактически, более новые версии Lombok используют Shadow ClassLoader (SCL), чтобы скрыть файлы .class
в Lombok как файлы .scl
. Таким образом, это заставляет разработчиков разветвлять исходный код Lombok и внедрять туда аннотации.
С положительной стороны, это упрощает процесс расширения пользовательских обработчиков и модификации AST с помощью служебных функций.
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 {}
И аннотация Синглтона :
@Target(ElementType.TYPE)
public @interface Singleton {}
Здесь важно подчеркнуть, что обработчик Lombok Singleton сгенерирует код реализации, который мы видели выше, путем изменения AST.
Поскольку AST отличается для каждого компилятора, для каждого необходим собственный обработчик Lombok. Lombok позволяет создавать собственные обработчики для javac
(используемые Maven/Gradle и Netbeans) и компилятора Eclipse.
В следующих разделах мы реализуем наш обработчик Annotation для каждого компилятора.
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
, нам нужно расширить Lombok JavacAnnotationHandler:
public class SingletonJavacHandler extends JavacAnnotationHandler<Singleton> {
public void handle(
AnnotationValues<Singleton> annotation,
JCTree.JCAnnotation ast,
JavacNode annotationNode) {}
}
Далее мы реализуем метод handle()
. Здесь аннотация AST предоставляется Lombok в качестве параметра.
4.3. Изменение АСТ
Здесь все становится сложнее. Как правило, изменить существующий 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);
}
Далее внутренний класс 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<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>
Теперь мы можем зарегистрировать наш обработчик на Lombok:
@MetaInfServices(JavacAnnotationHandler.class)
public class SingletonJavacHandler extends JavacAnnotationHandler<Singleton> {}
Это создаст файл lombok.javac.JavacAnnotationHandler
во время компиляции. Такое поведение характерно для всех фреймворков SPI.
5. Реализация обработчика для Eclipse IDE
5.1. Зависимость от Maven
Подобно tools.jar
, который мы добавили для доступа к AST для javac
, мы добавим eclipse jdt
для Eclipse IDE:
<dependency>
<groupId>org.eclipse.jdt</groupId>
<artifactId>core</artifactId>
<version>3.3.0-v_771</version>
</dependency>
5.2. Расширение EclipseAnnotationHandler
Теперь мы расширим EclipseAnnotationHandler
для нашего обработчика Eclipse:
@MetaInfServices(EclipseAnnotationHandler.class)
public class SingletonEclipseHandler
extends EclipseAnnotationHandler<Singleton> {
public void handle(
AnnotationValues<Singleton> annotation,
Annotation ast,
EclipseNode annotationNode) {}
}
Вместе с аннотацией SPI, MetaInfServices
, этот обработчик действует как процессор для нашей аннотации Singleton .
Следовательно, всякий раз, когда класс компилируется в Eclipse IDE, обработчик преобразует аннотированный класс в одноэлементную реализацию.
5.3. Модификация АСТ
Теперь, когда наш обработчик зарегистрирован в SPI, мы можем приступить к редактированию компилятора AST для Eclipse:
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);
}
Далее частный конструктор:
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. Вместо этого он обеспечивает поддержку Lombok через плагин .
В связи с этим любая новая аннотация должна явно поддерживаться плагином. Это также относится к любой аннотации, добавленной на Ломбок.
7. Заключение
В этой статье мы реализовали пользовательскую аннотацию с помощью обработчиков Lombok. Мы также кратко рассмотрели модификацию AST для нашей аннотации Singleton
в разных компиляторах, доступных в различных IDE.
Полный исходный код доступен на Github .