Javaアノテーションのコンパイル処理の詳細説明 AbstractProcessor

概要

連絡するアノテーションは、主に次の 2 つのカテゴリに分類されます。

  1. ランタイム アノテーション: リフレクションを介して実行時にアノテーション ロジックを動的に処理する

  2. コンパイル時の注釈: 注釈プロセッサを介して、コンパイル時に関連するロジックを動的に処理します

私たちが通常接触するフレームワークのほとんどは、@Autowire @Resoure @Bean などのランタイム アノテーションです。

それで、私たちが通常接触するコンパイル時の注釈@Lombok @AutoServiceなど.

これらのコンパイル時のアノテーションの機能は、コードを自動的に生成することであり、1 つはコーディングの効率を向上させることであり、もう 1 つは実行時にリフレクションを大量に使用することを回避し、コンパイル時にリフレクションを使用して実行時に使用する補助クラスとメソッドを生成することです。時間。

では、これらのコンパイラ アノテーションはどのように機能するのでしょうか?どのようにコードを自動的に生成するのでしょうか?

今日は詳しく紹介しますが、紹介する前にJavaアノテーションの基本概念を簡単に理解しておきましょ

注釈プロセッサ

アノテーション処理の流れ

注釈のコンパイル プロセスで最も重要なクラスはProcessorであり、これは注釈プロセッサのインターフェイス クラスです. コンパイル時に注釈を処理する必要があるすべてのロジックは、このProcessorインターフェイスを実装する必要があります. もちろん、AbstractProcessor抽象クラスが役立ちます大きなパーツはすべてプロセスであるため、この抽象クラスを実装するだけで、注釈プロセッサを簡単に定義できます。

注釈処理パイプラインは、複数のラウンドで実行されます。各ラウンドは、コンパイラがソース ファイルで注釈を検索し、それらの注釈に適した注釈から始まります。注釈プロセッサは、対応するソースで順番に呼び出されます。

このプロセス中にファイルが生成された場合、生成されたファイルを入力として別のラウンドが開始されます。このプロセスは、処理フェーズ中に新しいファイルが生成されなくなるまで続きます。

注釈プロセッサの処理手順:

  • Java コンパイラでビルドします。
  • コンパイラは、実行されていない注釈プロセッサの実行を開始します。
  • 注釈要素 (要素) を循環して、注釈によって変更されたクラス、メソッド、または属性を見つけます。
  • 対応するクラスを生成し、ファイルに書き込みます。
  • すべての注釈プロセッサが実行されたかどうかを判断し、そうでない場合は、次の注釈プロセッサの実行を続行します (ステップ 1 に戻ります)。

图TODO

AbstractProcessor

これは注釈プロセッサのコア抽象クラスです。内部のメソッドを見てみましょう

getSupportedOptions()

デフォルトの実装は、SupportedOptionsアノテーションから値を取得することです。これは文字配列です。たとえば、


@SupportedOptions({
    
    "name","age"})
public class SzzTestProcessor extends AbstractProcessor {
    
    
}

しかし、インターフェースは役に立たないようです。

一部の情報源は、このオプションのパラメーターが processingEnv から取得できることを示しています。

String resultPath = processingEnv.getOptions().get(参数);

実際、取得されたパラメーターは、コンパイル時に入力パラメーターによって -Akey=name設定され、getSupportedOptions とは関係ありません。

getSupportedAnnotationTypes

現在の注釈処理クラスが処理できる注釈タイプを取得します。デフォルトの実装は、SupportedAnnotationTypes注釈から取得することです。
注釈値は文字列配列String []です。
一致する注釈は、現在の注釈処理クラスのプロセス メソッドを介して渡されます。 .

たとえば、次の例では * ワイルドカードを使用してすべての注釈をサポートしています

@SupportedAnnotationTypes("*")
@SupportedSourceVersion(SourceVersion.RELEASE_11)
public class PrintingProcessor extends AbstractProcessor {
    
    

}

または、このインターフェイスを直接オーバーライドできます

  @Override
  public ImmutableSet<String> getSupportedAnnotationTypes() {
    
    
    return ImmutableSet.of(AutoService.class.getName());
  }

