paint-brush
Von Datenklassen bis Excel: Erstellen eines Java-Mappers mit Anmerkungenvon@sergeidzeboev
2,757 Lesungen
2,757 Lesungen

Von Datenklassen bis Excel: Erstellen eines Java-Mappers mit Anmerkungen

von Sergei Dzeboev18m2023/09/24
Read on Terminal Reader
Read this story w/o Javascript

Zu lang; Lesen

Entwicklung einer benutzerdefinierten Java-Bibliothek zum Generieren von Excel-Berichten unter Nutzung von Anmerkungen für flexible Zellgestaltung, Formelerstellung und dynamische Berichtserstellung. Der Prozess umfasst die Nutzung von Apache POI, die Erstellung spezieller Anmerkungen und die Implementierung eines Dienstes für die Erstellung von Excel-Berichten. Der maßgeschneiderte Ansatz bietet Leichtigkeit, Präzision und Potenzial für zukünftige Anpassungen.
featured image - Von Datenklassen bis Excel: Erstellen eines Java-Mappers mit Anmerkungen
Sergei Dzeboev HackerNoon profile picture
0-item

Trotz der Vielzahl der heute verfügbaren Bibliotheken kann es manchmal schwierig sein, eine zu finden, die die spezifische Funktionalität bietet, die für eine bestimmte Aufgabe benötigt wird. Anstatt Zeit mit der Suche nach der perfekten Bibliothek zu verschwenden, empfehle ich Ihnen, Ihre eigene Implementierung zu erstellen. auch wenn es speziell auf ein Projekt zugeschnitten ist.


Einmal brauchte ich eine Bibliothek, die Datenklassen einfach in ein Excel-Dokument zur Berichterstellung konvertieren konnte. Als ich keine geeignete Bibliothek finden konnte, beschloss ich, eine auf meine spezifischen Bedürfnisse zugeschnittene Funktionalität zu entwickeln.


Mein Ziel war es, eine Bibliothek ähnlich wie Jackson zu entwerfen, die Annotationen verwendet, um eine Liste von Datenklassen entsprechend den Vorgaben der Annotationen in ein Excel-Dokument umzuwandeln.


Ich möchte die von mir erstellte Bibliothek teilen und hoffe, dass sie anderen zugute kommt oder sie dazu inspiriert, ihren eigenen Mapper für ihre individuellen Aufgaben zu erstellen. Lassen Sie uns untersuchen, wie man in Java einen solchen Daten-Mapper entwickelt, um dies zu erreichen:


davon:

 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); } }


Anmerkungen definieren

Lassen Sie uns die wichtigsten Elemente identifizieren, die für die Excel-Zuordnung unerlässlich sind. Im Kern benötigen wir eine Excel-Spalte. Dieser grundlegende Bestandteil des Berichts sollte den Spaltennamen und den entsprechenden Wert in jeder Zeile deutlich hervorheben.


Darüber hinaus müssen wir die Unterstützung für Formelzellen integrieren, damit wir Werte nutzen und die Ergebnisse dynamisch darstellen können. Am Ende der Spalte ist eine abschließende Formel unerlässlich, unabhängig davon, ob sie einen Durchschnitt, eine Summe oder eine andere relevante Metrik für den Endbenutzer darstellt.


Über reine Datenzellen hinaus sollten wir auch Funktionen integrieren, um das Zellen-Styling einfach zu verwalten.


Nachdem die wesentlichen Elemente identifiziert wurden, besteht der nächste Schritt darin, die notwendigen Anmerkungen zu erstellen. In die erste Anmerkung werden Metadaten zum Zellenstil eingebettet. Diese Anmerkung umfasst grundlegende Attribute zusammen mit ihren Standardwerten:

 @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; }


Die für die Berichtserstellung entscheidenden primären Stilelemente werden als Attribute übermittelt. Anschließend kann die Initiierung einer Excel-Spaltenanmerkung erfolgen:

 @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; }


