java easyexcel主子表报表打印实践
作者:leijmdas
EasyExcel支持主子表报表导出,可通过多sheet分开展示或单sheet合并单元格方式实现,后者需自定义策略,适合传统报表形式,根据需求选择合适方案即可
Java EasyExcel 主子表报表打印
EasyExcel 是阿里巴巴开源的一个基于 Java 的简单、省内存的读写 Excel 的工具,特别适合大数据量的 Excel 操作。
下面我将介绍如何使用 EasyExcel 实现主子表结构的报表打印。
1. 添加依赖
首先需要在项目中添加 EasyExcel 依赖:
<dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>3.3.2</version> <!-- 使用最新版本 --> </dependency>
2. 主子表数据结构
假设我们有一个订单(主表)和订单项(子表)的结构:
// 主表数据 public class Order { private String orderNo; private Date orderDate; private String customerName; private List<OrderItem> items; // getters and setters } // 子表数据 public class OrderItem { private String productName; private Integer quantity; private BigDecimal price; private BigDecimal amount; // getters and setters }
3. 实现主子表导出
方法一:使用合并单元格方式
public class OrderExcelExport { public static void exportOrders(List<Order> orders, HttpServletResponse response) throws IOException { String fileName = "订单报表_" + System.currentTimeMillis() + ".xlsx"; response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); response.setHeader("Content-disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8")); ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).build(); // 主表sheet WriteSheet mainSheet = EasyExcel.writerSheet(0, "订单信息") .head(Order.class) .build(); // 子表sheet WriteSheet itemSheet = EasyExcel.writerSheet(1, "订单明细") .head(OrderItem.class) .build(); // 写入主表数据 excelWriter.write(orders, mainSheet); // 写入子表数据 for (Order order : orders) { excelWriter.write(order.getItems(), itemSheet); } excelWriter.finish(); } }
方法二:单个sheet中展示主子表关系
public class OrderExcelExport { public static void exportOrdersInOneSheet(List<Order> orders, HttpServletResponse response) throws IOException { String fileName = "订单报表_" + System.currentTimeMillis() + ".xlsx"; response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); response.setHeader("Content-disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8")); // 自定义合并策略 HorizontalCellStyleStrategy styleStrategy = new HorizontalCellStyleStrategy(); ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()) .registerWriteHandler(new OrderMergeStrategy(orders)) .registerWriteHandler(styleStrategy) .build(); WriteSheet sheet = EasyExcel.writerSheet("订单及明细") .head(OrderExcelData.class) .build(); // 转换数据 List<OrderExcelData> dataList = convertToExcelData(orders); excelWriter.write(dataList, sheet); excelWriter.finish(); } private static List<OrderExcelData> convertToExcelData(List<Order> orders) { List<OrderExcelData> result = new ArrayList<>(); for (Order order : orders) { boolean isFirstItem = true; for (OrderItem item : order.getItems()) { OrderExcelData data = new OrderExcelData(); if (isFirstItem) { data.setOrderNo(order.getOrderNo()); data.setOrderDate(order.getOrderDate()); data.setCustomerName(order.getCustomerName()); isFirstItem = false; } data.setProductName(item.getProductName()); data.setQuantity(item.getQuantity()); data.setPrice(item.getPrice()); data.setAmount(item.getAmount()); result.add(data); } } return result; } } // 合并策略 public class OrderMergeStrategy implements CellWriteHandler { private List<Order> orders; private int itemCount = 0; public OrderMergeStrategy(List<Order> orders) { this.orders = orders; for (Order order : orders) { itemCount += order.getItems().size(); } } @Override public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<CellData> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) { // 合并主表信息单元格 if (cell.getRowIndex() == 1 && !isHead) { for (int i = 0; i < orders.size(); i++) { Order order = orders.get(i); int itemSize = order.getItems().size(); if (itemSize > 1) { // 合并订单号、日期、客户名等主表信息 if (cell.getColumnIndex() == 0) { // 订单号 writeSheetHolder.getSheet().addMergedRegionUnsafe( new CellRangeAddress(cell.getRowIndex(), cell.getRowIndex() + itemSize - 1, 0, 0)); } else if (cell.getColumnIndex() == 1) { // 订单日期 writeSheetHolder.getSheet().addMergedRegionUnsafe( new CellRangeAddress(cell.getRowIndex(), cell.getRowIndex() + itemSize - 1, 1, 1)); } else if (cell.getColumnIndex() == 2) { // 客户名 writeSheetHolder.getSheet().addMergedRegionUnsafe( new CellRangeAddress(cell.getRowIndex(), cell.getRowIndex() + itemSize - 1, 2, 2)); } } } } } } // Excel数据模型 public class OrderExcelData { @ExcelProperty("订单编号") private String orderNo; @ExcelProperty("订单日期") @DateTimeFormat("yyyy-MM-dd") private Date orderDate; @ExcelProperty("客户名称") private String customerName; @ExcelProperty("产品名称") private String productName; @ExcelProperty("数量") private Integer quantity; @ExcelProperty("单价") private BigDecimal price; @ExcelProperty("金额") private BigDecimal amount; // getters and setters }
4. 控制器调用
@RestController @RequestMapping("/api/order") public class OrderController { @GetMapping("/export") public void exportOrders(HttpServletResponse response) throws IOException { // 模拟数据 List<Order> orders = getMockOrders(); // 导出Excel OrderExcelExport.exportOrdersInOneSheet(orders, response); } private List<Order> getMockOrders() { List<Order> orders = new ArrayList<>(); // 订单1 Order order1 = new Order(); order1.setOrderNo("ORD20230001"); order1.setOrderDate(new Date()); order1.setCustomerName("客户A"); List<OrderItem> items1 = new ArrayList<>(); OrderItem item1 = new OrderItem(); item1.setProductName("产品A"); item1.setQuantity(2); item1.setPrice(new BigDecimal("100.00")); item1.setAmount(new BigDecimal("200.00")); items1.add(item1); OrderItem item2 = new OrderItem(); item2.setProductName("产品B"); item2.setQuantity(1); item2.setPrice(new BigDecimal("50.00")); item2.setAmount(new BigDecimal("50.00")); items1.add(item2); order1.setItems(items1); orders.add(order1); // 订单2 Order order2 = new Order(); order2.setOrderNo("ORD20230002"); order2.setOrderDate(new Date()); order2.setCustomerName("客户B"); List<OrderItem> items2 = new ArrayList<>(); OrderItem item3 = new OrderItem(); item3.setProductName("产品C"); item3.setQuantity(3); item3.setPrice(new BigDecimal("30.00")); item3.setAmount(new BigDecimal("90.00")); items2.add(item3); order2.setItems(items2); orders.add(order2); return orders; } }
5. 高级功能
自定义样式
// 自定义样式策略 public class CustomCellStyleStrategy implements CellWriteHandler { @Override public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead) { } @Override public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) { Workbook workbook = writeSheetHolder.getSheet().getWorkbook(); // 标题样式 if (isHead) { CellStyle cellStyle = workbook.createCellStyle(); Font font = workbook.createFont(); font.setBold(true); font.setColor(IndexedColors.WHITE.getIndex()); cellStyle.setFont(font); cellStyle.setFillForegroundColor(IndexedColors.BLUE.getIndex()); cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); cellStyle.setAlignment(HorizontalAlignment.CENTER); cell.setCellStyle(cellStyle); } // 数据行样式 else { CellStyle cellStyle = workbook.createCellStyle(); cellStyle.setBorderTop(BorderStyle.THIN); cellStyle.setBorderBottom(BorderStyle.THIN); cellStyle.setBorderLeft(BorderStyle.THIN); cellStyle.setBorderRight(BorderStyle.THIN); // 金额列右对齐 if (cell.getColumnIndex() >= 5) { cellStyle.setAlignment(HorizontalAlignment.RIGHT); } cell.setCellStyle(cellStyle); } } @Override public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<CellData> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) { } }
然后在导出时注册这个样式策略:
ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()) .registerWriteHandler(new OrderMergeStrategy(orders)) .registerWriteHandler(new CustomCellStyleStrategy()) .build();
总结
EasyExcel 提供了灵活的方式来实现主子表结构的报表导出,主要有两种方式:
- 使用多个 sheet 分别展示主表和子表数据
- 在单个 sheet 中使用合并单元格的方式展示主子表关系
第二种方式更符合传统的报表展示形式,但实现起来稍复杂,需要自定义合并策略。根据实际需求选择合适的方式即可。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。