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)を使用して、Lombok内の.classファイルを.sclファイルとして非表示にします。 したがって、開発者はLombokソースコードをフォークし、そこに注釈を実装する必要があります。

良い面として、は、ユーティリティ関数を使用してカスタムハンドラーとAST変更を拡張するプロセスを簡素化します。

3.2. シングルトンアノテーション

一般に、シングルトンクラスを実装するには多くのコードが必要です。 依存性注入フレームワークを使用しないアプリケーションの場合、これは単なる定型的なものです。

たとえば、シングルトンクラスを実装する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 {}

そして、シングルトンアノテーション:

@Target(ElementType.TYPE)
public @interface Singleton {}

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

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

次のセクションでは、コンパイラごとにアノテーションハンドラを実装します。

4. javacのハンドラーの実装

4.1. Mavenの依存関係

最初にLombokに必要な依存関係を取得しましょう。

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

さらに、 javac ASTにアクセスして変更するには、Javaに同梱されているtools.jarも必要です。 ただし、そのための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の変更

これは物事がトリッキーになるところです。 一般に、既存の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);
}

Lombokが提供するthe 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 クラス:

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へのハンドラーの登録

これまでは、ASTを生成するためのLombokハンドラーのみを実装していました。 SingletonRegistry。 ここで、Lombokが注釈プロセッサとして機能することを繰り返すことが重要です。

通常、注釈プロセッサは META-INF /servicesを介して検出されます。 Lombokも同じ方法でハンドラーのリストを維持します。 さらに、はSPIという名前のフレームワークを使用してハンドラーリストを自動的に更新します。

ここでは、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. EclipseIDEのハンドラーの実装

5.1. Mavenの依存関係

tools.jar javac のASTにアクセスするために追加したのと同様に、EclipseIDEのeclipsejdtを追加します。

<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とともに、このハンドラーはシングルトンアノテーションのプロセッサーとして機能します。 したがって、クラスがEclipse IDEでコンパイルされるたびに、ハンドラーは注釈付きクラスをシングルトン実装に変換します。

5.3. ASTの変更

ハンドラーを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);
}

次に、プライベートコンストラクター:

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ハンドラーをサポートしていません。 代わりにプラグインを介してLombokサポートを提供します。

これによると、 新しいアノテーションは、プラグインで明示的にサポートされている必要があります。 これは、Lombokに追加されたすべての注釈にも適用されます。

7. 結論

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

完全なソースコードは、Githubから入手できます。