Diese Anmerkung umfasst mögliche Spaltennamen (für die Zuordnung aus Excel) und ein Pflichtfeld – „ position “. Dies bestimmt die Platzierung der Spalte und ist bei Formelberechnungen von entscheidender Bedeutung. Darüber hinaus wird der Stil sowohl der Kopfzeile als auch der Zelle detailliert beschrieben.


Exzellent. Lassen Sie uns nun eine Anmerkung speziell für Excel-Formeln formulieren. Da diese Annotation eine dynamische Formel erwartet, die von der Position der Zeile abhängt, gilt sie ausschließlich für folgende Methoden:

 @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; }


Lassen Sie uns abschließend eine Anmerkung für die Schlussformel einführen, die normalerweise die letzte Zeile in Excel einnimmt und das kumulative Ergebnis für eine Spalte zusammenfasst oder veranschaulicht. Angesichts der Tatsache, dass die Anmerkung der Formel auch ihre Anwendbarkeit ausschließlich auf Methoden vorschreibt:

 @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface ColumnExcelTotalFormula { boolean useValue() default false; int position(); ColumnExcelStyle cellStyle() default @ColumnExcelStyle; }


Erstellen eines Excel-Berichtsdienstes mit Apache POI

Nach der Erstellung aller erforderlichen Anmerkungen ist die Erstellung einer Serviceklasse der nächste Schritt. Die verwendete Kernbibliothek ist Apache POI und eignet sich für die Arbeit mit XLS- und XLSX-Dateien. Diese Klasse verwendet die Attribute von Anmerkungen, um einen Excel-Bericht zu erstellen.


Die primäre Methode akzeptiert eine Liste von Objekten als Eingabe und gibt eine vorbereitete Arbeitsmappe zurück.


Für zusätzliche Flexibilität wird eine überladene Methode eingeführt, um die Angabe sowohl des Dateinamens als auch des Blattnamens für die Berichtserstellung zu ermöglichen:

 <T> Workbook createWorkbookFromObject(List<T> reportObjects) { return createWorkbookFromObject(reportObjects, 0, "Report"); } <T> Workbook createWorkbookFromObject(List<T> reportObjects, int startRowNumber, String sheetName) { ... }


Um mithilfe der Reflektion Informationen über die Klasse zu extrahieren, wird ein beliebiges Element aus dem Array ausgewählt. Nach dem Zugriff auf die Kursdetails kann die erste Zeile erstellt werden. Die Verwendung von Daten aus der Anmerkung ermöglicht die Erstellung von Zellen mit ihren jeweiligen Namen.


Fehlt ein Name, kann alternativ der Name des Klassenfeldes dienen:

 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()); } }


Denken Sie beim Gestalten der Kopfzeile daran, jeder Zelle einen Stil zuzuweisen. Stilparameter können aus der Annotation @ColumnExcelStyle abgeleitet werden:

 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()); }


Der Prozess wendet sich dann der Generierung von Zeilen im Bericht zu, die auf Daten aus dem bereitgestellten Array basieren. Durch Iteration über die Daten werden aufeinanderfolgende Zeilen gebildet:

 for (T report : reportObjects) { Row bodyRow = sheet.createRow(proceedRowNumber); createCellsFromDeclaredExcelColumns(bodyRow, report); proceedRowNumber++; }


Eigenschaftsdeskriptoren werden beschafft, um Getter zu verwenden, anstatt direkten Zugriff auf Felder zu gewähren:

 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()); } }


Mit dem Eigenschaftsdeskriptor wird eine Zelle gebildet:

 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()); } }


Als Nächstes wenden wir uns der Verarbeitung der @ColumnExcelFormula Anmerkungen zu. Das Erstellen von Formeln erweist sich als etwas komplexer als das einfache Extrahieren eines Werts aus einem Feld. Von einer Methode wird erwartet, dass sie eine Formel generiert, die anschließend der Zelle zugewiesen wird.


Die Methode sollte konsistent eine Zeichenfolge zurückgeben und die Zeilennummer als Argument akzeptieren, um eine genaue Datennutzung aus benachbarten Zellen sicherzustellen.