最終的に、それらが有効になる場所はフィルタリングに使用されます。これは、処理中にすべての注釈が取得され、この構成に従って、それ自体で処理できる注釈が取得されるためです。

getSupportedSourceVersion

アノテーション プロセッサがサポートできる最大バージョンを取得します. デフォルトでは、アノテーションSupportedSourceVersionからか、メソッドを自分で書き換えます. 存在しない場合、デフォルト値はRELEASE_6です

@SupportedSourceVersion(SourceVersion.RELEASE_11)
public class PrintingProcessor extends AbstractProcessor {
    
    

}

または書き直します(推奨、最新バージョンを入手してください)

  @Override
  public SourceVersion getSupportedSourceVersion() {
    
    
    //设置为能够支持最新版本
    return SourceVersion.latestSupported();
  }

初期化の初期化

init は、ProcessingEnvironment オブジェクトを渡す初期化メソッドです。通常、抽象クラスを直接使用するだけで、書き換える必要はありません。
もちろん、自分の必要に応じて再作成することもできます。

    @Override
    public synchronized void init(ProcessingEnvironment pe) {
    
    
        super.init(pe);
        System.out.println("SzzTestProcessor.init.....");
        // 可以获取到编译器参数(下面两个是一样的)
        System.out.println(processingEnv.getOptions());
        System.out.println(pe.getOptions());

    }

コンパイラのカスタム パラメータを取得するなど、多くの情報を取得できます. カスタム パラメータの設定については、次のセクションのコンパイル用パラメータの設定方法を参照してください。

パラメータの説明

方法 説明
要素 getElementUtils() 要素を操作するためのツール クラスである Elements インターフェイスを実装するオブジェクトを返します。
ファイル getFiler() ファイル、クラス、および補助ファイルを作成するための Filer インターフェイスを実装するオブジェクトを返します。
メッセンジャー getMessenger() エラー メッセージと警告リマインダーを報告するために使用される Messager インターフェイスを実装するオブジェクトを返します。
Map<String,String> getOptions() 指定されたパラメーター オプションを返します。
タイプ getTypeUtils() 型を操作するためのツール クラスである Types インターフェイスを実装するオブジェクトを返します。

プロセス処理方法

process メソッドは 2 つのパラメーターを提供します。1 つ目は処理を要求するアノテーション タイプのコレクション (つまり、getSupportedAnnotationTypes メソッドをオーバーライドして指定したアノテーション タイプ) で、2 つ目は現在および最後のサイクルに関する情報の環境です。

戻り値は、これらの注釈がこの Processor によって宣言されているかどうかを示します
. true を返す場合、これらの注釈は後続の Processor によって処理されません.
false を返す場合、これらの注釈は後続の Processor によって処理されます.


    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    
    
        System.out.println("SzzTestProcessor.process.....;");

        return false;
    }
    

RoundEnvironment インターフェイスを介して注釈要素を取得できます. 注釈は注釈型のみであり、どのインスタンスに注釈が付けられているかはわかりません. RoundEnvironment は、どのインスタンスに注釈が付けられているかを知ることができます.

方法 説明
セット<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a) 指定された注釈型で注釈が付けられた要素のコレクションを返します。
セット<? 要素を拡張> getElementsAnnotatedWith(TypeElement a) 指定された注釈型で注釈が付けられた要素のコレクションを返します。
processingOver() ループ処理が完了した場合は true、そうでない場合は false を返します。

この部分の使用の概要については、次のカスタム アノテーション プロセッサの例を参照してください。

注釈プロセッサの登録方法

アノテーション プロセッサのいくつかのコア メソッドは上記で紹介されていますが、アノテーション プロセッサを登録するにはどうすればよいでしょうか

アノテーション プロセッサ (AbstractProcessor) はコンパイル時に実行され、Jar パッケージとして有効になるため、AbstractProcessor クラスを実装すると有効になるわけではありません。そのため、アノテーションプロセッサのモジュールとして使用してパックする必要があります。. 次に、注釈プロセッサへの Module 参照を使用する必要があります

この注釈プロセッサが配置されているモジュールをパッケージ化するときは、次の点に注意する必要があります。

AbstractProcessor は基本的にServiceLoader (SPI)を介してロードされるため、正常に登録される必要があります。それから2つの方法があります

