paint-brush
От классов данных к Excel: создание Java Mapper с аннотациямик@sergeidzeboev
2,784 чтения
2,784 чтения

От классов данных к Excel: создание Java Mapper с аннотациями

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

Слишком долго; Читать

Разработана специальная библиотека Java для создания отчетов Excel, использующая аннотации для гибкого оформления ячеек, создания формул и создания динамических отчетов. Этот процесс включает в себя использование Apache POI, создание специализированных аннотаций и внедрение службы для создания отчетов Excel. Индивидуальный подход обеспечивает простоту, точность и потенциал для будущей индивидуальной настройки.
featured image - От классов данных к Excel: создание Java Mapper с аннотациями
Sergei Dzeboev HackerNoon profile picture
0-item

Даже несмотря на множество доступных сегодня библиотек, иногда бывает сложно найти ту, которая предлагает конкретную функциональность, необходимую для конкретной задачи. Вместо того, чтобы тратить время на поиск идеальной библиотеки, я рекомендую создать собственную реализацию; даже если он заточен специально под один проект.


Однажды мне понадобилась библиотека, которая могла бы легко конвертировать классы данных в документ Excel для создания отчетов. Когда я не смог найти подходящую библиотеку, я решил разработать функциональность, адаптированную к моим конкретным потребностям.


Моей целью было разработать библиотеку, подобную Джексону, которая использовала бы аннотации для преобразования списка классов данных в документ Excel в соответствии с аннотациями.


Я хочу поделиться созданной мной библиотекой, надеясь, что она может принести пользу другим или вдохновить их на создание собственного картографа для своих уникальных задач. Давайте рассмотрим, как разработать на 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; }


Создание службы отчетов Excel с помощью Apache POI

После создания всех необходимых аннотаций следующим шагом становится создание класса обслуживания. В качестве основной библиотеки будет использоваться 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()); } }


Последний шаг включает в себя создание строки для отображения итоговых результатов. Важно отметить, что эта строка должна быть сгенерирована только один раз, независимо от количества объектов, передаваемых обработчику. Для этой цели предполагается аннотация в статическом методе.


Этот метод получает в качестве аргументов номер начальной строки и текущей строки, в которой будет создан экземпляр ячейки.


Предоставление номера начальной строки имеет жизненно важное значение, позволяя методу разработать формулу, которая использует совокупный результат для всего столбца:

 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 . Кроме того, в нем есть формула продаж и итоговая строка с итогами: 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(); }


Учитывая номер строки и индексы столбцов, создание любой конкретной формулы становится возможным.


Внутри класса методы, предназначенные для выводов итоговых формул, являются статическими и требуют двух параметров: номера начальной строки и номера конечной строки:

 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 .