1. Overview
1.概述
In this tutorial, we’ll implement a custom annotation using Lombok to remove the boiler-plate around implementing Singletons in an application.
在本教程中,我们将使用Lombok实现一个自定义注解,以消除在应用程序中实现Singletons的模板。
Lombok is a powerful Java library that aims to reduce the boiler-plate code in Java. If you’re not familiar with it, here you can find the introduction to all the features of Lombok.
Lombok是一个强大的Java库,旨在减少Java中的模板代码。如果你对它不熟悉,在这里你可以找到Lombok的所有功能介绍。
An important note: Lombok 1.14.8 is the latest compatible version we can use to follow this tutorial. Since version 1.16.0, Lombok has hidden its internal API, and it’s no longer possible to create custom annotations in the way we present here.
重要提示:Lombok 1.14.8是我们可以用来遵循本教程的最新兼容版本。从1.16.0版本开始,Lombok已经隐藏了它的内部API,不再可能以我们这里介绍的方式创建自定义注释。
2. Lombok as an Annotation Processor
2.Lombok作为一个注释处理器
Java allows application developers to process annotations during the compilation phase; most importantly, to generate new files based on an annotation. As a result, libraries like Hibernate allow developers to reduce the boiler-plate code and use annotations instead.
Java允许应用程序开发人员在编译阶段处理注释;最重要的是,根据注释生成新的文件。因此,像Hibernate这样的库允许开发者减少模板代码,而使用注解。
Annotation processing is covered in depth in this tutorial.
本教程中深入介绍了注释处理。
In the same way, Project Lombok also works as an Annotation Processor. It processes the annotation by delegating it to a specific handler.
以同样的方式,Project Lombok也作为一个注释处理器工作。它通过将注解委托给一个特定的处理程序来处理注解。。
When delegating, it sends the compiler’s Abstract Syntax Tree (AST) of the annotated code to the handler. Therefore, it allows the handlers to modify the code by extending the AST.
当委托时,它将编译器的注释代码的抽象语法树(AST)发送给处理程序。因此,它允许处理程序通过扩展AST来修改代码。
3. Implementing a Custom Annotation
3.实现自定义注释
3.1. Extending Lombok
3.1.延伸Lombok
Surprisingly, Lombok is not easy to extend and add a custom annotation.
令人惊讶的是,Lombok并不容易扩展和添加自定义注释。
In fact, 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.
事实上,较新版本的Lombok使用Shadow ClassLoader(SCL)将Lombok中的.class文件隐藏为.scl文件。因此,它迫使开发者分叉Lombok源代码并在那里实现注释。
On the positive side, it simplifies the process of extending custom handlers and AST modification using utility functions.
从积极的一面来看,它简化了使用实用函数扩展自定义处理程序和AST修改的过程。
3.2. Singleton Annotation
3.2.单子注解
Generally, a lot of code is required for implementing a Singleton class. For applications that don’t use a dependency injection framework, this is just boilerplate stuff.
一般来说,实现一个Singleton类需要大量的代码。对于不使用依赖注入框架的应用程序,这只是模板性的东西。
For instance, here’s one way of implementing a Singleton class:
例如,这里有一种实现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
}
In contrast, this is how it would look like if we implement an annotation version of it:
相比之下,如果我们实现它的注释版本,就会是这个样子。
@Singleton
public class SingletonRegistry {}
And, the Singleton annotation :
而且,Singleton注解。
@Target(ElementType.TYPE)
public @interface Singleton {}
It is important to emphasize here that a Lombok Singleton handler would generate the implementation code we saw above by modifying the AST.
这里需要强调的是Lombok Singleton处理程序将通过修改AST来生成我们上面看到的执行代码。
Since the AST is different for every compiler, a custom Lombok handler is needed for each. Lombok allows custom handlers for javac (used by Maven/Gradle and Netbeans) and the Eclipse compiler.
由于每个编译器的AST都是不同的,所以每个编译器都需要一个自定义的Lombok处理程序。Lombok允许为javac(由Maven/Gradle和Netbeans使用)和Eclipse编译器定制处理程序。
In the following sections, we’ll implement our Annotation handler for each compiler.
在下面的章节中,我们将为每个编译器实现我们的注解处理程序。
4. Implementing a Handler for javac
4.为javac实现一个处理程序
4.1. Maven Dependency
4.1.Maven的依赖性
Let’s pull the required dependencies for Lombok first:
让我们先为Lombok拉出所需的依赖项。
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.14.8</version>
</dependency>
Additionally, we would also need the tools.jar shipped with Java for accessing and modifying the javac AST. However, there is no Maven repository for it. The easiest way to include this in a Maven project is by adding it to Profile:
此外,我们还需要随Java一起提供的tools.jar,用于访问和修改javac AST。然而,Maven库中没有这个库。将其纳入Maven项目的最简单方法是将其添加到Profile:中。
<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. Extending JavacAnnotationHandler
4.2.扩展JavacAnnotationHandler
In order to implement a custom javac handler, we need to extend Lombok’s JavacAnnotationHandler:
为了实现一个自定义的javac处理程序,我们需要扩展Lombok的JavacAnnotationHandler:。
public class SingletonJavacHandler extends JavacAnnotationHandler<Singleton> {
public void handle(
AnnotationValues<Singleton> annotation,
JCTree.JCAnnotation ast,
JavacNode annotationNode) {}
}
Next, we’ll implement the handle() method. Here, the annotation AST is made available as a parameter by Lombok.
接下来,我们将实现handle() 方法。在这里,注释AST被Lombok作为一个参数提供。
4.3. Modifying the AST
4.3.修改AST
This is where things get tricky. Generally, changing an existing AST is not as straightforward.
这就是事情变得棘手的地方。一般来说,改变一个现有的AST并不那么简单。
Luckily, Lombok provides many utility functions in JavacHandlerUtil and JavacTreeMaker for generating code and injecting it in the AST. With this in mind, let’s use these functions and create the code for our SingletonRegistry:
幸运的是,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);
}
It’s important to point out that the deleteAnnotationIfNeccessary() and the deleteImportFromCompilationUnit() methods provided by Lombok are used for removing annotations and any imports for them.
需要指出的是,Lombok提供的thedeleteAnnotationIfNeccessary()和deleteImportFromCompilationUnit()方法是用来移除注释和它们的任何导入。
Now, let’s see how other private methods are implemented for generating the code. First, we’ll generate the private constructor:
现在,让我们看看其他私有方法是如何实现生成代码的。首先,我们将生成私有构造函数。
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);
}
Next, the inner SingletonHolder class:
接下来是内部的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);
}
Now, we’ll add an instance variable in the holder class:
现在,我们将在持有人类中添加一个实例变量。
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);
}
Finally, let’s add a factory method for accessing the singleton object:
最后,让我们添加一个工厂方法来访问单子对象。
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);
}
Clearly, the factory method returns the instance variable from the holder class. Let’s implement that as well:
很明显,工厂方法从持有者类中返回实例变量。让我们也来实现这一点。
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());
}
As a result, we have the modified AST for our Singleton class.
因此,我们有了为我们的Singleton类修改的AST。
4.4. Registering Handler with SPI
4.4.将处理程序注册到SPI
Until now, we only implemented a Lombok handler for generating an AST for our SingletonRegistry. Here, it’s important to repeat that Lombok works as an annotation processor.
到目前为止,我们只实现了一个Lombok处理程序,为我们的SingletonRegistry生成一个AST。在这里,重要的是重申Lombok是作为一个注释处理器工作的。
Usually, Annotation Processors are discovered via META-INF/services. Lombok also maintains a list of handlers in the same way. Additionally, it uses a framework named SPI for automatically updating the handler list.
通常,注解处理器是通过META-INF/services发现的。Lombok也以同样的方式维护一个处理程序的列表。此外,它使用一个名为SPI的框架来自动更新处理器列表。
For our purpose, we’ll use the metainf-services:
为了我们的目的,我们将使用metainf-services。
<dependency>
<groupId>org.kohsuke.metainf-services</groupId>
<artifactId>metainf-services</artifactId>
<version>1.8</version>
</dependency>
Now, we can register our handler with Lombok:
现在,我们可以向Lombok注册我们的处理程序。
@MetaInfServices(JavacAnnotationHandler.class)
public class SingletonJavacHandler extends JavacAnnotationHandler<Singleton> {}
This will generate a lombok.javac.JavacAnnotationHandler file at compile time. This behavior is common for all SPI frameworks.
这将在编译时生成一个lombok.javac.JavacAnnotationHandler文件。这种行为对所有SPI框架来说都是通用的。
5. Implementing a Handler for Eclipse IDE
5.为Eclipse IDE实现一个处理程序
5.1. Maven Dependency
5.1.Maven的依赖性
Similar to tools.jar we added for accessing the AST for javac, we’ll add eclipse jdt for Eclipse IDE:
与我们为访问javac的AST而添加的tools.jar相似,我们将为Eclipse IDE添加eclipse jdt。
<dependency>
<groupId>org.eclipse.jdt</groupId>
<artifactId>core</artifactId>
<version>3.3.0-v_771</version>
</dependency>
5.2. Extending EclipseAnnotationHandler
5.2.扩展EclipseAnnotationHandler
We’ll now extend EclipseAnnotationHandler for our Eclipse handler:
我们现在将扩展EclipseAnnotationHandler ,用于我们的Eclipse处理器。
@MetaInfServices(EclipseAnnotationHandler.class)
public class SingletonEclipseHandler
extends EclipseAnnotationHandler<Singleton> {
public void handle(
AnnotationValues<Singleton> annotation,
Annotation ast,
EclipseNode annotationNode) {}
}
Together with the SPI annotation, MetaInfServices, this handler acts as a processor for our Singleton annotation. Hence, whenever a class is compiled in Eclipse IDE, the handler converts the annotated class into a singleton implementation.
与SPI注解MetaInfServices一起,这个处理程序充当了我们的Singleton注解的处理器。因此,无论何时在 Eclipse IDE 中编译一个类,该处理程序都会将注释的类转换为单子实现。
5.3. Modifying AST
5.3.修改AST
With our handler registered with SPI, we can now start editing the AST for Eclipse compiler:
随着我们的处理程序在SPI注册,我们现在可以开始为Eclipse编译器编辑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);
}
Next, the private constructor:
接下来是私人构造函数。
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;
}
And for the instance variable:
而对于实例变量。
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;
}
Lastly, the factory method:
最后是工厂法。
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);
}
Additionally, we must plug this handler into the Eclipse boot classpath. Generally, it is done by adding the following parameter to the eclipse.ini:
此外,我们必须将这个处理程序插入Eclipse启动classpath中。一般来说,可以通过在eclipse.ini中添加以下参数来完成:eclipse.ini:。
-Xbootclasspath/a:singleton-1.0-SNAPSHOT.jar
6. Custom Annotation in IntelliJ
6.IntelliJ中的自定义注解
Generally speaking, a new Lombok handler is needed for every compiler, like the javac and Eclipse handlers that we implemented before.
一般来说,每个编译器都需要一个新的Lombok处理程序,就像我们之前实现的javac和Eclipse处理程序。
Conversely, IntelliJ doesn’t support Lombok handler. It provides Lombok support through a plugin instead.
相反,IntelliJ并不支持Lombok处理器。它通过插件来提供Lombok支持。。
Due to this, any new annotation must be supported by the plugin explicitly. This also applies to any annotation added to Lombok.
由于这个原因,任何新的注释都必须由插件明确支持。这也适用于添加到Lombok的任何注释。
7. Conclusion
7.结语
In this article, we implemented a custom annotation using Lombok handlers. We also briefly looked at AST modification for our Singleton annotation in different compilers, available in various IDEs.
在这篇文章中,我们使用Lombok处理程序实现了一个自定义注解。我们还简要介绍了在不同的编译器中对我们的Singleton 注解进行AST修改的情况,这些编译器在各种IDE中都有。
The full source code is available over on Github.
完整的源代码可在Github上获得,。