一键操作 Word 表格:Hutool TableUtil 从入门到实战
在企业应用开发中,生成 Word 格式的报表、合同、审批单是常见需求。Apache POI 虽然功能强大,但操作 Word 表格的 API 相对繁琐,需要手动处理行列创建、数据填充等细节。Hutool 的 TableUtil 对 POI 的表格操作进行了封装,让 Word 表格的创建和数据填充变得简单高效。
核心功能
TableUtil 专注于 Word 文档(docx 格式)中的表格操作,提供以下能力:
-
1. 快速创建表格:支持空表格和带数据的表格创建 -
2. 智能行列管理:自动获取或创建行和单元格,避免索引越界 -
3. 灵活数据填充:支持集合、Map、JavaBean 等多种数据源 -
4. 简化代码逻辑:减少样板代码,提升开发效率
常用 API
1. 创建空表格
最基础的表格创建方式,生成一个只有一行的空表格。
import cn.hutool.poi.word.TableUtil;import org.apache.poi.xwpf.usermodel.XWPFDocument;import org.apache.poi.xwpf.usermodel.XWPFTable;import java.io.FileOutputStream;// 创建 Word 文档对象XWPFDocument doc = new XWPFDocument();// 创建空表格(默认只有一行)XWPFTable table = TableUtil.createTable(doc);// 保存文档try (FileOutputStream out = new FileOutputStream("empty_table.docx")) { doc.write(out);}
这个方法适用于需要手动控制表格结构的场景,比如复杂的表头设计或动态列数的表格。
2. 创建表格并填充数据
直接传入数据集合,TableUtil 会自动创建表格并填充内容。
import cn.hutool.core.collection.CollUtil;// 准备数据(List 集合)List<List<String>> data = CollUtil.newArrayList( CollUtil.newArrayList("姓名", "部门", "职位"), CollUtil.newArrayList("张三", "技术部", "Java 工程师"), CollUtil.newArrayList("李四", "产品部", "产品经理"), CollUtil.newArrayList("王五", "运营部", "运营专员"));XWPFDocument doc = new XWPFDocument();// 创建表格并填充数据XWPFTable table = TableUtil.createTable(doc, data);try (FileOutputStream out = new FileOutputStream("employee_table.docx")) { doc.write(out);}
这种方式特别适合数据结构简单、格式统一的表格,比如员工名单、商品清单等。
3. 获取或创建行
在动态构建表格时,经常需要在指定位置插入数据。getOrCreateRow 方法会智能判断:如果行已存在则直接返回,不存在则自动创建。
XWPFDocument doc = new XWPFDocument();XWPFTable table = TableUtil.createTable(doc);// 获取或创建第 0 行(已存在,直接返回)XWPFTableRow row0 = TableUtil.getOrCreateRow(table, 0);// 获取或创建第 1 行(不存在,自动创建)XWPFTableRow row1 = TableUtil.getOrCreateRow(table, 1);// 获取或创建第 5 行(跳过 2-4 行,自动创建所有缺失的行)XWPFTableRow row5 = TableUtil.getOrCreateRow(table, 5);System.out.println("表格总行数: " + table.getRows().size());// 输出: 表格总行数: 6
这个方法的优势在于避免了手动判断行是否存在,也不用担心索引越界异常。
4. 获取或创建单元格
与获取行类似,getOrCreateCell 方法用于获取或创建指定列的单元格。
XWPFDocument doc = new XWPFDocument();XWPFTable table = TableUtil.createTable(doc);XWPFTableRow row = table.getRow(0);// 获取或创建第 0 列单元格XWPFTableCell cell0 = TableUtil.getOrCreateCell(row, 0);cell0.setText("姓名");// 获取或创建第 1 列单元格XWPFTableCell cell1 = TableUtil.getOrCreateCell(row, 1);cell1.setText("年龄");// 获取或创建第 2 列单元格XWPFTableCell cell2 = TableUtil.getOrCreateCell(row, 2);cell2.setText("部门");
配合 getOrCreateRow 使用,可以灵活地在表格任意位置插入数据。
5. 写入行数据(集合方式)
writeRow 方法支持多种数据源,最简单的是传入集合。
XWPFDocument doc = new XWPFDocument();XWPFTable table = TableUtil.createTable(doc);// 写入表头XWPFTableRow headerRow = table.getRow(0);List<String> headers = CollUtil.newArrayList("编号", "商品名称", "单价", "库存");TableUtil.writeRow(headerRow, headers);// 写入数据行XWPFTableRow dataRow = TableUtil.getOrCreateRow(table, 1);List<Object> rowData = CollUtil.newArrayList("P001", "咖啡豆", 89.9, 500);TableUtil.writeRow(dataRow, rowData);try (FileOutputStream out = new FileOutputStream("product_table.docx")) { doc.write(out);}
6. 写入行数据(Map 方式)
当数据源是 Map 时,可以选择是否将 Key 作为表头写入。
XWPFDocument doc = new XWPFDocument();XWPFTable table = TableUtil.createTable(doc);// 准备 Map 数据Map<String, Object> userData = new LinkedHashMap<>();userData.put("姓名", "张三");userData.put("年龄", 28);userData.put("职位", "Java 工程师");userData.put("薪资", 15000);// 写入数据,第一行作为表头(isWriteKeyAsHead = true)XWPFTableRow row0 = table.getRow(0);TableUtil.writeRow(row0, userData, true);// 写入第二行数据,不写入 Key(isWriteKeyAsHead = false)Map<String, Object> userData2 = new LinkedHashMap<>();userData2.put("姓名", "李四");userData2.put("年龄", 30);userData2.put("职位", "产品经理");userData2.put("薪资", 18000);XWPFTableRow row1 = TableUtil.getOrCreateRow(table, 1);TableUtil.writeRow(row1, userData2, false);
注意:使用 LinkedHashMap 可以保证 Key 的顺序,避免列顺序混乱。
7. 写入行数据(JavaBean 方式)
对于 JavaBean 对象,TableUtil 会自动提取字段值填充到表格中。
// 定义 JavaBean@Datapublic class Employee { private String name; private Integer age; private String department; private Double salary;}// 使用示例XWPFDocument doc = new XWPFDocument();XWPFTable table = TableUtil.createTable(doc);Employee emp = new Employee();emp.setName("王五");emp.setAge(25);emp.setDepartment("技术部");emp.setSalary(12000.0);// 写入数据,第一行作为表头XWPFTableRow row = table.getRow(0);TableUtil.writeRow(row, emp, true);
这种方式特别适合从数据库查询出实体对象后直接导出到 Word 文档。
8. 批量填充表格数据
writeTable 方法可以一次性将整个数据集合填充到表格中。
XWPFDocument doc = new XWPFDocument();// 准备数据List<List<String>> data = CollUtil.newArrayList( CollUtil.newArrayList("订单号", "客户", "金额", "状态"), CollUtil.newArrayList("ORD001", "张三", "1580.00", "已完成"), CollUtil.newArrayList("ORD002", "李四", "2300.00", "待发货"), CollUtil.newArrayList("ORD003", "王五", "890.00", "已取消"));// 先创建空表格XWPFTable table = TableUtil.createTable(doc);// 批量填充数据TableUtil.writeTable(table, data);try (FileOutputStream out = new FileOutputStream("order_table.docx")) { doc.write(out);}
这个方法内部会自动处理行的创建和数据填充,是最便捷的批量操作方式。
实战场景:生成月度销售报表
在咖啡店管理系统中,每月需要生成销售报表提交给管理层。报表包含商品销售统计、员工业绩排名等信息。使用 TableUtil 可以快速将数据库查询结果导出为 Word 文档。
import cn.hutool.poi.word.TableUtil;import org.apache.poi.xwpf.usermodel.*;import org.springframework.stereotype.Service;import java.io.FileOutputStream;import java.math.BigDecimal;import java.time.LocalDate;import java.time.format.DateTimeFormatter;import java.util.ArrayList;import java.util.List;@Servicepublic class SalesReportService { /** * 生成月度销售报表 */ public void generateMonthlySalesReport(int year, int month) throws Exception { // 创建 Word 文档 XWPFDocument doc = new XWPFDocument(); // 添加标题 XWPFParagraph title = doc.createParagraph(); title.setAlignment(ParagraphAlignment.CENTER); XWPFRun titleRun = title.createRun(); titleRun.setText(year + "年" + month + "月销售报表"); titleRun.setFontSize(18); titleRun.setBold(true); // 添加生成时间 XWPFParagraph datePara = doc.createParagraph(); XWPFRun dateRun = datePara.createRun(); dateRun.setText("生成时间:" + LocalDate.now().format( DateTimeFormatter.ofPattern("yyyy-MM-dd"))); dateRun.setFontSize(10); // 空行 doc.createParagraph(); // 第一部分:商品销售统计 XWPFParagraph section1 = doc.createParagraph(); XWPFRun section1Run = section1.createRun(); section1Run.setText("一、商品销售统计"); section1Run.setFontSize(14); section1Run.setBold(true); // 从数据库查询商品销售数据(模拟) List<ProductSales> productSalesList = queryProductSales(year, month); // 构建表格数据 List<List<Object>> productData = new ArrayList<>(); productData.add(List.of("排名", "商品名称", "销售数量", "销售金额", "占比")); int rank = 1; for (ProductSales sales : productSalesList) { productData.add(List.of( rank++, sales.getProductName(), sales.getQuantity(), sales.getAmount().toString(), sales.getPercentage() + "%" )); } // 创建商品销售表格 XWPFTable productTable = TableUtil.createTable(doc); TableUtil.writeTable(productTable, productData); // 设置表格样式 productTable.setWidth("100%"); // 空行 doc.createParagraph(); // 第二部分:员工业绩排名 XWPFParagraph section2 = doc.createParagraph(); XWPFRun section2Run = section2.createRun(); section2Run.setText("二、员工业绩排名"); section2Run.setFontSize(14); section2Run.setBold(true); // 从数据库查询员工业绩数据(模拟) List<EmployeePerformance> performanceList = queryEmployeePerformance(year, month); // 构建员工业绩表格 XWPFTable empTable = TableUtil.createTable(doc); // 写入表头 XWPFTableRow headerRow = empTable.getRow(0); List<String> headers = List.of("排名", "员工姓名", "接单数", "销售额", "客户评分"); TableUtil.writeRow(headerRow, headers); // 逐行写入员工数据 int empRank = 1; for (EmployeePerformance perf : performanceList) { XWPFTableRow dataRow = TableUtil.getOrCreateRow(empTable, empRank); List<Object> rowData = List.of( empRank, perf.getEmployeeName(), perf.getOrderCount(), perf.getSalesAmount().toString(), perf.getRating() ); TableUtil.writeRow(dataRow, rowData); empRank++; } // 设置表格样式 empTable.setWidth("100%"); // 空行 doc.createParagraph(); // 第三部分:总结 XWPFParagraph summary = doc.createParagraph(); XWPFRun summaryRun = summary.createRun(); summaryRun.setText("三、数据总结"); summaryRun.setFontSize(14); summaryRun.setBold(true); // 计算汇总数据 BigDecimal totalAmount = productSalesList.stream() .map(ProductSales::getAmount) .reduce(BigDecimal.ZERO, BigDecimal::add); int totalOrders = performanceList.stream() .mapToInt(EmployeePerformance::getOrderCount) .sum(); // 创建汇总表格 XWPFTable summaryTable = TableUtil.createTable(doc); List<List<String>> summaryData = List.of( List.of("指标", "数值"), List.of("总销售额", totalAmount.toString() + " 元"), List.of("总订单数", totalOrders + " 单"), List.of("客单价", totalAmount.divide( BigDecimal.valueOf(totalOrders), 2, BigDecimal.ROUND_HALF_UP ).toString() + " 元") ); TableUtil.writeTable(summaryTable, summaryData); // 保存文档 String fileName = String.format("销售报表_%d年%d月.docx", year, month); try (FileOutputStream out = new FileOutputStream(fileName)) { doc.write(out); } System.out.println("报表生成成功:" + fileName); } // 模拟数据库查询方法 private List<ProductSales> queryProductSales(int year, int month) { List<ProductSales> list = new ArrayList<>(); list.add(new ProductSales("美式咖啡", 320, new BigDecimal("9600.00"), "35.2")); list.add(new ProductSales("拿铁咖啡", 280, new BigDecimal("11200.00"), "41.0")); list.add(new ProductSales("卡布奇诺", 150, new BigDecimal("6000.00"), "22.0")); list.add(new ProductSales("摩卡咖啡", 50, new BigDecimal("2500.00"), "9.2")); return list; } private List<EmployeePerformance> queryEmployeePerformance(int year, int month) { List<EmployeePerformance> list = new ArrayList<>(); list.add(new EmployeePerformance("张三", 180, new BigDecimal("18000.00"), 4.8)); list.add(new EmployeePerformance("李四", 150, new BigDecimal("15500.00"), 4.6)); list.add(new EmployeePerformance("王五", 120, new BigDecimal("12000.00"), 4.5)); return list; }}// 商品销售数据实体@Dataclass ProductSales { private String productName; private Integer quantity; private BigDecimal amount; private String percentage; public ProductSales(String productName, Integer quantity, BigDecimal amount, String percentage) { this.productName = productName; this.quantity = quantity; this.amount = amount; this.percentage = percentage; }}// 员工业绩数据实体@Dataclass EmployeePerformance { private String employeeName; private Integer orderCount; private BigDecimal salesAmount; private Double rating; public EmployeePerformance(String employeeName, Integer orderCount, BigDecimal salesAmount, Double rating) { this.employeeName = employeeName; this.orderCount = orderCount; this.salesAmount = salesAmount; this.rating = rating; }}
运行效果:
生成的 Word 文档包含三个部分:
-
1. 商品销售统计表:展示各商品的销售数量、金额和占比 -
2. 员工业绩排名表:展示员工的接单数、销售额和客户评分 -
3. 数据总结表:汇总总销售额、总订单数和客单价
这个实战案例展示了 TableUtil 的几个关键用法:
-
• 使用 createTable 和 writeTable 快速创建带数据的表格 -
• 使用 getOrCreateRow 和 writeRow 逐行填充数据 -
• 结合 POI 的原生 API 设置表格样式和文档格式 -
• 处理复杂的业务数据结构(集合、实体类)
使用技巧
1. 数据源选择
根据数据结构选择合适的方法:
-
• 简单二维数据:使用 List<List<?>> + writeTable -
• 键值对数据:使用 Map + writeRow(适合动态列) -
• 实体对象:使用 JavaBean + writeRow(适合固定结构)
2. 表格样式设置
TableUtil 专注于数据填充,样式设置需要结合 POI 原生 API:
// 设置表格宽度table.setWidth("100%");// 设置单元格背景色XWPFTableCell cell = row.getCell(0);cell.setColor("F0F0F0");// 设置单元格文本对齐XWPFParagraph para = cell.getParagraphs().get(0);para.setAlignment(ParagraphAlignment.CENTER);
3. 性能优化
处理大量数据时的建议:
-
• 避免频繁创建文档对象,复用 XWPFDocument -
• 批量操作优先使用 writeTable,减少方法调用次数 -
• 大数据量时考虑分页生成多个文档
4. 异常处理
文件操作需要妥善处理异常:
try (FileOutputStream out = new FileOutputStream("report.docx")) { doc.write(out);} catch (IOException e) { log.error("文档生成失败", e); throw new BusinessException("报表导出失败,请稍后重试");}
总结
TableUtil 是 Hutool 针对 Word 表格操作的实用工具类,它的价值在于:
-
1. 简化代码:将繁琐的行列创建逻辑封装成简洁的方法调用 -
2. 提升效率:支持多种数据源,减少数据转换的工作量 -
3. 降低出错:智能的 getOrCreate 机制避免索引越界异常 -
4. 易于维护:代码可读性强,后续修改成本低
在需要生成 Word 报表、合同、审批单等场景中,TableUtil 配合 POI 可以显著提升开发效率。掌握这个工具类,能让你在处理文档导出需求时更加从容。
运行效果:
END
夜雨聆风