Java注釈処理(Annotation Processor)実戦:Excelエクスポートインタフェース自動生成


以前のJava APTに関する記事から半年近くが経ち、この半年の間にAPTに関するアプリケーションもいくつか行われていましたが、最近は正月に家でExcelエクスポートインタフェースが自動的に生成したプロジェクトをGithub(https://github.com/DreamJM/DreamSpringExcel)、皆さんが好きならリンクをクリックして星をクリックしてサポートしてください.
1.需要&背景
同様のバックグラウンド管理システムでは、いくつかのTableデータのクエリーと表示ページがあり、同時にデータをExcelにエクスポートできることが要求され、エクスポートデータは基本的にTableページの表示データと同じである.一方,poiおよび同じデータソースを用いてExcelエクスポートインタフェースを実装する場合,通常は大量の雷同コードを伴う.
そのためには、既存のデータ照会インタフェースに加えて、注釈定義およびコンパイラの注釈プロセッサによって、Excelエクスポートインタフェースを自動的に生成することが望ましい.
2.現在の実装
今回のオープンソースプロジェクトは上記のニーズに対して実現され、注釈および関連汎用ツールはexcel-baseに定義され、注釈プロセッサ関連ロジックはexcel-processorにあり、興味のある方は説明とソースコードを見て、現在使用について説明します.
@ExcelSupport("com.dream.spring.excel.test.controller.excel.ExcelController")
@RestController
public class TestController {

    @ExcelExport(value = "/api/excel/test",
            annotations = {@AnnotationDef(clazz = TestAnnotation.class, members = {@AnnotationMember(name = "value", value = "\"hello\""),
                    @AnnotationMember(name = "children", value = "value=\"child\"", annotation = ChildValue.class)})})
    @GetMapping("/api/test")
    public Result> test(@RequestParam(required = false) String param1, @ParamIgnore("-1") @RequestParam int type,
                                         @ParamIgnore @RequestParam(required = false) Integer pageNum,
                                         @ParamIgnore @RequestParam(required = false) Integer pageSize) {
        ... ...
    }
}

上記の例では、既存のインタフェースに基づいて、次の2つの注釈が追加されています.
  • ExcelSupport:注釈プロセッサのエントリであり、そのvalue値は自動生成されたExcelエクスポートインタフェースのController名を示し、デフォルトでは注釈されたControllerと同じクラスのパッケージの下でExcelControllerが生成され、複数の注釈されたControllerは同じController名を指定できます.
  • ExcelExport:Excelによって生成されるデータソースメソッドを指定し、そのメソッドに基づいてExcelのエクスポートインタフェースを生成します.注記のvalue Excelエクスポートインタフェースのurlパスを指定し、annotationsは生成インタフェースに追加の注記を追加します.生成されたインタフェースメソッドのパラメータは、注記を含む一貫性を保っていますが、@ParamIgnoreで識別された注記は無視されます.Excelエクスポートインタフェースを生成すると、@ExcelExportによって注記されるメソッドが実際に呼び出されます.

  • 上記注釈は、インタフェースの生成を導出するために用いられ、具体的なExcel導出データとフォーマットの定義は、上記の例に示すように、そのExcelの各行のデータはTestクラスに基づいて生成され、Testクラスの注釈の例は以下の通りである.
    @Sheet(value = "Test", i18nSupport = false, indexIncluded = true,
            categories = {@Category(value = "test.child", start = 4, end = 5)},
            headers = {
                    @Header(value = "test.name", field = "name", width = 15, note = @HeaderNote(necessary = true, content = "test_note", i18nSupport = false)),
                    @Header(value = "test.value", field = "value"), @Header(value = "test.type", field = "type", width = 8),
                    @Header(value = "test.date", field = "date", width = 20), @Header(value = "test.childName", field = "component.childName"),
                    @Header(value = "test.childValue", field = "component.childValue")})
    public class Test extends BaseTest {
    
        private String name;
    
        @Column(converter = @Converter(clazz = ConverterUtils.class, method = "formatType"),
                cellStyles = @CellItemStyle(condition = "{value} == 1", style = @CellStyle(backgroundColor = IndexedColors.BLUE, fontColor = IndexedColors.WHITE)))
        private int type;
    
        @Column(converter = @Converter(clazz = ConverterUtils.class, method = "formatDate"))
        private Date date;
        
        private Component component;
        ... ...
    }
  • Sheet:ExcelエクスポートSheetデータの内容と基本スタイルを定義し、valueはsheet名、indexIncludedはシーケンス番号列を含むかどうかを示し、headersはすべての列のタイトル名と列の幅などのスタイルを識別する.特別な@Headerのfield属性は、列に対応するTestクラスのフィールド名を識別します.この名前は「.」上記の例のcomponentのようなネストを行う.childValueは、このカラムがcomponentプロパティの下にあるchildValueを埋めていることを示します.
  • Column:タイプを読み取り可能な文字列に変換したり、日付タイプをフォーマットしたりするなど、カラムのデータを特別に処理します.または、60未満のセルの背景色を赤に設定するなど、いくつかのセルのスタイルを定義します.

  • ただし、ExcelExportでマークされているメソッドの戻り値はCollectionやArrayタイプではなく、他のタイプによってパッケージされているため、@SheetWrapperタグで簡単にマークする必要があります.注記プロセッサは@ExcelExport戻り値タイプや@SheetWrapperタグに基づいて下を探し続けます.@SheetWrapperタグとしてマークされ、タイプがCollectionまたはArrayの値が見つかるまで、このCollectionまたはArrayのデータ型はエクスポートデータ型であり、@Sheet注記を解析します.
    public class Result {
    
        @SheetWrapper
        private T data;
        
        ... ...
    }
    public class PageResult {
    
        @SheetWrapper
        private List values;
        
        ....
     }

    上記の現在自動生成されているコードの例を次に示します.
    @RestController
    public class ExcelController {
      @Autowired
      TestController ref0;
      
      ... ...
    
      @GetMapping("/api/excel/test")
      @TestAnnotation(
          value = "hello",
          children = @com.dream.spring.excel.test.annotation.ChildValue(value="child")
      )
      public void test(@RequestParam(required = false) String param1, HttpServletResponse response)
          throws IOException {
          ... ...
          Result> result = ref0.test(param1,-1,null,null);
          ... ...
      }
      ... ...
    }

    「/api/excel/test」インタフェースを直接呼び出すことで、Excelファイルを直接エクスポートできます.これにより、Excelエクスポートコードを繰り返し書く必要はありません.
    3.Annotation Processor&Poetの一般的なピット
    具体的な実装は言うまでもなく、興味のある方はプロジェクトの「excel-processor」の実装を見て、ここでは主に過程でよくある問題を説明します.
    a)TypeElementの使用
    TypeElementは、処理で最も一般的に使用されるクラスであり、クラスとインタフェースタイプの要素を表し、対応するメソッド要素はExecutableElementである.実際の処理の過程で、得られたElementは実際の状況に応じて対応するTypeElementまたはExecutableElementに強く転換する必要があります.例えば、ExcelSupportが注釈するクラスのフルネームを取得したい場合:
    for (Element element : roundEnv.getElementsAnnotatedWith(ExcelSupport.class)) {
        TypeElement typeElement = (TypeElement) element;
        String fullName = typeElement.getQualifiedName().toString();
        ... ...
    }

    また、注釈処理の過程で、ある種類が工事中に存在するかどうかを判断する必要があり、以下の方法を使用することができる.
    TypeElement apiElement = processingEnv.getElementUtils().getTypeElement("io.swagger.annotations.Api");
    if (apiElement != null) {
        ... ...
    }

    b) TypeMirror & DeclaredType
    DeclaredTypeはTypeMirrorのサブタイプで、すべてのクラスとインタフェースのタイプを表しています.多くの場合、強い回転が必要です.たとえば、メソッドの戻りタイプでSheetWrapperラベル付きのサブ要素を取得する必要があります.
    DeclaredType returnType = ((DeclaredType) method.getMethodElement().getReturnType());
    Element element = returnType.asElement();
    for (Element childElem : element.getEnclosedElements()) {
         if (childElem.getAnnotation(SheetWrapper.class) != null) {
             ... ...
         }
    }

    c)注記にClassを使う
    注釈にはいくつかのタイプを定義する必要がある場合が多く、Classクラスが使用されますが、注釈処理では、classの取得を呼び出すと例外が放出されます(コンパイル中であることも理解できます).したがって、注釈処理中にclassに遭遇した場合、その例外をキャプチャし、例外プロンプトに基づいてclassに対応するクラスのフルネームを取得する必要があります.
    String annName = null;
    try {
        annDef.clazz();
    } catch (MirroredTypeException mte) {
        annName = mte.getTypeMirror().toString();
    }

    d)注記プロセッサにおける参照依存しない注記処理
    注記プロセッサで対応する要素の注記を取得するには、TypeElementのgetAnnotation(Class annotationType)メソッド。ただし、依存が導入されていない(すなわち、対応するClassがない)場合は、TypeElementのgetAnnotationMirrorsを巡る方法が必要です。)を使用します.
    for (AnnotationMirror ann : typeElement.getAnnotationMirrors()) {
        if ("io.swagger.annotations.Api".equals(ann.getAnnotationType().toString())) {
            for (Map.Entry extends ExecutableElement, ? extends AnnotationValue> entry : ann.getElementValues().entrySet()) {
                if ("tags()".equals(entry.getKey().toString())) {
                    String values = entry.getValue().toString();
                    ... ...
                }
            }
        }
    }

    e)Poetにおけるメンバー変数の汎用的なサポート
    Poetを使用して、汎用メンバー変数を定義するには、次のようにParameterizedType Nameを使用します.
    TypeElement opElem = processingEnv.getElementUtils().getTypeElement("org.springframework.beans.factory.ObjectProvider");
    TypeElement configElem = processingEnv.getElementUtils().getTypeElement("com.dream.spring.excel.bean.ExcelExportConfig");
    typeBuilder.addField(FieldSpec.builder(ParameterizedTypeName.get(ClassName.get(opElem), TypeName.get(configElem.asType())), "configProvider").addAnnotation(AnnotationSpec.builder(ClassName.get(autowiredElem)).build()).build());

    f)タイプがCollectionサブタイプであるか否かを判定する
    処理中gettypeUtils()には、isSubtypeを含めてサブタイプを判断していますが、判断するときに、ターゲットTypeMirrorがCollectionのサブタイプであるか否かを判断するときは、常にfalseであり、以下のようになります.
    TypeElement collectionType = 
    processingEnv.getElementUtils().getTypeElement("java.util.Collection")
    boolean isCollection = processingEnv.getTypeUtils().isSubtype(targetType, collectionType.asType())

    主な原因は汎用型であり、Collectionは汎用型を有するため、親子関係を判断することができないため、processingEnvを用いることができる.gettypeUtils()のisAssignableメソッドでCollectionかどうかを判断します.isAssignableは、その名の通り、ターゲットタイプが別のタイプに直接付与できるかどうかを判断し、ターゲットタイプがCollection>に直接付与できるかどうかを判断すれば、別の考え方でよい.
    private boolean isCollection(TypeMirror type) {
        TypeElement collectionType = processingEnv.getElementUtils().getTypeElement("java.util.Collection");
        WildcardType wildcardTypeNull = processingEnv.getTypeUtils().getWildcardType(null, null);
        DeclaredType parentType = processingEnv.getTypeUtils().getDeclaredType(collectionType, wildcardTypeNull);
        return processingEnv.getTypeUtils().isAssignable(type, parentType);
    }

    詳細については、以下を参照してください.https://stackoverflow.com/questions/12749517/types-isassignable-and-issubtype-misunderstanding?r=SearchResults
    このライブラリは個人的にはOKで、上記の機能に加えてファイルキャッシュ、国際化サポート、ファイルインポートサポートなどが追加されており、性能面でも実際のプロジェクトで検証されており、その後は長期的にメンテナンスされるはずです.