カスタム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の拡張
驚くべきことに、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クラスを実装するには多くのコードが必要です。 依存性注入フレームワークを使用しないアプリケーションの場合、これは単なる定型的なものです。
たとえば、シングルトンクラスを実装する1つの方法は次のとおりです。
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. メーベン依存
まず、Lombokに必要な依存関係を取得しましょう。
org.projectlombok
lombok
1.18.4
さらに、javac ASTにアクセスして変更するには、Javaに同梱されているtools.jar も必要になります。 ただし、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ハンドラーを実装するには、LombokのJavacAnnotationHandler:を拡張する必要があります
public class SingletonJavacHandler extends JavacAnnotationHandler {
public void handle(
AnnotationValues annotation,
JCTree.JCAnnotation ast,
JavacNode annotationNode) {}
}
次に、handle() メソッドを実装します。 ここで、アノテーションASTはLombokによってパラメーターとして使用可能になります。
4.3. ASTの変更
これが、事が巧妙になるところです。 一般に、既存の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 class:
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());
}
その結果、SingletonクラスのASTが変更されました。
4.4. SPIを使用したハンドラーの登録
これまでは、SingletonRegistry. のASTを生成するためのLombokハンドラーのみを実装していました。ここでは、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. メーベン依存
javacのASTにアクセスするために追加したtools.jar weと同様に、Eclipse IDEのeclipse jdtを追加します。
org.eclipse.jdt
core
3.3.0-v_771
5.2. EclipseAnnotationHandlerの拡張
EclipseハンドラーのEclipseAnnotationHandler を拡張します。
@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. ASTの変更
ハンドラーをSPIに登録したら、EclipseコンパイラーのASTの編集を開始できます。
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のカスタムアノテーション
一般的に、以前に実装したjavacやEclipseハンドラーなど、すべてのコンパイラーに新しいLombokハンドラーが必要です。
逆に、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ハンドラーを使用してカスタムアノテーションを実装しました。 また、さまざまなIDEで利用可能なさまざまなコンパイラでのSingleton サノテーションのAST変更についても簡単に説明しました。
完全なソースコードはover on Githubで入手できます。