Ngay cả với vô số thư viện hiện có ngày nay, đôi khi vẫn khó tìm được thư viện cung cấp chức năng cụ thể cần thiết cho một tác vụ cụ thể. Thay vì dành thời gian tìm kiếm thư viện hoàn hảo, tôi khuyên bạn nên tự triển khai; ngay cả khi nó được thiết kế riêng cho một dự án.
Một lần, tôi thấy mình cần một thư viện có thể dễ dàng chuyển đổi các lớp dữ liệu thành tài liệu Excel để tạo báo cáo. Khi tôi không thể tìm được thư viện phù hợp, tôi quyết định phát triển chức năng phù hợp với nhu cầu cụ thể của mình.
Tham vọng của tôi là thiết kế một thư viện giống như Jackson, thư viện này sẽ sử dụng các chú thích để chuyển đổi danh sách các lớp dữ liệu thành tài liệu Excel theo yêu cầu của các chú thích.
Tôi muốn chia sẻ thư viện mà tôi đã tạo, hy vọng nó có thể mang lại lợi ích cho người khác hoặc truyền cảm hứng cho họ tạo công cụ lập bản đồ của riêng mình cho các nhiệm vụ riêng của họ. Hãy cùng khám phá cách phát triển một trình ánh xạ dữ liệu như vậy trong Java để đạt được điều này:
từ đây:
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); } }
Hãy xác định các yếu tố chính cần thiết cho việc lập bản đồ Excel. Về cốt lõi, chúng tôi yêu cầu một cột Excel. Thành phần cơ bản này của báo cáo phải hiển thị rõ ràng tên cột và giá trị tương ứng trong mỗi hàng.
Hơn nữa, chúng tôi phải kết hợp hỗ trợ cho các ô công thức, cho phép chúng tôi sử dụng các giá trị và trình bày kết quả một cách linh hoạt. Ở cuối cột, công thức kết luận là cần thiết, cho dù nó đại diện cho giá trị trung bình, tổng hay bất kỳ số liệu thích hợp nào khác cho người dùng cuối.
Ngoài các ô dữ liệu đơn thuần, chúng ta cũng nên tích hợp các tính năng để dễ dàng quản lý kiểu dáng ô.
Sau khi xác định được các yếu tố cần thiết, bước tiếp theo là tạo các chú thích cần thiết. Chú thích ban đầu sẽ nhúng siêu dữ liệu về kiểu dáng ô. Chú thích này sẽ bao gồm các thuộc tính cơ bản cùng với các giá trị mặc định của chúng:
@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; }
Các thành phần kiểu chính quan trọng cho việc tạo báo cáo được chuyển tải dưới dạng thuộc tính. Theo đó, việc bắt đầu chú thích cột Excel có thể được thực hiện:
@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; }
Chú thích này bao gồm các tên cột tiềm năng (để ánh xạ từ Excel) và trường bắt buộc - ' position
'. Điều này sẽ xác định vị trí của cột và là công cụ tính toán công thức. Ngoài ra, nó sẽ trình bày chi tiết về kiểu dáng của cả tiêu đề và ô.
Xuất sắc. Bây giờ, hãy tạo một chú thích cụ thể cho các công thức Excel. Dự đoán một công thức động phụ thuộc vào vị trí của hàng, chú thích này sẽ dành riêng cho các phương thức:
@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; }
Cuối cùng, hãy giới thiệu chú thích cho công thức tính tổng, thường chiếm hàng cuối cùng trong Excel, tóm tắt hoặc minh họa kết quả tích lũy cho một cột. Vì chú thích của công thức cũng chỉ yêu cầu khả năng áp dụng của nó cho các phương thức:
@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface ColumnExcelTotalFormula { boolean useValue() default false; int position(); ColumnExcelStyle cellStyle() default @ColumnExcelStyle; }
Sau khi tạo tất cả các chú thích cần thiết, việc tạo một lớp dịch vụ sẽ trở thành bước tiếp theo. Thư viện cốt lõi được sử dụng sẽ là Apache POI , hiệu quả để làm việc với các tệp .xls và .xlsx. Lớp này sẽ sử dụng các thuộc tính của chú thích để tạo báo cáo Excel.
Phương thức chính chấp nhận danh sách các đối tượng làm đầu vào và trả về một sổ làm việc đã chuẩn bị sẵn.
Để tăng tính linh hoạt, một phương pháp nạp chồng sẽ được giới thiệu để cho phép đặc tả cả tên tệp và tên trang tính để tạo báo cáo:
<T> Workbook createWorkbookFromObject(List<T> reportObjects) { return createWorkbookFromObject(reportObjects, 0, "Report"); } <T> Workbook createWorkbookFromObject(List<T> reportObjects, int startRowNumber, String sheetName) { ... }
Để trích xuất thông tin về lớp bằng cách sử dụng sự phản chiếu, bất kỳ phần tử nào từ mảng đều được chọn. Sau khi truy cập chi tiết lớp, hàng đầu tiên có thể được thiết lập. Việc sử dụng dữ liệu từ chú thích cho phép tạo các ô có tên tương ứng.
Nếu tên vắng mặt, tên của trường lớp có thể dùng để thay thế:
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()); } }
Khi tạo kiểu cho tiêu đề, hãy nhớ gán kiểu cho mỗi ô. Các tham số kiểu có thể được lấy từ chú thích @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()); }
Sau đó, quy trình sẽ chuyển sang tạo các hàng trong báo cáo dựa trên dữ liệu từ mảng được cung cấp. Bằng cách lặp lại dữ liệu, các hàng liên tiếp được hình thành:
for (T report : reportObjects) { Row bodyRow = sheet.createRow(proceedRowNumber); createCellsFromDeclaredExcelColumns(bodyRow, report); proceedRowNumber++; }
Bộ mô tả thuộc tính được mua để sử dụng getters thay vì cấp quyền truy cập trực tiếp vào các trường:
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()); } }
Với bộ mô tả thuộc tính, một ô được hình thành:
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()); } }
Tiếp theo, hãy chuyển sự chú ý của chúng ta sang việc xử lý các chú thích @ColumnExcelFormula
. Việc tạo các công thức tỏ ra phức tạp hơn một chút so với việc chỉ trích xuất một giá trị từ một trường. Một phương thức dự kiến sẽ tạo ra một công thức, sau đó được gán cho ô.
Phương thức phải trả về một chuỗi một cách nhất quán và chấp nhận số hàng làm đối số, đảm bảo việc sử dụng dữ liệu chính xác từ các ô liền kề.
Do đó, trách nhiệm của trình xử lý là xác minh các điều kiện này được đáp ứng trước khi tạo ô với công thức đã chỉ định:
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()); } }
Bước cuối cùng liên quan đến việc tạo một hàng để hiển thị kết quả cuối cùng. Điều quan trọng là hàng này chỉ được tạo một lần, bất kể số lượng đối tượng được chuyển tiếp đến trình xử lý. Một chú thích trong một phương thức tĩnh được dự đoán cho mục đích này.
Phương thức này nhận cả số hàng đầu tiên và hàng hiện tại nơi ô sẽ được khởi tạo làm đối số.
Việc cung cấp số của hàng đầu tiên là rất quan trọng, cho phép phương pháp đưa ra công thức tận dụng kết quả tổng hợp cho toàn bộ cột:
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()); } } }
Chức năng chính hiện đã có sẵn và đã đến lúc xem nó hoạt động như thế nào. Hãy xây dựng một báo cáo đơn giản để chứng minh hoạt động của nó. Để làm điều này, hãy tạo một lớp ' Sales
' và kết hợp tất cả các chú thích cần thiết:
@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() + ")"; } }
Lớp này bao gồm ba trường: date
, sold
và pricePerUnit
. Ngoài ra, nó còn có công thức bán hàng và dòng kết luận với tổng số: unitsSold
và totalSales
. Các trường sử dụng chú thích @ColumnExcel
, biểu thị vị trí và tên của cột.
Chú thích @ColumnExcelStyle
xác định kiểu cho cả ô dữ liệu tiêu đề và riêng lẻ:
@ColumnExcel( position = 0, applyNames = {"Date"}, headerStyle = @ColumnExcelStyle( fontColor = WHITE, cellColor = DARK_BLUE, isCentreAlignment = true), cellStyle = @ColumnExcelStyle( cellColor = GREY_25_PERCENT, cellTypePattern = DATE) )
Như đã thảo luận trước đó, khi tạo công thức, phương thức phải chấp nhận tham số cho biết số hàng. Yêu cầu này được thể hiện rõ trong chữ ký của phương thức:
public String sales(int rowNum) { return new CellAddress(rowNum, 1).formatAsString() + "*" + new CellAddress(rowNum, 2).formatAsString(); }
Với số hàng và chỉ số cột, việc tạo bất kỳ công thức cụ thể nào đều trở nên khả thi.
Trong lớp, các phương thức dành cho công thức kết luận là tĩnh và yêu cầu hai tham số: số của hàng bắt đầu và số của hàng kết thúc:
public static String unitsSold(int firstRowNum, int lastRowNum) { return "SUM(" + new CellAddress(firstRowNum, 1).formatAsString() + ":" + new CellAddress(lastRowNum - 1, 1).formatAsString() + ")"; }
Bây giờ, hãy khởi chạy phương thức:
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); } }
Và kiểm tra báo cáo được tạo:
Viết một thư viện chuyên dụng cho một nhiệm vụ cụ thể tỏ ra đơn giản. Thư viện được tạo ra đáp ứng các yêu cầu và bao gồm các chức năng theo kế hoạch. Cách tiếp cận sử dụng các chú thích tạo điều kiện cho việc tùy chỉnh nhanh chóng và thuận tiện các kiểu ô, sửa đổi công thức và tạo báo cáo động từ nhiều nguồn dữ liệu khác nhau.
Do đó, lần tới khi khó tìm được một thư viện phù hợp, việc phát triển một thư viện được cá nhân hóa có thể mang lại lợi ích.
Đương nhiên, việc trình bày từng dòng mã trong bài viết này là không khả thi; do đó, chỉ những phương pháp chính cần thiết cho hoạt động của người lập bản đồ mới được đánh dấu. Mã hoàn chỉnh có sẵn trên trang GitHub của tôi.