ロンボク島実装原理分析


序文

LombokはJava開発をされている方にはよく知られていると思いますが、Lombokをベースにエンティティクラスに一般的なGetメソッドやSetメソッドなどの簡単なアノテーションを追加することで、元のコードを変更することなくソースコードに補足情報を埋め込むことができます。
それで、何人かの友人は、根底にある実装原則が何であるかを考えましたか?


1. ロンボク島のアノテーションの分析

ここでは、最もよく使用される @Data を分析の例として取り上げます。

package lombok;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({
    
    ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface Data {
    
    
    String staticConstructor() default "";
}

例証します:

  • メタアノテーション@Target({ElementType.TYPE})- アノテーション @Data がクラス、インターフェイス (アノテーション タイプを含む)、または列挙型を記述するために使用されることを示すために使用されます。
  • メタアノテーション@Retention(RetentionPolicy.SOURCE)- アノテーション @Data がソース ファイル内で有効である (つまり、ソース ファイルが保持される) が、コンパイル中に失われ、アノテーション情報が .class ファイルに保持されないことを説明するために使用されます。

プログラム開発の過程で最もよく使われるカスタムアノテーションは実行時@Retention(RetentionPolicy.RUNTIME)アノテーションであり、アスペクト、インターセプタ、リフレクションなどの仕組みと組み合わせることで、プログラムの実行中にクラスに対してアノテーションに応じた論理的な処理を行うことができます。

Lombok のアノテーションはソース ファイルの保持レベルのアノテーションであり、対応するアノテーション情報はクラス ファイルにコンパイルされると失われます。では、エンティティ クラスを強化するためにどのようなメカニズムが使用されるのでしょうか? ? ?

補足:
注釈情報の 3 つの保持戦略:

public enum RetentionPolicy {
    
    
    /**
     * Annotations are to be discarded by the compiler.
     * 注解信息被编译器丢弃
     */
    SOURCE,

    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time.  This is the default
     * behavior.
     * 注解信息会被编译器保留到class文件中,但是JVM运行期间不会保留。默认保留策略
     */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     * 注解信息会被编译器保留再class文件中,并且在JVM运行期间保留
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}

2. コンパイル時のアノテーション処理

Pluggable Annotation Processing APIJDK 6以降では、JSR 269:(コンパイル時のアノテーションプロセッサ)が追加され
、この処理期間を通じて、Lombok実装の核となる、コンパイル時のアノテーション情報に応じて、生成されるクラス情報を強化することができます。

ソース ファイル レベルで一連のアノテーションを宣言し、コンパイル時に AbstractProcessor クラスを継承してアノテーション プロセッサをカスタマイズし、その init() メソッドと process() メソッドを書き換えて、コンパイル時に Lombok アノテーションを Java ルーチンに変換します。

しかし同時に、Lombok には次のような使用上の欠点もあります。降低了可调试性互換性の問題がある可能性があるため、ビジネス シナリオと実際の状況に応じて、Lombok を使用するかどうか、および Lombok を使用する方法を選択する必要があります。

次に、Oracle の javac コンパイル ツールを例として、lombok の原理を分析します。Java 6 以降、javac は JSR 269 Pluggable Annotation Processing API 仕様のサポートを開始しており、プログラムが API を実装している限り、Java ソース コードのコンパイル時に定義されたアノテーションを呼び出すことができます。
たとえば、「JSR 269 API」を実装するプログラム A がある場合、javac を使用してソース コードをコンパイルするときの具体的なプロセスは次のとおりです。
1. javac はソース コードを分析し、抽象構文ツリー (AST) を生成します。
2. 実行プロセス中に「JSR 269 API」を実装する A プログラムを呼び出す;
3. このとき、A プログラムは、最初のステップで取得した抽象構文ツリー (AST) の変更を含む、独自のロジックを完成させることができます
。 javac は、変更された抽象構文ツリー (AST) を使用してバイトコード ファイルを生成します。

詳細なフローチャートは次のとおりです。
ここに画像の説明を挿入
コンパイル段階で、Java ソース コードが構文ツリー (AST) に抽象化されると、Lombok は独自の注釈プロセッサに従って AST を動的に変更し、新しいコードを追加することがわかります (これがすべて実行された後、解析を通じて最終的なバイトコード (.class) ファイルが生成されます。これが Lombok の実行原理です。

3. ロンボク島の使い方

Lombok プロジェクトの使用方法はシンプルで、次の 4 つのステップに分かれています。

  1. プラグインをインストールし、lombok.jar パッケージをコンパイル クラス パスに追加します (具体的なインストール方法は Baidu で見つけることができます)。
  2. 簡略化する必要があるクラスまたはメソッドに、使用するアノテーションを追加します。
  3. lombok をサポートするコンパイル ツールを使用してソース コードをコンパイルします (lombok をサポートするコンパイル ツールについては、「4. lombok をサポートするコンパイル ツール」を参照)。
  4. Lombok アノテーションに対応するメソッドまたはコードは、コンパイルされたバイトコード ファイル内に自動的に生成されます。

共通の IDEA 開発ツールを例にとると、まず IDEA に Lombok プラグインをインストールする必要があります。このステップの機能は、Lombok アノテーションのコンパイル時アノテーション プロセッサを追加することです。
ここに画像の説明を挿入
lombok の Meven 依存関係をプロジェクトに導入する必要があります。プロジェクトには主に lombok 宣言のすべての注釈情報が含まれます。

  <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
  </dependency>

4. カスタム注釈プロセッサ

カスタム アノテーション プロセッサを実装するには 3 つの手順が必要です。1
つ目はカスタム アノテーションを宣言すること、2 つ目はアノテーションを処理するプロセッサ インターフェイスを実装すること、3 つ目はアノテーション プロセッサを登録することです。

1. カスタム注釈

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({
    
    ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface CustomAnnotation{
    
    

}

2. プロセッサー・インターフェースの実装

アノテーション プロセッサは Processor インターフェイスを実装することでカスタマイズできますが、ここでは AbstractProcessor クラスを継承することでより簡単にカスタム アノテーション プロセッサを実装する方法を採用します。必要な関数を処理するために抽象メソッドのプロセスを実装します。

アノテーション プロセッサには JDK1.5 からこの機能がありましたが、当時のアノテーション プロセッサは apt であり、関連する API は com.sun.mirror パッケージの下にありました。JDK1.6 以降、apt 関連の機能が javac に組み込まれ、アノテーションを処理するための新しい API が javax.annotation.processing および javax.lang.model の 2 つのパッケージで提供されます。古いアノテーション プロセッサ API は JDK1.7 で非推奨としてマークされ、apt および関連 API は JDK1.8 で削除されました。

public class CustomProcessor extends AbstractProcessor {
    
    
    //核心方法:注解处理过程
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
    
    
        return false;
    }

    //支持的注解类型
    @Override
    public Set<String> getSupportedAnnotationTypes() {
    
    
        Set<String> annotataions = new LinkedHashSet<String>();
        annotataions.add(CustomAnnotation.class.getCanonicalName());
        return annotataions;
    }

    //支持的java版本
    @Override
    public SourceVersion getSupportedSourceVersion() {
    
    
        return SourceVersion.latestSupported();
    }
}

サポートされているアノテーション タイプと JDK バージョンをアノテーションで指定することもできます。

@SupportedAnnotationTypes({
    
    "com.laowan.annotation.CustomAnnotation"})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class CustomProcessor extends AbstractProcessor {
    
    
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
    
    
        return false;
    }
}

互換性上の理由から、特に Android プラットフォームの場合、 @SupportedAnnotationTypes および @SupportedSourceVersion の代わりに、オーバーロードされた getSupportedAnnotationTypes() および getSupportedSourceVersion() メソッドを使用することをお勧めします。

3. アノテーションプロセッサを登録する

最後に、カスタム注釈プロセッサを登録する必要もあります。

方法 1:
リソースに新しいフォルダーを作成し、そのディレクトリの下にresources新しいフォルダーを作成し、そのディレクトリの下に新しいサービス フォルダーを作成し、そのディレクトリの下に新しいファイルを作成して、カスタム アノテーション プロセッサの完全なクラス名をこのファイルに書き込みます。 :META-INFjavax.annotation.processing.Processor

com.laowan.annotation.CustomProcessor

注 ⚠️:
上記の方法を使用してカスタム アノテーション プロセッサを登録する場合は、必ずresourcesフォルダーをリソース ルートとして設定してください。そうしないと、コンパイル中に、ファイル内に構成されたプロセッサが見つからない
というメッセージのみが表示されます。javax.annotation.processing.Processor
ここに画像の説明を挿入

例、lombok にアノテーション プロセッサを登録します。
ここに画像の説明を挿入

方法 2:自動サービス
上の登録方法は面倒すぎるため、このファイルを生成するためのアノテーション プロセッサの作成に Google が協力してくれました。
github アドレス: https://github.com/google/auto

依存関係を追加します。

<!-- https://mvnrepository.com/artifact/com.google.auto.service/auto-service -->
<dependency>
  <groupId>com.google.auto.service</groupId>
  <artifactId>auto-service</artifactId>
  <version>1.0.1</version>
</dependency>

注釈を追加します。

@AutoService(Processor.class)
public class CustomProcessor extends AbstractProcessor {
    
    
    ...
}

ロンボク島の例:

@SupportedAnnotationTypes({
    
    "lombok.*"})
public static class ClaimingProcessor extends AbstractProcessor {
    
    
    public ClaimingProcessor() {
    
    
    }

    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    
    
        return true;
    }

    public SourceVersion getSupportedSourceVersion() {
    
    
        return SourceVersion.latest();
    }
}

それを実行して、注釈プロセッサの能力を実感してください。後で、注釈プロセッサの処理ロジックに注目するだけで済みます。

5. 実際のMyGetterノート

Getter メソッドをカスタマイズするために Lombok の単純なバージョンを実装します。実装手順は次のとおりです。

  1. 注釈 MyGetter をカスタマイズし、カスタム注釈プロセッサを実装します。
  2. tools.jar の javac API を使用して AST (抽象構文ツリー) を処理します。
  3. カスタム アノテーション プロセッサを使用してコードをコンパイルします。

1. 新しい Maven プロジェクト myLombok を作成します

これには 2 つのサブモジュールが含まれています。myget はカスタム アノテーションとアノテーション プロセッサを保存するために使用され、person モジュールはカスタム アノテーションを使用するために使用されます。
ここに画像の説明を挿入

myLombok プロジェクトの pom.xml ファイル:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <packaging>pom</packaging>
    <modules>
        <module>myget</module>
        <module>person</module>
    </modules>

    <groupId>com.example</groupId>
    <artifactId>myLombok</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>myLombok</name>

    <properties>
        <java.version>1.8</java.version>
    </properties>
</project>

注:
ここでは spring-boot-maven-plugin コンパイラーを使用しないでください。使用しないと、コンパイルが失敗します。

2. 新しいサブモジュール myget を作成します

1.Maven 依存関係を追加する

    <dependencies>
        <!--Processor中的解析过程需要依赖tools.jar-->
        <dependency>
            <groupId>com.sun</groupId>
            <artifactId>tools</artifactId>
            <version>1.6.0</version>
            <scope>system</scope>
            <systemPath>${java.home}/../lib/tools.jar</systemPath>
        </dependency>

        <!--采用google的auto-service来注入注解处理器-->
        <dependency>
            <groupId>com.google.auto.service</groupId>
            <artifactId>auto-service</artifactId>
            <version>1.0.1</version>
        </dependency>
    </dependencies>

2. まず、MyGetter.java カスタム アノテーションを作成します。コードは次のとおりです。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.SOURCE) // 注解只在源码中保留
@Target(ElementType.TYPE) // 用于修饰类
public @interface MyGetter {
    
     // 定义 Getter

}

2. カスタム アノテーション プロセッサ MyGetterProcessor を実装します。コードは次のとおりです。

//这里的导入最好直接拷贝过去
import com.sun.source.tree.Tree;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.tree.TreeTranslator;
import com.sun.tools.javac.util.*;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import java.util.Set;

@AutoService(Processor.class) //自动注入注解处理器
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.example.myget.annotation.MyGetter")
public class MyGetterProcessor extends AbstractProcessor {
    
    

    private Messager messager; // 编译时期输入日志的
    private JavacTrees javacTrees; // 提供了待处理的抽象语法树
    private TreeMaker treeMaker; // 封装了创建AST节点的一些方法
    private Names names; // 提供了创建标识符的方法

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
    
    
        super.init(processingEnv);
        this.messager = processingEnv.getMessager();
        this.javacTrees = JavacTrees.instance(processingEnv);
        Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
        this.treeMaker = TreeMaker.instance(context);
        this.names = Names.instance(context);
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    
    
        Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(MyGetter.class);
        elementsAnnotatedWith.forEach(e -> {
    
    
            JCTree tree = javacTrees.getTree(e);
            tree.accept(new TreeTranslator() {
    
    
                @Override
                public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
    
    
                    List<JCTree.JCVariableDecl> jcVariableDeclList = List.nil();
                    // 在抽象树中找出所有的变量
                    for (JCTree jcTree : jcClassDecl.defs) {
    
    
                        if (jcTree.getKind().equals(Tree.Kind.VARIABLE)) {
    
    
                            JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) jcTree;
                            jcVariableDeclList = jcVariableDeclList.append(jcVariableDecl);
                        }
                    }
                    // 对于变量进行生成方法的操作
                    jcVariableDeclList.forEach(jcVariableDecl -> {
    
    
                        messager.printMessage(Diagnostic.Kind.NOTE, jcVariableDecl.getName() + " has been processed");
                        jcClassDecl.defs = jcClassDecl.defs.prepend(makeGetterMethodDecl(jcVariableDecl));
                    });
                    super.visitClassDef(jcClassDecl);
                }
            });
        });
        return true;
    }

    private JCTree.JCMethodDecl makeGetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl) {
    
    
        ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
        // 生成表达式 例如 this.a = a;
        JCTree.JCExpressionStatement aThis = makeAssignment(treeMaker.Select(treeMaker.Ident(
                names.fromString("this")), jcVariableDecl.getName()), treeMaker.Ident(jcVariableDecl.getName()));
        statements.append(aThis);
        JCTree.JCBlock block = treeMaker.Block(0, statements.toList());

        // 生成入参
        JCTree.JCVariableDecl param = treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER),
                jcVariableDecl.getName(), jcVariableDecl.vartype, null);
        List<JCTree.JCVariableDecl> parameters = List.of(param);

        // 生成返回对象
        JCTree.JCExpression methodType = treeMaker.Type(new Type.JCVoidType());
        return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC),
                getNewMethodName(jcVariableDecl.getName()), methodType, List.nil(),
                parameters, List.nil(), block, null);

    }

    private Name getNewMethodName(Name name) {
    
    
        String s = name.toString();
        return names.fromString("get" + s.substring(0, 1).toUpperCase() + s.substring(1, name.length()));
    }

    private JCTree.JCExpressionStatement makeAssignment(JCTree.JCExpression lhs, JCTree.JCExpression rhs) {
    
    
        return treeMaker.Exec(
                treeMaker.Assign(
                        lhs,
                        rhs
                )
        );
    }
}

