paint-brush
Des classes de données à Excel : créer un mappeur Java avec des annotationspar@sergeidzeboev
2,490 lectures
2,490 lectures

Des classes de données à Excel : créer un mappeur Java avec des annotations

par Sergei Dzeboev18m2023/09/24
Read on Terminal Reader

Trop long; Pour lire

Développement d'une bibliothèque Java personnalisée pour générer des rapports Excel, en tirant parti des annotations pour un style de cellule flexible, la création de formules et la création de rapports dynamiques. Le processus implique l'utilisation d'Apache POI, la création d'annotations spécialisées et la mise en œuvre d'un service de génération de rapports Excel. L’approche sur mesure offre facilité, précision et potentiel de personnalisation future.
featured image - Des classes de données à Excel : créer un mappeur Java avec des annotations
Sergei Dzeboev HackerNoon profile picture
0-item

Même avec la multitude de bibliothèques disponibles aujourd'hui, il peut parfois être difficile d'en trouver une qui offre les fonctionnalités spécifiques nécessaires à une tâche particulière. Au lieu de passer du temps à chercher la bibliothèque parfaite, je vous recommande de créer votre propre implémentation ; même s'il est spécialement conçu pour un projet.


Un jour, j'ai eu besoin d'une bibliothèque capable de convertir facilement des classes de données en un document Excel pour la génération de rapports. Ne parvenant pas à trouver une bibliothèque adaptée, j'ai décidé de développer des fonctionnalités adaptées à mes besoins spécifiques.


Mon ambition était de concevoir une bibliothèque semblable à Jackson, qui utiliserait des annotations pour transformer une liste de classes de données en un document Excel comme le dictent les annotations.


Je souhaite partager la bibliothèque que j'ai créée, en espérant qu'elle puisse profiter à d'autres ou les inciter à créer leur propre mappeur pour leurs tâches uniques. Voyons comment développer en Java un tel mappeur de données pour y parvenir :


à partir de ceci :

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


Définir des annotations

Identifions les principaux éléments indispensables à la cartographie Excel. À la base, nous avons besoin d’une colonne Excel. Ce composant fondamental du rapport doit présenter distinctement le nom de la colonne et la valeur correspondante dans chaque ligne.


De plus, nous devons intégrer la prise en charge des cellules de formule, nous permettant d'utiliser les valeurs et de présenter les résultats de manière dynamique. À la fin de la colonne, une formule de conclusion est essentielle, qu'elle représente une moyenne, une somme ou toute autre mesure pertinente pour l'utilisateur final.


Au-delà des simples cellules de données, nous devons également intégrer des fonctionnalités permettant de gérer facilement le style des cellules.


Après avoir identifié les éléments essentiels, l’étape suivante consiste à rédiger les annotations nécessaires. L'annotation initiale intégrera des métadonnées sur le style des cellules. Cette annotation englobera les attributs fondamentaux ainsi que leurs valeurs par défaut :

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


Les principaux éléments de style cruciaux pour la création de rapports sont transmis sous forme d'attributs. Suite à cela, le lancement d'une annotation de colonne Excel peut être entrepris :

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


Cette annotation englobe les noms de colonnes potentiels (pour le mappage à partir d'Excel) et un champ obligatoire - « position ». Cela déterminera l'emplacement de la colonne et jouera un rôle déterminant dans les calculs de formule. De plus, il détaillera le style de l’en-tête et de la cellule.


Excellent. Formulons maintenant une annotation spécifique aux formules Excel. Anticipant une formule dynamique dépendante de la position de la ligne, cette annotation sera exclusive aux méthodes :

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


Enfin, introduisons une annotation pour la formule culminante, qui occupe généralement la dernière ligne d'Excel, résumant ou illustrant le résultat cumulé d'une colonne. Étant donné que l'annotation de la formule impose également son applicabilité uniquement aux méthodes :

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


Création d'un service de reporting Excel avec Apache POI

Après la création de toutes les annotations nécessaires, la création d'une classe de service devient l'étape suivante. La bibliothèque principale utilisée sera Apache POI , efficace pour travailler avec les fichiers .xls et .xlsx. Cette classe utilisera les attributs des annotations pour générer un rapport Excel.


La méthode principale accepte une liste d'objets en entrée et renvoie un classeur préparé.


Pour plus de flexibilité, une méthode surchargée sera introduite pour permettre la spécification du nom du fichier et du nom de la feuille pour la génération du rapport :

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


Pour extraire des informations sur la classe à l'aide de la réflexion, n'importe quel élément du tableau est sélectionné. Après avoir accédé aux détails de la classe, la première ligne peut être établie. L'utilisation des données de l'annotation permet de créer des cellules avec leurs noms respectifs.


Si un nom est absent, le nom du champ classe peut servir d'alternative :

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


Lors de la création de l'en-tête, n'oubliez pas d'attribuer un style à chaque cellule. Les paramètres de style peuvent être dérivés de l'annotation @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()); }


