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にあり、興味のある方は説明とソースコードを見て、現在使用について説明します.
上記の例では、既存のインタフェースに基づいて、次の2つの注釈が追加されています. ExcelSupport:注釈プロセッサのエントリであり、そのvalue値は自動生成されたExcelエクスポートインタフェースのController名を示し、デフォルトでは注釈されたControllerと同じクラスのパッケージの下でExcelControllerが生成され、複数の注釈されたControllerは同じController名を指定できます. ExcelExport:Excelによって生成されるデータソースメソッドを指定し、そのメソッドに基づいてExcelのエクスポートインタフェースを生成します.注記のvalue Excelエクスポートインタフェースのurlパスを指定し、annotationsは生成インタフェースに追加の注記を追加します.生成されたインタフェースメソッドのパラメータは、注記を含む一貫性を保っていますが、@ParamIgnoreで識別された注記は無視されます.Excelエクスポートインタフェースを生成すると、@ExcelExportによって注記されるメソッドが実際に呼び出されます.
上記注釈は、インタフェースの生成を導出するために用いられ、具体的なExcel導出データとフォーマットの定義は、上記の例に示すように、そのExcelの各行のデータはTestクラスに基づいて生成され、Testクラスの注釈の例は以下の通りである. 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注記を解析します.
上記の現在自動生成されているコードの例を次に示します.
「/api/excel/test」インタフェースを直接呼び出すことで、Excelファイルを直接エクスポートできます.これにより、Excelエクスポートコードを繰り返し書く必要はありません.
3.Annotation Processor&Poetの一般的なピット
具体的な実装は言うまでもなく、興味のある方はプロジェクトの「excel-processor」の実装を見て、ここでは主に過程でよくある問題を説明します.
a)TypeElementの使用
TypeElementは、処理で最も一般的に使用されるクラスであり、クラスとインタフェースタイプの要素を表し、対応するメソッド要素はExecutableElementである.実際の処理の過程で、得られたElementは実際の状況に応じて対応するTypeElementまたはExecutableElementに強く転換する必要があります.例えば、ExcelSupportが注釈するクラスのフルネームを取得したい場合:
また、注釈処理の過程で、ある種類が工事中に存在するかどうかを判断する必要があり、以下の方法を使用することができる.
b) TypeMirror & DeclaredType
DeclaredTypeはTypeMirrorのサブタイプで、すべてのクラスとインタフェースのタイプを表しています.多くの場合、強い回転が必要です.たとえば、メソッドの戻りタイプでSheetWrapperラベル付きのサブ要素を取得する必要があります.
c)注記にClassを使う
注釈にはいくつかのタイプを定義する必要がある場合が多く、Classクラスが使用されますが、注釈処理では、classの取得を呼び出すと例外が放出されます(コンパイル中であることも理解できます).したがって、注釈処理中にclassに遭遇した場合、その例外をキャプチャし、例外プロンプトに基づいてclassに対応するクラスのフルネームを取得する必要があります.
d)注記プロセッサにおける参照依存しない注記処理
注記プロセッサで対応する要素の注記を取得するには、TypeElementのgetAnnotation(Class annotationType)メソッド。ただし、依存が導入されていない(すなわち、対応するClassがない)場合は、TypeElementのgetAnnotationMirrorsを巡る方法が必要です。)を使用します.
e)Poetにおけるメンバー変数の汎用的なサポート
Poetを使用して、汎用メンバー変数を定義するには、次のようにParameterizedType Nameを使用します.
f)タイプがCollectionサブタイプであるか否かを判定する
処理中gettypeUtils()には、isSubtypeを含めてサブタイプを判断していますが、判断するときに、ターゲットTypeMirrorがCollectionのサブタイプであるか否かを判断するときは、常にfalseであり、以下のようになります.
主な原因は汎用型であり、Collectionは汎用型を有するため、親子関係を判断することができないため、processingEnvを用いることができる.gettypeUtils()のisAssignableメソッドでCollectionかどうかを判断します.isAssignableは、その名の通り、ターゲットタイプが別のタイプに直接付与できるかどうかを判断し、ターゲットタイプがCollection>に直接付与できるかどうかを判断すれば、別の考え方でよい.
詳細については、以下を参照してください.https://stackoverflow.com/questions/12749517/types-isassignable-and-issubtype-misunderstanding?r=SearchResults
このライブラリは個人的にはOKで、上記の機能に加えてファイルキャッシュ、国際化サポート、ファイルインポートサポートなどが追加されており、性能面でも実際のプロジェクトで検証されており、その後は長期的にメンテナンスされるはずです.
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つの注釈が追加されています.
上記注釈は、インタフェースの生成を導出するために用いられ、具体的な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;
... ...
}
ただし、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で、上記の機能に加えてファイルキャッシュ、国際化サポート、ファイルインポートサポートなどが追加されており、性能面でも実際のプロジェクトで検証されており、その後は長期的にメンテナンスされるはずです.