Mesmo com a grande variedade de bibliotecas disponíveis atualmente, às vezes pode ser um desafio encontrar uma que ofereça a funcionalidade específica necessária para uma tarefa específica. Em vez de perder tempo procurando a biblioteca perfeita, recomendo criar sua própria implementação; mesmo que seja adaptado especificamente para um projeto.
Certa vez, precisei de uma biblioteca que pudesse converter facilmente classes de dados em um documento Excel para geração de relatórios. Quando não consegui localizar uma biblioteca adequada, decidi desenvolver funcionalidades adaptadas às minhas necessidades específicas.
Minha ambição era projetar uma biblioteca semelhante à de Jackson, que usaria anotações para transformar uma lista de classes de dados em um documento Excel conforme ditado pelas anotações.
Quero compartilhar a biblioteca que criei, esperando que possa beneficiar outras pessoas ou inspirá-las a criar seu próprio mapeador para suas tarefas específicas. Vamos explorar como desenvolver em Java esse mapeador de dados para conseguir isso:
disto:
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); } }
Vamos identificar os principais elementos essenciais para o mapeamento do Excel. Basicamente, exigimos uma coluna do Excel. Este componente fundamental do relatório deve mostrar claramente o nome da coluna e o valor correspondente em cada linha.
Além disso, devemos incorporar suporte para células de fórmula, permitindo-nos utilizar valores e apresentar os resultados de forma dinâmica. Ao final da coluna, é fundamental uma fórmula final, seja ela uma média, uma soma ou qualquer outra métrica pertinente para o usuário final.
Além de meras células de dados, devemos também integrar recursos para gerenciar facilmente o estilo das células.
Após identificar os elementos essenciais, o próximo passo é elaborar as anotações necessárias. A anotação inicial incorporará metadados sobre o estilo das células. Esta anotação abrangerá atributos fundamentais juntamente com seus valores padrão:
@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; }
Os principais elementos de estilo cruciais para a criação de relatórios são transmitidos como atributos. Em seguida, o início de uma anotação de coluna do Excel pode ser realizado:
@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; }
Esta anotação abrange nomes de colunas potenciais (para mapeamento do Excel) e um campo obrigatório - ' position
'. Isso determinará o posicionamento da coluna e será fundamental nos cálculos das fórmulas. Além disso, detalhará o estilo do cabeçalho e da célula.
Excelente. Agora, vamos formular uma anotação específica para fórmulas do Excel. Antecipando uma fórmula dinâmica dependente da posição da linha, esta anotação será exclusiva dos métodos:
@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; }
Por último, vamos introduzir uma anotação para a fórmula culminante, que normalmente ocupa a linha final do Excel, resumindo ou ilustrando o resultado cumulativo de uma coluna. Dado que a anotação da fórmula também determina a sua aplicabilidade apenas aos métodos:
@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface ColumnExcelTotalFormula { boolean useValue() default false; int position(); ColumnExcelStyle cellStyle() default @ColumnExcelStyle; }
Após a criação de todas as anotações necessárias, a elaboração de uma classe de serviço torna-se o próximo passo. A biblioteca principal utilizada será Apache POI , eficaz para trabalhar com arquivos .xls e .xlsx. Esta classe utilizará atributos de anotações para gerar um relatório Excel.
O método primário aceita uma lista de objetos como entrada e retorna uma pasta de trabalho preparada.
Para maior flexibilidade, um método sobrecarregado será introduzido para permitir a especificação do nome do arquivo e do nome da planilha para geração de relatórios:
<T> Workbook createWorkbookFromObject(List<T> reportObjects) { return createWorkbookFromObject(reportObjects, 0, "Report"); } <T> Workbook createWorkbookFromObject(List<T> reportObjects, int startRowNumber, String sheetName) { ... }
Para extrair informações sobre a classe usando reflexão, qualquer elemento do array é selecionado. Após acessar os detalhes da turma, a primeira linha pode ser estabelecida. A utilização dos dados da anotação permite a criação de células com seus respectivos nomes.
Se um nome estiver ausente, o nome do campo de classe pode servir como alternativa:
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()); } }
Ao criar o cabeçalho, lembre-se de atribuir um estilo a cada célula. Os parâmetros de estilo podem ser derivados da anotação @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()); }
O processo então passa a gerar linhas no relatório com base nos dados da matriz fornecida. Ao iterar os dados, linhas sucessivas são formadas:
for (T report : reportObjects) { Row bodyRow = sheet.createRow(proceedRowNumber); createCellsFromDeclaredExcelColumns(bodyRow, report); proceedRowNumber++; }
Os descritores de propriedades são adquiridos para utilizar getters em vez de conceder acesso direto aos campos:
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()); } }
Com o descritor de propriedade, uma célula é formada:
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()); } }
A seguir, vamos voltar nossa atenção para o processamento das anotações @ColumnExcelFormula
. Criar fórmulas é um pouco mais complexo do que simplesmente extrair um valor de um campo. Espera-se que um método gere uma fórmula, que é posteriormente atribuída à célula.
O método deve retornar consistentemente uma string e aceitar o número da linha como argumento, garantindo o uso preciso dos dados das células adjacentes.
Assim, cabe ao manipulador verificar se essas condições foram atendidas antes de formar a célula com a fórmula especificada:
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()); } }
A etapa final envolve a criação de uma linha para exibir os resultados finais. É importante ressaltar que esta linha deve ser gerada apenas uma vez, independentemente do número de objetos retransmitidos ao manipulador. Uma anotação em um método estático é prevista para esse propósito.
Este método recebe como argumentos o número da linha inicial e a linha atual onde a célula será instanciada.
O fornecimento do número da linha inicial é vital, permitindo que o método crie uma fórmula que aproveite o resultado agregado para toda a coluna:
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()); } } }
A funcionalidade principal já está implementada e é hora de vê-la em ação. Vamos construir um relatório simples para demonstrar seu funcionamento. Para isso, vamos criar uma classe ‘ Sales
’ e incorporar todas as anotações necessárias:
@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() + ")"; } }
A classe compreende três campos: date
, sold
e pricePerUnit
. Além disso, possui uma fórmula de vendas e uma linha final com os totais: unitsSold
e totalSales
. Os campos utilizam a anotação @ColumnExcel
, denotando a posição e o nome da coluna.
A anotação @ColumnExcelStyle
define o estilo do cabeçalho e das células de dados individuais:
@ColumnExcel( position = 0, applyNames = {"Date"}, headerStyle = @ColumnExcelStyle( fontColor = WHITE, cellColor = DARK_BLUE, isCentreAlignment = true), cellStyle = @ColumnExcelStyle( cellColor = GREY_25_PERCENT, cellTypePattern = DATE) )
Conforme discutido anteriormente, ao criar uma fórmula, o método deve aceitar um parâmetro que indique o número da linha. Este requisito é evidente na assinatura do método:
public String sales(int rowNum) { return new CellAddress(rowNum, 1).formatAsString() + "*" + new CellAddress(rowNum, 2).formatAsString(); }
Dados o número da linha e os índices das colunas, a elaboração de qualquer fórmula específica torna-se viável.
Dentro da classe, os métodos destinados às fórmulas finais são estáticos e requerem dois parâmetros: o número da linha inicial e o número da linha final:
public static String unitsSold(int firstRowNum, int lastRowNum) { return "SUM(" + new CellAddress(firstRowNum, 1).formatAsString() + ":" + new CellAddress(lastRowNum - 1, 1).formatAsString() + ")"; }
Agora, vamos iniciar o método:
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); } }
E examine o relatório gerado:
Escrever uma biblioteca especializada para uma tarefa específica provou ser simples. A biblioteca elaborada atende aos requisitos e inclui as funcionalidades planejadas. A abordagem que utiliza anotações facilita a personalização rápida e conveniente de estilos de células, modificação de fórmulas e criação dinâmica de relatórios a partir de diversas fontes de dados.
Portanto, da próxima vez que uma biblioteca adequada for difícil, considerar o desenvolvimento de uma biblioteca personalizada pode ser benéfico.
Naturalmente, não foi viável apresentar todas as linhas de código deste artigo; assim, foram destacados apenas os métodos primários essenciais à operação do mapeador. O código completo está disponível na minha página do GitHub .