Le processus consiste ensuite à générer des lignes dans le rapport en fonction des données du tableau fourni. En itérant sur les données, des lignes successives sont formées :

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


Les descripteurs de propriété sont fournis pour utiliser des getters au lieu d'accorder un accès direct aux champs :

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


Avec le descripteur de propriété, une cellule est formée :

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


Ensuite, tournons notre attention vers le traitement des annotations @ColumnExcelFormula . L'élaboration de formules s'avère légèrement plus complexe que la simple extraction d'une valeur d'un champ. Une méthode est censée générer une formule, qui est ensuite attribuée à la cellule.


La méthode doit systématiquement renvoyer une chaîne et accepter le numéro de ligne comme argument, garantissant ainsi une utilisation précise des données des cellules adjacentes.


Ainsi, il incombe au gestionnaire de vérifier que ces conditions sont remplies avant de former la cellule avec la formule spécifiée :

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


La dernière étape consiste à créer une ligne pour afficher les résultats finaux. Il est important de noter que cette ligne ne doit être générée qu'une seule fois, quel que soit le nombre d'objets relayés vers le gestionnaire. Une annotation dans une méthode statique est prévue à cet effet.


Cette méthode reçoit à la fois le numéro de la ligne initiale et la ligne actuelle où la cellule sera instanciée comme arguments.


La fourniture du numéro de la ligne initiale est vitale, permettant à la méthode de concevoir une formule qui exploite le résultat agrégé pour l'ensemble de la colonne :

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


Faire un rapport

La fonctionnalité principale est désormais en place et il est temps de la voir en action. Construisons un rapport simple pour démontrer son fonctionnement. Pour cela, créons une classe ' Sales ' et intégrons toutes les annotations nécessaires :

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


La classe comprend trois champs : date , sold et pricePerUnit . De plus, il comporte une formule de vente et une ligne de conclusion avec les totaux : unitsSold et totalSales . Les champs utilisent l'annotation @ColumnExcel , indiquant la position et le nom de la colonne.


L'annotation @ColumnExcelStyle définit le style de l'en-tête et des cellules de données individuelles :

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


Comme indiqué précédemment, lors de la création d'une formule, la méthode doit accepter un paramètre indiquant le numéro de ligne. Cette exigence transparaît dans la signature de la méthode :

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


Compte tenu du numéro de ligne et des indices de colonne, l’élaboration de toute formule spécifique devient réalisable.


Au sein de la classe, les méthodes destinées aux formules de conclusion sont statiques et nécessitent deux paramètres : le numéro de la ligne de départ et le numéro de la ligne de fin :

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


Maintenant, lançons la méthode :

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


Et examinez le rapport généré :

Conclusion

Écrire une bibliothèque spécialisée pour une tâche spécifique s’est avéré simple. La bibliothèque conçue répond aux exigences et inclut les fonctionnalités prévues. L'approche utilisant les annotations facilite une personnalisation rapide et pratique des styles de cellules, la modification des formules et la création de rapports dynamiques à partir de diverses sources de données.


Par conséquent, la prochaine fois qu’une bibliothèque appropriée s’avère insaisissable, il pourrait être bénéfique d’envisager le développement d’une bibliothèque personnalisée.


Naturellement, il n’était pas possible de présenter chaque ligne de code dans cet article ; ainsi, seules les principales méthodes essentielles au fonctionnement du mappeur ont été mises en évidence. Le code complet est disponible sur ma page GitHub .