現在は多数のライブラリが利用可能ですが、特定のタスクに必要な特定の機能を提供するライブラリを見つけるのが難しい場合があります。完璧なライブラリを探すのに時間を費やす代わりに、独自の実装を作成することをお勧めします。たとえそれが 1 つのプロジェクト専用にカスタマイズされたものであっても。
かつて、レポート生成のためにデータ クラスを Excel ドキュメントに簡単に変換できるライブラリが必要であることに気づきました。適切なライブラリが見つからなかったとき、私は自分の特定のニーズに合わせた機能を開発することにしました。
私の野望は、注釈を使用して、注釈の指示に従ってデータ クラスのリストを Excel ドキュメントに変換する、Jackson に似たライブラリを設計することでした。
私が作成したライブラリを共有したいと思います。それが他の人に利益をもたらしたり、独自のタスク用に独自のマッパーを作成するきっかけになったりすることを願っています。これを実現するために、このようなデータ マッパーを Java で開発する方法を見てみましょう。
これから:
void demoReport() { var excelMapper = new ExcelMapperImpl(); var fileName = "demo-out-" + LocalTime.now() + ".xlsx"; List<Demo> demos = generateDemos(); try (Workbook workbook = excelMapper.createWorkbookFromObject(demos); var fileOutputStream = new FileOutputStream(fileName)) { workbook.write(fileOutputStream); } }
Excel マッピングに不可欠な主な要素を特定してみましょう。その核心として Excel 列が必要です。レポートのこの基本コンポーネントでは、列名と各行の対応する値を明確に表示する必要があります。
さらに、値を利用して結果を動的に表示できるように、数式セルのサポートを組み込む必要があります。列の最後には、平均、合計、またはエンドユーザーに関連するその他の指標を表す結論の式が不可欠です。
単なるデータ セルを超えて、セルのスタイルを簡単に管理するための機能も統合する必要があります。
重要な要素を特定したら、次のステップは必要な注釈を作成することです。最初の注釈には、セルのスタイルに関するメタデータが埋め込まれます。このアノテーションには、基本的な属性とそのデフォルト値が含まれます。
@Retention(RetentionPolicy.RUNTIME) public @interface ColumnExcelStyle { ExcelColumnDataFormat cellTypePattern() default ExcelColumnDataFormat.NONE; IndexedColors cellColor() default IndexedColors.AUTOMATIC; boolean isWrapText() default false; boolean isCentreAlignment() default false; boolean isFramed() default true; ExcelColumnFont fontName() default ExcelColumnFont.DEFAULT; short fontSize() default -1; boolean isFontBold() default false; ExcelColumnCellTextColor fontColor() default ExcelColumnCellTextColor.AUTOMATIC; }
レポート作成に重要な主要なスタイル要素は属性として伝えられます。これに続いて、Excel 列の注釈を開始できます。
@Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface ColumnExcel { String[] applyNames() default {}; int position(); ColumnExcelStyle headerStyle() default @ColumnExcelStyle( fontColor = ExcelColumnCellTextColor.BLACK, isCentreAlignment = true, isFontBold = true, fontSize = 14, isWrapText = true); ColumnExcelStyle cellStyle() default @ColumnExcelStyle; }
この注釈には、潜在的な列名 (Excel からのマッピング用) と必須フィールド - ' position
' が含まれます。これは列の配置を決定し、数式の計算に役立ちます。さらに、ヘッダーとセルの両方のスタイルについても詳しく説明します。
素晴らしい。ここで、Excel の数式に固有の注釈を作成してみましょう。行の位置に応じて動的な数式が予想されるため、このアノテーションはメソッド専用になります。
@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface ColumnExcelFormula { String name() default ""; int position(); ColumnExcelStyle headerStyle() default @ColumnExcelStyle( fontColor = ExcelColumnCellTextColor.BLACK, isCentreAlignment = true, isFontBold = true, fontSize = 14, isWrapText = true); ColumnExcelStyle cellStyle() default @ColumnExcelStyle; }
最後に、最終的な数式の注釈を導入しましょう。これは通常、Excel の最後の行を占め、列の累積結果を要約または図示します。数式の注釈によって、メソッドのみに適用できることが義務付けられているとすると、次のようになります。
@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface ColumnExcelTotalFormula { boolean useValue() default false; int position(); ColumnExcelStyle cellStyle() default @ColumnExcelStyle; }
必要なアノテーションをすべて作成したら、次のステップはサービス クラスの作成です。使用されるコア ライブラリはApache POIで、.xls および .xlsx ファイルの操作に効果的です。このクラスは、注釈の属性を使用して Excel レポートを生成します。
主なメソッドは、オブジェクトのリストを入力として受け取り、準備されたワークブックを返します。
柔軟性を高めるために、レポート生成時にファイル名とシート名の両方を指定できるようにするオーバーロード メソッドが導入されます。
<T> Workbook createWorkbookFromObject(List<T> reportObjects) { return createWorkbookFromObject(reportObjects, 0, "Report"); } <T> Workbook createWorkbookFromObject(List<T> reportObjects, int startRowNumber, String sheetName) { ... }
リフレクションを使用してクラスに関する情報を抽出するには、配列の任意の要素が選択されます。クラスの詳細にアクセスした後、最初の行を確立できます。注釈のデータを利用すると、それぞれの名前を持つセルを作成できます。
名前が存在しない場合は、クラス フィールドの名前が代替として機能します。
private <T> void createHeaderFromDeclaredExcelColumns(Row row, Class<T> clazz, PropertyDescriptor propertyDescriptor) { try { Field field = clazz.getDeclaredField(propertyDescriptor.getName()); ColumnExcel columnExcel = field.getDeclaredAnnotation(ColumnExcel.class); if (nonNull(columnExcel)) { String headerName = columnExcel.applyNames().length > 0 ? columnExcel.applyNames()[0] : field.getName(); createHeader(row, columnExcel.position(), headerName, columnExcel.headerStyle()); } } catch (NoSuchFieldException e) { log.debug(e.getLocalizedMessage()); } }
ヘッダーを作成するときは、すべてのセルにスタイルを割り当てることを忘れないでください。スタイル パラメーターは@ColumnExcelStyle
アノテーションから派生できます。
private void createHeader(Row row, int position, String name, ColumnExcelStyle columnExcelStyle) { Cell cell = row.createCell(position); cell.setCellValue(name); setCellFormatting(cell, columnExcelStyle); row.getSheet().autoSizeColumn(cell.getColumnIndex()); }
次に、プロセスは、提供された配列のデータに基づいてレポート内の行を生成します。データを反復処理することにより、連続する行が形成されます。
for (T report : reportObjects) { Row bodyRow = sheet.createRow(proceedRowNumber); createCellsFromDeclaredExcelColumns(bodyRow, report); proceedRowNumber++; }
プロパティ記述子は、フィールドへの直接アクセスを許可するのではなく、ゲッターを利用するために取得されます。
private <T> void createCellsFromDeclaredExcelColumns(Row row, T tObject) { try { PropertyDescriptor[] propertyDescriptors = Introspector.getBeanInfo(tObject.getClass()).getPropertyDescriptors(); for (var propertyDescriptor : propertyDescriptors) { createCellFromDeclaredExcelColumns(row, tObject, propertyDescriptor); } } catch (IntrospectionException ex) { log.debug(ex.getLocalizedMessage()); } }
プロパティ記述子を使用してセルが形成されます。
private <T> void createCellFromDeclaredExcelColumns(Row row, T tObject, PropertyDescriptor propertyDescriptor) { try { Field field = tObject.getClass().getDeclaredField(propertyDescriptor.getName()); Method readMethod = propertyDescriptor.getReadMethod(); ColumnExcel columnExcel = field.getDeclaredAnnotation(ColumnExcel.class); if (nonNull(columnExcel)) { Class<?> returnType = readMethod.getReturnType(); Cell cell = row.createCell(columnExcel.position()); Object invokeResult = readMethod.invoke(tObject); if (nonNull(invokeResult)) { defineAndAssignCellValue(returnType, cell, invokeResult, readMethod); } setCellFormatting(cell, columnExcel.cellStyle()); } } catch (NoSuchFieldException | InvocationTargetException | IllegalAccessException e) { log.debug(e.getLocalizedMessage()); } }
次に、 @ColumnExcelFormula
アノテーションの処理に注目してみましょう。数式の作成は、単にフィールドから値を抽出するよりも少し複雑であることがわかります。メソッドは数式を生成し、その後セルに割り当てられることが期待されます。
このメソッドは一貫して文字列を返し、引数として行番号を受け入れ、隣接するセルからの正確なデータ使用を保証する必要があります。
したがって、指定された数式でセルを形成する前に、これらの条件が満たされていることを確認するのはハンドラーの役割になります。
private <T> void createCellFromDeclaredExcelFormula(Row row, T tObject, Method readMethod) throws IllegalAccessException, InvocationTargetException { ColumnExcelFormula columnExcelFormula = readMethod.getDeclaredAnnotation(ColumnExcelFormula.class); if (columnExcelFormula != null) { Class<?> returnType = readMethod.getReturnType(); Cell cell = row.createCell(columnExcelFormula.position()); if (returnType.isAssignableFrom(String.class)) { cell.setCellFormula((String) readMethod.invoke(tObject, row.getRowNum())); } else { log.debug(" Return type for the method: " + readMethod.getName() + " with @ColumnExcelFormula annotation has to be String " + "and now it's: " + returnType.getName() + " method is ignored for the reason"); } setCellFormatting(cell, columnExcelFormula.cellStyle()); } }
最後のステップでは、最終結果を表示する行を作成します。重要なのは、ハンドラーに中継されるオブジェクトの数に関係なく、この行は 1 回だけ生成される必要があることです。この目的のために、静的メソッドのアノテーションが想定されています。
このメソッドは、セルがインスタンス化される最初の行と現在の行の両方の番号を引数として受け取ります。
最初の行の番号を指定することは非常に重要であり、列全体の集計結果を利用する式をメソッドで考案できるようになります。
private <T> void createTotalFormula(Class<T> tClazz, Row row, int firstRowNum) { Method[] methods = tClazz.getDeclaredMethods(); for (Method method : methods) { ColumnExcelTotalFormula columnExcelTotalFormula = method.getAnnotation(ColumnExcelTotalFormula.class); if (columnExcelTotalFormula != null && method.getReturnType().isAssignableFrom(String.class) && method.getParameters().length == 2 && Modifier.isStatic(method.getModifiers()) && !Modifier.isPrivate(method.getModifiers()) ) { String cellFormula = (String) method.invoke(tClazz, firstRowNum, row.getRowNum()); Cell cell = row.createCell(columnExcelTotalFormula.position()); cell.setCellFormula(cellFormula); if (columnExcelTotalFormula.useValue()) { cell = applyFormulasValue(cell); } setCellFormatting(cell, columnExcelTotalFormula.cellStyle()); } } }
主な機能が整ったので、実際に動作するところを見てみましょう。簡単なレポートを作成して、その操作を示してみましょう。このために、「 Sales
」クラスを作成し、必要なアノテーションをすべて組み込みましょう。
@Data @Accessors(chain = true) public class Sales { @ColumnExcel( position = 0, applyNames = {"Date"}, headerStyle = @ColumnExcelStyle( fontColor = WHITE, cellColor = DARK_BLUE, isCentreAlignment = true), cellStyle = @ColumnExcelStyle( cellColor = GREY_25_PERCENT, cellTypePattern = DATE)) private LocalDate date; @ColumnExcel( position = 1, applyNames = {"Sold"}, headerStyle = @ColumnExcelStyle( fontColor = WHITE, cellColor = DARK_BLUE, isCentreAlignment = true), cellStyle = @ColumnExcelStyle( cellColor = GREY_25_PERCENT)) private Integer sold; @ColumnExcel( position = 2, applyNames = {"Price Per Unit (USD)"}, headerStyle = @ColumnExcelStyle( fontColor = WHITE, cellColor = DARK_BLUE, isCentreAlignment = true), cellStyle = @ColumnExcelStyle( cellColor = GREY_25_PERCENT, cellTypePattern = USD)) private Double pricePerUnit; @ColumnExcelFormula( position = 3, name = "Total Sales (USD)", headerStyle = @ColumnExcelStyle( fontColor = WHITE, cellColor = DARK_BLUE, isCentreAlignment = true), cellStyle = @ColumnExcelStyle( cellColor = GREY_25_PERCENT, cellTypePattern = USD)) public String sales(int rowNum) { return new CellAddress(rowNum, 1).formatAsString() + "*" + new CellAddress(rowNum, 2).formatAsString(); } @ColumnExcelTotalFormula( position = 0, cellStyle = @ColumnExcelStyle( cellColor = LIGHT_BLUE)) public static String total(int firstRowNum, int lastRowNum) { return "CONCATENATE(\"Total\")"; } @ColumnExcelTotalFormula( position = 1, cellStyle = @ColumnExcelStyle( cellColor = LIGHT_BLUE)) public static String unitsSold(int firstRowNum, int lastRowNum) { return "SUM(" + new CellAddress(firstRowNum, 1).formatAsString() + ":" + new CellAddress(lastRowNum - 1, 1).formatAsString() + ")"; } @ColumnExcelTotalFormula( position = 3, cellStyle = @ColumnExcelStyle( isCentreAlignment = false, cellColor = LIGHT_BLUE, cellTypePattern = USD)) public static String totalSales(int firstRowNum, int lastRowNum) { return "SUM(" + new CellAddress(firstRowNum, 3).formatAsString() + ":" + new CellAddress(lastRowNum - 1, 3).formatAsString() + ")"; } }
このクラスは、 date
、 sold
、およびpricePerUnit
3 つのフィールドで構成されます。さらに、売上の数式と、合計を含む最終行 ( unitsSold
およびtotalSales
が含まれています。フィールドでは@ColumnExcel
注釈を使用して、列の位置と名前を示します。
@ColumnExcelStyle
注釈は、ヘッダーと個々のデータ セルの両方のスタイルを定義します。
@ColumnExcel( position = 0, applyNames = {"Date"}, headerStyle = @ColumnExcelStyle( fontColor = WHITE, cellColor = DARK_BLUE, isCentreAlignment = true), cellStyle = @ColumnExcelStyle( cellColor = GREY_25_PERCENT, cellTypePattern = DATE) )
前述したように、数式を作成するとき、メソッドは行番号を示すパラメーターを受け入れる必要があります。この要件は、メソッドのシグネチャで明らかです。
public String sales(int rowNum) { return new CellAddress(rowNum, 1).formatAsString() + "*" + new CellAddress(rowNum, 2).formatAsString(); }
行番号と列インデックスが与えられると、特定の数式を作成することが可能になります。
クラス内では、結論の式を対象としたメソッドは静的であり、開始行の番号と終了行の番号という 2 つのパラメータが必要です。
public static String unitsSold(int firstRowNum, int lastRowNum) { return "SUM(" + new CellAddress(firstRowNum, 1).formatAsString() + ":" + new CellAddress(lastRowNum - 1, 1).formatAsString() + ")"; }
次に、メソッドを起動しましょう。
void salesReport() { var excelMapper = new ExcelMapperImpl(); var fileName = "sales-out-" + LocalTime.now() + ".xlsx"; List<Sales> sales = List.of( new Sales().setDate(LocalDate.of(2023, 1, 1)) .setSold(50) .setPricePerUnit(10d), new Sales().setDate(LocalDate.of(2023, 1, 2)) .setSold(40) .setPricePerUnit(11d), new Sales().setDate(LocalDate.of(2023, 1, 3)) .setSold(55) .setPricePerUnit(9d); try (Workbook workbook = excelMapper.createWorkbookFromObject(sales); var fileOutputStream = new FileOutputStream(fileName)) { workbook.write(fileOutputStream); } }
そして、生成されたレポートを調べます。
特定のタスクに特化したライブラリを作成するのは簡単であることがわかりました。作成されたライブラリは要件を満たしており、計画された機能が含まれています。注釈を利用したアプローチにより、セル スタイルのカスタマイズ、数式の変更、さまざまなデータ ソースからの動的なレポート作成が迅速かつ便利になります。
したがって、次回適切なライブラリが見つからない場合は、パーソナライズされたライブラリの開発を検討すると有益になる可能性があります。
当然のことながら、この記事でコードのすべての行を紹介するのは不可能です。したがって、マッパーの操作に不可欠な主要なメソッドのみが強調表示されています。完全なコードは私のGitHub ページで入手できます。