Daher obliegt es dem Handler, zu überprüfen, ob diese Bedingungen erfüllt sind, bevor er die Zelle mit der angegebenen Formel bildet:

 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()); } }


Der letzte Schritt besteht darin, eine Zeile zu erstellen, um die abschließenden Ergebnisse anzuzeigen. Wichtig ist, dass diese Zeile nur einmal generiert werden sollte, unabhängig von der Anzahl der an den Handler weitergeleiteten Objekte. Zu diesem Zweck wird eine Annotation in einer statischen Methode erwartet.


Diese Methode empfängt sowohl die Nummer der Anfangszeile als auch die Nummer der aktuellen Zeile, in der die Zelle instanziiert wird, als Argumente.


Die Angabe der Nummer der ersten Zeile ist von entscheidender Bedeutung, damit die Methode eine Formel entwickeln kann, die das Gesamtergebnis für die gesamte Spalte nutzt:

 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()); } } }


Einen Bericht erstellen

Die Hauptfunktionalität ist jetzt vorhanden und es ist Zeit, sie in Aktion zu sehen. Lassen Sie uns einen einfachen Bericht erstellen, um seine Funktionsweise zu demonstrieren. Dazu erstellen wir eine Klasse „ Sales “ und integrieren alle notwendigen Anmerkungen:

 @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() + ")"; } }


Die Klasse umfasst drei Felder: date , sold und pricePerUnit . Darüber hinaus verfügt es über eine Umsatzformel und eine Abschlusszeile mit den Gesamtsummen: unitsSold und totalSales . Die Felder nutzen die Annotation @ColumnExcel , die die Position und den Namen der Spalte angibt.


Die Annotation @ColumnExcelStyle definiert den Stil sowohl für die Kopfzeile als auch für einzelne Datenzellen:

 @ColumnExcel( position = 0, applyNames = {"Date"}, headerStyle = @ColumnExcelStyle( fontColor = WHITE, cellColor = DARK_BLUE, isCentreAlignment = true), cellStyle = @ColumnExcelStyle( cellColor = GREY_25_PERCENT, cellTypePattern = DATE) )


Wie bereits erwähnt, muss die Methode beim Erstellen einer Formel einen Parameter akzeptieren, der die Zeilennummer angibt. Diese Anforderung wird in der Signatur der Methode deutlich:

 public String sales(int rowNum) { return new CellAddress(rowNum, 1).formatAsString() + "*" + new CellAddress(rowNum, 2).formatAsString(); }


Angesichts der Zeilennummer und der Spaltenindizes ist die Erstellung einer beliebigen spezifischen Formel möglich.


Innerhalb der Klasse sind die für die Abschlussformeln vorgesehenen Methoden statisch und erfordern zwei Parameter: die Nummer der Startzeile und die Nummer der Endzeile:

 public static String unitsSold(int firstRowNum, int lastRowNum) { return "SUM(" + new CellAddress(firstRowNum, 1).formatAsString() + ":" + new CellAddress(lastRowNum - 1, 1).formatAsString() + ")"; }


Nun starten wir die Methode:

 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); } }


Und prüfen Sie den generierten Bericht:

Abschluss

Das Schreiben einer Spezialbibliothek für eine bestimmte Aufgabe erwies sich als unkompliziert. Die erstellte Bibliothek entspricht den Anforderungen und umfasst die geplante Funktionalität. Der Ansatz, der Anmerkungen nutzt, ermöglicht eine schnelle und bequeme Anpassung von Zellstilen, die Änderung von Formeln und die dynamische Berichterstellung aus verschiedenen Datenquellen.


Wenn Sie also das nächste Mal keine geeignete Bibliothek finden, könnte die Entwicklung einer personalisierten Bibliothek von Vorteil sein.


Natürlich war es nicht möglich, jede Codezeile in diesem Artikel vorzustellen; Daher wurden nur die primären Methoden hervorgehoben, die für die Arbeit des Mappers wesentlich sind. Der vollständige Code ist auf meiner GitHub-Seite verfügbar.