Implementando uma anotação personalizada do Lombok
1. Visão geral
Neste tutorial,we’ll implement a custom annotation using Lombok to remove the boiler-plate around implementing Singletons in an application.
Lombok é uma poderosa biblioteca Java que visa reduzir o código da placa de caldeira em Java. Se você não estiver familiarizado com ele, aqui pode encontrarthe introduction to all the features of Lombok.
2. Lombok como um processador de anotação
Java permite que os desenvolvedores de aplicativos processem anotações durante a fase de compilação; o mais importante é gerar novos arquivos com base em uma anotação. Como resultado, bibliotecas como o Hibernate permitem que os desenvolvedores reduzam o código da placa da caldeira e usem anotações.
O processamento de anotações é abordado em profundidade nestetutorial.
Da mesma forma,Project Lombok also works as an Annotation Processor. It processes the annotation by delegating it to a specific handler.
Ao delegar,it sends the compiler’s Abstract Syntax Tree (AST) of the annotated code to the handler. Portanto, permite que os manipuladores modifiquem o código estendendo o AST.
3. Implementando uma anotação personalizada
3.1. Estendendo Lombok
Surpreendentemente, o Lombok não é fácil de estender e adicionar uma anotação personalizada.
Na verdade,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.
Do lado positivo,it simplifies the process of extending custom handlers and AST modification using utility functions.
3.2. Anotação Singleton
Geralmente, é necessário muito código para implementar uma classe Singleton. Para aplicativos que não usam uma estrutura de injeção de dependência, isso é apenas material clichê.
Por exemplo, aqui está uma maneira de implementar uma classe 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
}
Por outro lado, é assim que seria se implementássemos uma versão de anotação:
@Singleton
public class SingletonRegistry {}
E, a anotaçãoSingleton:
@Target(ElementType.TYPE)
public @interface Singleton {}
É importante enfatizar aquithat a Lombok Singleton handler would generate the implementation code we saw above by modifying the AST.
Como o AST é diferente para cada compilador, é necessário um manipulador Lombok personalizado para cada um. Lombok allows custom handlers for javac (used by Maven/Gradle and Netbeans) and the Eclipse compiler.
Nas seções a seguir, implementaremos nosso gerenciador de anotações para cada compilador.
4. Implementando um manipulador parajavac
4.1. Dependência do Maven
Vamos extrair as dependências necessárias paraLombok primeiro:
org.projectlombok
lombok
1.18.4
Além disso, também precisaríamos dotools.jar enviado com Java para acessar e modificar o ASTjavac. No entanto, não há repositório Maven para ele. A maneira mais fácil de incluir isso em um projeto Maven é adicioná-lo aProfile:
default-tools.jar
java.vendor
Oracle Corporation
com.sun
tools
${java.version}
system
${java.home}/../lib/tools.jar
4.2. EstendendoJavacAnnotationHandler
Para implementar um manipuladorjavac personalizado, precisamos estender oJavacAnnotationHandler: do Lombok
public class SingletonJavacHandler extends JavacAnnotationHandler {
public void handle(
AnnotationValues annotation,
JCTree.JCAnnotation ast,
JavacNode annotationNode) {}
}
A seguir, implementaremos o métodohandle() . Aqui, a anotação AST é disponibilizada como parâmetro pelo Lombok.
4.3. Modificando o AST
É aqui que as coisas ficam complicadas. Geralmente, alterar um AST existente não é tão simples.
Felizmente,Lombok provides many utility functions in JavacHandlerUtil and JavacTreeMaker for generating code and injecting it in the AST. Com isso em mente, vamos usar essas funções e criar o código para nossoSingletonRegistry:
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);
}
É importante ressaltar quethedeleteAnnotationIfNeccessary() and the deleteImportFromCompilationUnit() methods provided by Lombok are used for removing annotations and any imports for them.
Agora, vamos ver como outros métodos privados são implementados para gerar o código. Primeiro, vamos gerar o construtor privado:
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);
}
Em seguida, aSingletonHolder class interna:
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);
}
Agora, vamos adicionar uma variável de instância na classe titular:
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);
}
Finalmente, vamos adicionar um método de fábrica para acessar o objeto singleton:
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);
}
Claramente, o método factory retorna a variável de instância da classe holder. Vamos implementar isso também:
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());
}
Como resultado, temos o AST modificado para nossa classe Singleton.
4.4. Registrando manipulador com SPI
Até agora, implementamos apenas um manipulador do Lombok para gerar um AST para nossoSingletonRegistry. . Aqui, é importante repetir que o Lombok funciona como um processador de anotação.
Normalmente, os processadores de anotação são descobertos por meio deMETA-INF/services. O Lombok também mantém uma lista de manipuladores da mesma maneira. Além disso,it uses a framework named SPI for automatically updating the handler list.
Para nosso propósito, usaremos ometainf-services:
org.kohsuke.metainf-services
metainf-services
1.8
Agora, podemos registrar nosso manipulador no Lombok:
@MetaInfServices(JavacAnnotationHandler.class)
public class SingletonJavacHandler extends JavacAnnotationHandler {}
This will generate a lombok.javac.JavacAnnotationHandler file at compile time. Este comportamento é comum para todas as estruturas SPI.
5. Implementando um manipulador para o Eclipse IDE
5.1. Dependência do Maven
Semelhante atools.jar we adicionado para acessar o AST parajavac, adicionaremoseclipse jdt para Eclipse IDE:
org.eclipse.jdt
core
3.3.0-v_771
5.2. EstendendoEclipseAnnotationHandler
Agora vamos estenderEclipseAnnotationHandler para nosso manipulador Eclipse:
@MetaInfServices(EclipseAnnotationHandler.class)
public class SingletonEclipseHandler
extends EclipseAnnotationHandler {
public void handle(
AnnotationValues annotation,
Annotation ast,
EclipseNode annotationNode) {}
}
Junto com a anotação SPI,MetaInfServices, esse manipulador atua como um processador para nossa anotaçãoSingleton . Portanto,whenever a class is compiled in Eclipse IDE, the handler converts the annotated class into a singleton implementation.
5.3. Modificando AST
Com nosso manipulador registrado no SPI, agora podemos começar a editar o compilador AST for 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);
}
Em seguida, o construtor privado:
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;
}
E para a variável de instância:
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;
}
Por fim, o método de fábrica:
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);
}
Além disso, devemos conectar esse manipulador ao caminho de classe de inicialização do Eclipse. Geralmente, isso é feito adicionando o seguinte parâmetro aoeclipse.ini:
-Xbootclasspath/a:singleton-1.0-SNAPSHOT.jar
6. Anotação personalizada no IntelliJ
De modo geral, um novo manipulador do Lombok é necessário para cada compilador, como os manipuladoresjavace Eclipse que implementamos antes.
Por outro lado, o IntelliJ não oferece suporte ao gerenciador do Lombok. It provides Lombok support through a plugin instead.
Devido a isso,any new annotation must be supported by the plugin explicitly. This also applies to any annotation added to Lombok.
7. Conclusão
Neste artigo, implementamos uma anotação personalizada usando manipuladores Lombok. Também vimos brevemente a modificação AST para nossaSingleton annotation em diferentes compiladores, disponíveis em vários IDEs.
O código-fonte completo está disponívelover on Github.