1.SPI を構成する

  1. resource/META-INF.servicesフォルダーの下にという名前のファイルを作成します。内部のコンテンツは、注釈プロセッサjavax.annotation.processing.Processor完全修飾クラス名です。
    ここに画像の説明を挿入

  2. コンパイル中に処理が禁止されている理由は、プロセスを禁止しない場合、ServiceLoader は先ほど設定したアノテーション プロセッサをロードしますが、コンパイル中のため、Class ファイルが正常にロードされないため、次のようにスローされます。例外

    
    服务配置文件不正确, 或构造处理程序对象javax.annotation.processing.Processor: Provider org.example.SzzTestProcessor not found时抛出异常错误
    
    
    

    Mavenでコンパイルされている場合は、次の構成を追加してください<compilerArgument>-proc:none</compilerArgument>

              <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.5.1</version>
                    <configuration>
                        <source>1.8</source>
                        <target>1.8</target>
                    </configuration>
                    <executions>
                        <execution>
                            <id>default-compile</id>
                            <configuration>
                                <compilerArgument>-proc:none</compilerArgument>
                            </configuration>
                        </execution>
                        <execution>
                            <id>compile-project</id>
                            <phase>compile</phase>
                            <goals>
                                <goal>compile</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
    
  3. 注釈プロセッサは正常にパッケージ化されており、他のモジュールに提供できます。

2. @AutoService を使用して、SPI 構成ファイルを自動的に構成します。

@AutoService は、Google によってオープン ソース化された小さなプラグインです。自動的にMETA-INF/servicesファイルを生成できるため、構成ファイルを手動で作成する必要はありません。

もちろん、上記の<compilerArgument>-proc:none</compilerArgument>パラメータも必要ありません。

したがって、コンパイル時にそのような問題は発生しませんxxx not found コンパイル時に注釈プロセッサがMETA-INF/services構成されていないため、ロード例外はスローされません。

たとえば、以下では @AutoService(Processor.class) を使用して、対応する構成ファイルを自動的に生成します。

@AutoService(Processor.class)
public class SzzBuildProcessor extends AbstractProcessor {
    
    

}

ここに画像の説明を挿入

さらに、実際には@AutoService はAbstractProcessor を介して構成ファイルを自動的に生成します。

具体的な使用方法については、@AutoService の詳細な説明を参照してください。

コンパイル時のコードをデバッグする方法

注釈プロセッサを自分で作成した後、デバッグが必要になる場合があるため、コンパイル時のデバッグは実行時のデバッグとは異なります。

参照: IDEA でコンパイル時のソース コードをデバッグする方法

Maven関連の設定(有効なプロセッサを指定)

Maven を使用してコンパイルしている場合は、設定できるパラメーターがいくつかあります。

たとえば、注釈プロセッサが有効になり、コードが生成されるソース パスを指定します。デフォルトはtarget/generated-sources/annotations

特別な事情がない限り、通常、これらのパラメータを設定する必要はありません。

<build>
    <plugins>

        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.5.1</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
                <encoding>UTF-8</encoding>
                <!-- 主动设置生成的源码的文件夹路径,默认的就是下面的地址。一般不需要主动设置除非你有自己的需求  -->
                <generatedSourcesDirectory>${
    
    project.build.directory} /generated-sources/</generatedSourcesDirectory>
                 <!-- 指定生效的注解处理器,这里设置之后,只会有下面配置的注解处理器生效; 一般情况也不用主动配置,可以将下面的全部删除 -->
                <annotationProcessors>
                    <annotationProcessor>
                            org.example.SzzTestProcessor
                    </annotationProcessor>
                </annotationProcessors>
            </configuration>
        </plugin>

    </plugins>
</build>

予防

注釈注釈プロセッサは別個のモジュールです。注釈プロセッサはコンパイル時にのみ使用する必要があり、注釈モジュールは注釈プロセッサの Jar パッケージをインポートするだけで済みます。したがって、注釈プロセッサを別のモジュールに分離する必要があります。

また、パッケージ化する場合は、アノテーションプロセッサのモジュールを先にパッケージ化してください。

カスタム Processor クラスは最終的に jar にパッケージ化され、コンパイル プロセス中に呼び出されます。

カスタム注釈プロセッサの例

