カスタムLombokアノテーションを実装する

1.概要

このチュートリアルでは、** Lombokを使用してカスタムアノテーションを実装し、アプリケーションにシングルトンを実装する際の定型句を削除します。

Lombokは、Javaの定型コードを減らすことを目的とした強力なJavaライブラリです。慣れていない場合は、https://www.baeldung.com/intro-to-project-lombok[Lombokの全機能の紹介]を参照してください。

2.注釈プロセッサとしてのLombok

Javaを使用すると、アプリケーション開発者はコンパイル段階で注釈を処理できます。最も重要なことは、注釈に基づいて新しいファイルを生成することです。その結果、Hibernateのようなライブラリは開発者が定型コードを減らして代わりにアノテーションを使うことを可能にします。

注釈処理については、このhttps://www.baeldung.com/java-annotation-processing-builder[チュートリアル]で詳しく説明しています。

同様に、** Project Lombokもアノテーションプロセッサとして機能します。注釈を特定のハンドラに委任することによって注釈を処理します。

委任時には、** 注釈付きコードのコンパイラの抽象構文木(AST)をハンドラに送信します。したがって、ハンドラはASTを拡張してコードを変更できます。

3.カスタムアノテーションを実装する

3.1. Lombokを拡張する

驚くべきことに、Lombokはカスタムアノテーションを拡張して追加するのは簡単ではありません。

実際、** 新しいバージョンのLombokはLombokの .class ファイルを .scl ファイルとして隠すためにShadow ClassLoader(SCL)を使用しています。そのため、開発者はLombokのソースコードをフォークし、そこでアノテーションを実装する必要があります。

プラス面としては、カスタムハンドラの拡張やユーティリティ関数を使ったASTの変更のプロセスが簡単になります。

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 {}

ここで強調しておくことが重要です。LombokSingletonハンドラーは、ASTを変更することによって上記の実装コードを生成します。

ASTはコンパイラごとに異なるため、それぞれにカスタムLombokハンドラが必要です。 Lombokでは、 javac (Maven/GradleおよびNetbeansで使用)およびEclipseコンパイラ用のカスタムハンドラを使用できます。

次のセクションでは、各コンパイラに対してAnnotationハンドラを実装します。

4. javac のハンドラを実装する

4.1. メーベン依存

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

さらに、 javac ASTにアクセスしてそれを変更するためには、Javaに付属の __tools.jarも必要です。しかし、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. 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の修正

これが、事が巧妙になるところです。一般に、既存のASTを変更することはそれほど簡単ではありません。

幸いなことに、** Lombokは JavacHandlerUtil JavacTreeMaker にコードを生成してそれをASTに挿入するための多くのユーティリティ関数を提供しています。

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

Lombokが提供する __ deleteAnnotationIfNeccessary() および deleteImportFromCompilationUnit() __メソッドは、注釈の削除およびそれらのインポートに使用されることに注意してください。

それでは、コードを生成するために他のプライベートメソッドがどのように実装されているかを見てみましょう。まず、プライベートコンストラクタを生成します。

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 __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<JCTree.JCStatement> statements = new ListBuffer<>();
    statements.append(returnValue);

    return singletonClassTreeMaker.Block(0L, statements.toList());
}

その結果、シングルトンクラスのASTが変更されました。

4.4. SPIにハンドラを登録する

今までは、 __SingletonRegistryのASTを生成するためのLombokハンドラーしか実装していませんでした。 __ここで、Lombokがアノテーションプロセッサとして機能することを繰り返すことが重要です。

通常、注釈プロセッサは META-INF/services を介して発見されます。

Lombokも同様にハンドラのリストを管理しています。さらに、 ハンドラリストを自動的に更新するために SPIというフレームワークを使用します。

私たちの目的のために、__https://search.maven.org/search?q = g:org.kohsuke.metainf-servicesを使用します

<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. メーベン依存

javac のASTにアクセスするために追加された __tools.jarと同様に、 https://search.maven.org/search?q = g:org.eclipse.jdt%20AND%20a:coreを追加します。

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

5.2. EclipseAnnotationHandler を拡張する

Eclipseハンドラー用に____EclipseAnnotationHandlerを拡張します。

@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. ASTを変更する

ハンドラをSPIに登録したら、AST for 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のカスタムアノテーション

一般的に言えば、以前に実装した javac やEclipseハンドラーのように、すべてのコンパイラーに新しいLombokハンドラーが必要です。

逆に、IntelliJはLombokハンドラをサポートしていません。 代わりにhttps://github.com/mplushnikov/lombok-intellij-plugin[plugin]を介してLombokサポートを提供します。

このため、** 新しい注釈はプラグインによって明示的にサポートされている必要があります。これはLombokに追加された注釈にも適用されます。

7.まとめ

この記事では、Lombokハンドラーを使用してカスタム注釈を実装しました。また、さまざまなIDEで利用可能な、さまざまなコンパイラでの __Singleton __アノテーションのAST修正についても簡単に説明しました。

完全なソースコードはhttps://github.com/eugenp/tutorials/tree/master/lombok-custom[over on Github]から入手できます。