paint-brush
데이터 클래스에서 Excel까지: 주석을 사용하여 Java 매퍼 구축~에 의해@sergeidzeboev
2,784 판독값
2,784 판독값

데이터 클래스에서 Excel까지: 주석을 사용하여 Java 매퍼 구축

~에 의해 Sergei Dzeboev18m2023/09/24
Read on Terminal Reader
Read this story w/o Javascript

너무 오래; 읽다

유연한 셀 스타일 지정, 수식 생성 및 동적 보고서 생성을 위한 주석을 활용하여 Excel 보고서를 생성하는 사용자 정의 Java 라이브러리를 개발했습니다. 이 프로세스에는 Apache POI 활용, 특수 주석 생성, Excel 보고서 생성을 위한 서비스 구현이 포함됩니다. 맞춤형 접근 방식은 용이성, 정확성 및 향후 맞춤화 가능성을 제공합니다.
featured image - 데이터 클래스에서 Excel까지: 주석을 사용하여 Java 매퍼 구축
Sergei Dzeboev HackerNoon profile picture
0-item

오늘날 사용 가능한 수많은 라이브러리에도 불구하고 때로는 특정 작업에 필요한 특정 기능을 제공하는 라이브러리를 찾는 것이 어려울 수 있습니다. 완벽한 라이브러리를 검색하는 데 시간을 소비하는 대신 자신만의 구현을 만드는 것이 좋습니다. 심지어 하나의 프로젝트를 위해 특별히 맞춤 제작된 경우에도 마찬가지입니다.


한번은 보고서 생성을 위해 데이터 클래스를 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 열이 필요합니다. 보고서의 기본 구성 요소는 열 이름과 각 행의 해당 값을 명확하게 표시해야 합니다.


또한, 값을 활용하고 결과를 동적으로 표시할 수 있도록 수식 셀에 대한 지원을 통합해야 합니다. 열 끝에는 최종 사용자에 대한 평균, 합계 또는 기타 관련 측정항목을 나타내는지 여부에 관계없이 결론 공식이 필수적입니다.


단순한 데이터 셀을 넘어 셀 스타일을 쉽게 관리할 수 있는 기능도 통합해야 합니다.


필수 요소를 식별한 후 다음 단계는 필요한 주석을 작성하는 것입니다. 초기 주석에는 셀 스타일 지정에 대한 메타데이터가 포함됩니다. 이 주석은 기본값과 함께 기본 속성을 포함합니다.

 @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를 사용하여 Excel 보고 서비스 구축

필요한 모든 주석을 생성한 후 서비스 클래스를 만드는 것이 다음 단계가 됩니다. 활용되는 핵심 라이브러리는 .xls 및 .xlsx 파일 작업에 효과적인 Apache POI 입니다. 이 클래스는 주석의 속성을 사용하여 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++; }


필드에 직접 액세스 권한을 부여하는 대신 getter를 활용하기 위해 속성 설명자가 확보됩니다.

 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 , soldpricePerUnit 세 가지 필드로 구성됩니다. 또한 판매 공식과 총계를 포함하는 결론 라인( unitsSoldtotalSales 이 있습니다. 필드는 열의 위치와 이름을 나타내는 @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 페이지 에서 확인할 수 있습니다.