例 1: Build コンストラクターを自動的に生成する

1. 要件の説明

いくつかのフィールドを持つ、注釈付きのユーザー モジュールにいくつかの単純な POJO クラスがあるとします。


public class Company {
    
    

    private String name;

    private String email ;
    
}

public class Personal {
    
    

    private String name;

    private String age;
}

POJO クラスをよりスムーズにインスタンス化するために、対応するビルダー ヘルパー クラスを作成したい

        Company company = new CompanyBuilder()
                .setName("ali").build();
        Personal personal = new PersonalBuilder()
                .setName("szz").build();

2.需要分析

POJO がない場合、対応するビルド ビルダーを手動で作成するのは複雑すぎます. 注釈を使用して、POJO クラスに対応するビルド ビルダーを自動的に生成できますが、もちろん、すべてが生成されるわけではありません. オンデマンドで生成されます。

  1. @BuildProperty アノテーションを定義し、対応する setXX メソッドを生成する必要があるメソッドにアノテーションをマークします

  2. カスタムアノテーションプロセッサは@BuildProperty アノテーションをスキャンし、要件に従ってビルド ビルダーを自動的に生成します。たとえば、CompanyBuilder

    	public class CompanyBuilder {
          
          
    
        private Company object = new Company();
    
        public Company build() {
          
          
            return object;
        }
    
        public CompanyBuilder setName(java.lang.String value) {
          
          
            object.setName(value);
            return this;
        }
    
    }
    

3.コーディング

アノテーション プロセッサ モジュールを作成します: szz-test-processor-handler

@ビルドプロパティ

@Target(ElementType.METHOD) // 注解用在方法上
@Retention(RetentionPolicy.SOURCE) // 尽在Source处理期间可用,运行期不可用
public @interface BuildProperty {
    
    
}

注釈プロセッサ

@SupportedAnnotationTypes("org.example.BuildProperty") // 只处理这个注解;
public class SzzBuildProcessor extends AbstractProcessor {
    
    

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    
    
        System.out.println("SzzBuildProcessor.process ;");

        for (TypeElement annotation : annotations) {
    
    
            // 获取所有被该注解 标记过的实例
            Set<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(annotation);

            // 按照需求 检查注解使用的是否正确 以set开头,并且参数只有一个
            Map<Boolean, List<Element>> annotatedMethods = annotatedElements.stream().collect(
                    Collectors.partitioningBy(element ->
                            ((ExecutableType) element.asType()).getParameterTypes().size() == 1
                                    && element.getSimpleName().toString().startsWith("set")));

            List<Element> setters = annotatedMethods.get(true);
            List<Element> otherMethods = annotatedMethods.get(false);

            // 打印注解使用错误的case
            otherMethods.forEach(element ->
                    processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
                            "@BuilderProperty 注解必须放到方法上并且是set开头的单参数方法", element));

            if (setters.isEmpty()) {
    
    
                continue;
            }


            Map<String ,List<Element>> groupMap = new HashMap();