Lombok の単純バージョンを実装するには、カスタム アノテーション プロセッサが最も重要です。AbstractProcessor クラスを継承し、その init() メソッドと process() メソッドを書き直す必要があります。process() メソッドでは、最初にすべての変数をクエリします、対応するメソッドを変数に追加します。使用 TreeMaker 对象和 Names 来处理 AST,这一步需要依赖 tool.jar上記のコードに示すように、We 。

3. 新しいサブモジュール person を作成します

1. Maven 依存関係を導入する

      <dependency>
          <groupId>com.example</groupId>
          <artifactId>myget</artifactId>
          <version>0.0.1-SNAPSHOT</version>
      </dependency>

2. Personクラスを追加し、@MyGetterクラスのアノテーションを追加します

@MyGetter
public class Person {
    
    
    private String name;
}

4. 結果をコンパイルして表示する

1. コンパイルとパッケージングを実行する
ここに画像の説明を挿入

2. Person クラスのコンパイル結果を確認します。get メソッドが自動的に生成され、カスタム アノテーション @MyGetter が有効になっていることがわかります。
ここに画像の説明を挿入


要約する

この記事では主に Lombok の実装原理を紹介し、カスタム アノテーション @MyGetter を介したコンパイル時アノテーション プロセッサの使用方法を示します。
1. アノテーション情報の RetentionPolicy は、メタアノテーション @Retention を通じて構成できます。

  • SOURCEソース ファイル保持ポリシー、アノテーション情報はコンパイル中に破棄されます
  • CLASSクラス ファイル保持ポリシーでは、アノテーション情報はコンパイラによってクラス ファイルに保持されますが、JVM の動作中は保持されません。デフォルトの保持ポリシー
  • RUNTIME実行時保持ポリシー。注釈情報はコンパイラによってクラス ファイルに保持され、JVM 操作中に保持されます。

2. Lombok のアノテーションは、SOURCE ソースファイル保持ポリシーのアノテーションであり、実装原則は JDK 6 以降に追加された JSR 269 (Pluggable Annotation Processing APIコンパイル時のアノテーションプロセッサ) を使用することであり、生成されるクラス情報が強化されています。

3. カスタムコンパイル時アノテーションの登録方法には、リソース方式と自動サービス方式の 2 つがあります。

おすすめ

転載: blog.csdn.net/w1014074794/article/details/128244097