            // 按照全限定类名分组。一个类创建一个Build
            setters.forEach(setter ->{
    
    
                // 全限定类名
                String className = ((TypeElement) setter
                        .getEnclosingElement()).getQualifiedName().toString();
                List<Element> elements = groupMap.get(className);
                if(elements != null){
    
    
                    elements.add(setter);
                }else {
    
    
                    List<Element> newElements = new ArrayList<>();
                    newElements.add(setter);
                    groupMap.put(className,newElements);
                }
            });

            
            groupMap.forEach((groupSetterKey,groupSettervalue)->{
    
    
                //获取 类名SimpleName 和 set方法的入参
                Map<String, String> setterMap = groupSettervalue.stream().collect(Collectors.toMap(
                        setter -> setter.getSimpleName().toString(),
                        setter -> ((ExecutableType) setter.asType())
                                .getParameterTypes().get(0).toString()
                ));
                try {
    
    
                    // 组装XXXBuild类。并创建对应的类文件
                    writeBuilderFile(groupSetterKey,setterMap);
                } catch (IOException e) {
    
    
                    throw new RuntimeException(e);
                }

            });
        }

        // 返回false 表示 当前处理器处理了之后 其他的处理器也可以接着处理,返回true表示,我处理完了之后其他处理器不再处理
        return true;
    }

    private void writeBuilderFile(
            String className, Map<String, String> setterMap)
            throws IOException {
    
    

        String packageName = null;
        int lastDot = className.lastIndexOf('.');
        if (lastDot > 0) {
    
    
            packageName = className.substring(0, lastDot);
        }

        String simpleClassName = className.substring(lastDot + 1);
        String builderClassName = className + "Builder";
        String builderSimpleClassName = builderClassName
                .substring(lastDot + 1);

        JavaFileObject builderFile = processingEnv.getFiler()
                .createSourceFile(builderClassName);

        try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {
    
    

            if (packageName != null) {
    
    
                out.print("package ");
                out.print(packageName);
                out.println(";");
                out.println();
            }

            out.print("public class ");
            out.print(builderSimpleClassName);
            out.println(" {");
            out.println();

            out.print("    private ");
            out.print(simpleClassName);
            out.print(" object = new ");
            out.print(simpleClassName);
            out.println("();");
            out.println();

            out.print("    public ");
            out.print(simpleClassName);
            out.println(" build() {");
            out.println("        return object;");
            out.println("    }");
            out.println();

            setterMap.entrySet().forEach(setter -> {
    
    
                String methodName = setter.getKey();
                String argumentType = setter.getValue();

                out.print("    public ");
                out.print(builderSimpleClassName);
                out.print(" ");
                out.print(methodName);

                out.print("(");

                out.print(argumentType);
                out.println(" value) {");
                out.print("        object.");
                out.print(methodName);
                out.println("(value);");
                out.println("        return this;");
                out.println("    }");
                out.println();
            });

            out.println("}");
        }
    }

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
    
    
        super.init(processingEnv);
        System.out.println("----------");

        System.out.println(processingEnv.getOptions());

    }
    @Override
    public SourceVersion getSupportedSourceVersion() {
    
    
        return SourceVersion.latestSupported();
    }


}

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

ここに画像の説明を挿入

5. コンパイル パラメータを設定する

ここでは手動構成が選択されているためMETA-INF.services、コンパイル中にプロセッサを無視するように構成する必要があります。
主なパラメータは次のとおりです。

<compilerArgument>-proc:none</compilerArgument>

次のように

<build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.5.1</version>
                    <configuration>
                        <source>1.8</source>
                        <target>1.8</target>
                    </configuration>
                    <executions>
                        <execution>
                            <id>default-compile</id>
                            <configuration>
                                <compilerArgument>-proc:none</compilerArgument>
                            </configuration>
                        </execution>
                        <execution>
                            <id>compile-project</id>
                            <phase>compile</phase>
                            <goals>
                                <goal>compile</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>

             </plugins>

        </pluginManagement>

    </build>


6. コンパイルとパッケージの実行

mvn インストール後、他のモジュールを参照できます。

7.デモモジュールは注釈プロセッサに依存します

新しいモジュールを作成します: szz-test-demo ; 上記のszz-test-processor-handlerに依存するようにします

また、Company のいくつかのメソッドでアノテーションを使用します。
ここに画像の説明を挿入

8. デモ モジュールをコンパイルし、BuildCompany クラスを自動的に生成します。

デモ モジュールがコンパイルされると、ターゲット フォルダーに BuildXXX クラスが生成されます。また、注釈 BuildProperty でマークしたメソッドのみが、対応するメソッドを生成します。
注釈 BuildProperty が間違った方法で使用されている場合は、例外も出力されます。
ここに画像の説明を挿入

例 2:

追加予定。

コンパイル用パラメータの設定方法

init によって初期化されたインターフェイスでは、コンパイラのいくつかのカスタム パラメーターを取得できます。

    String verify = processingEnv.getOptions().get("自定义key");

-A开头取得されたコンパイラ パラメーターは、フィルター処理されているため、対応するパラメーターのみを取得できることに注意してください。

ここに画像の説明を挿入

では、このカスタム パラメータはどこから設定されているのでしょうか。

あなたがIDEAコンパイルの場合

-Akey=value 或者 -Akey

ここに画像の説明を挿入

Mavenでコンパイルした場合

ここに画像の説明を挿入

おすすめ

転載: blog.csdn.net/u010634066/article/details/129941559