导出 Word 报表总踩坑?用它快速生成文本、表格、样式
简介
后端生成 Word 文档是个很常见的需求:导出合同、输出报告、生成通知函。Apache POI 能做这些,但直接用 POI 写代码不轻松,光是处理 XWPFDocument、XWPFParagraph、XWPFRun 这几个对象的关系,理清楚就要一段时间,更别说处理字体、段落格式、表格这些细节了。
Hutool 的 WordUtil 是 Apache POI 的轻量封装,提供两个静态工厂方法创建 Word07Writer(.docx 格式),通过 Word07Writer 完成段落文本、图片、表格的写入操作,最后输出到文件或流。核心能力包括:
-
• 两种创建模式:内存模式(不预设输出文件)和文件模式(直接指定目标文件路径) -
• 文本段落写入:addText() 写入段落,支持字体样式配置 -
• 图片插入:addPicture() 支持多种图片格式 -
• 表格写入:addTable() 传入二维数据自动生成表格
常用 API 详解
1. 内存模式创建写入器 – getWriter()
API 签名:
static Word07Writer getWriter()
功能: 创建一个 Word07Writer,文档内容暂存在内存中,不关联磁盘文件。适合需要在写完之后把文档内容输出到 HTTP 响应流(即让浏览器直接下载)的场景,或者输出路径需要在业务逻辑里动态决定的场景。写完内容后调用 flush(OutputStream) 把文档冲入流,最后调用 close() 释放资源。
示例:
import cn.hutool.poi.word.Word07Writer;import cn.hutool.poi.word.WordUtil;import java.io.FileOutputStream;public class GetWriterDemo { public static void main(String[] args) throws Exception { // 内存模式:不预设文件路径 Word07Writer writer = WordUtil.getWriter(); // 写入内容 writer.addText("这是第一段正文内容"); writer.addText("这是第二段正文内容"); // 写完后输出到流(比如 HTTP 响应流,或者此处示例写到文件) try (FileOutputStream out = new FileOutputStream("/tmp/output.docx")) { writer.flush(out); } writer.close(); System.out.println("文档已生成"); }}
实战场景: 接口里生成文档并直接推给浏览器下载,不落磁盘:
public void downloadDoc(HttpServletResponse response) throws Exception { response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document"); response.setHeader("Content-Disposition", "attachment; filename=report.docx"); Word07Writer writer = WordUtil.getWriter(); writer.addText("导出报告内容"); writer.flush(response.getOutputStream()); writer.close();}
2. 文件模式创建写入器 – getWriter(File destFile)
API 签名:
static Word07Writer getWriter(File destFile)
功能: 创建一个 Word07Writer,并关联到指定的目标文件。写入完成后调用 flush() 无参版本,内容会直接写入 destFile 所指向的文件路径,不需要手动传输出流。目标文件如果不存在会自动创建,已存在则覆盖。适合输出路径明确、不需要走流的场景。
示例:
import cn.hutool.poi.word.Word07Writer;import cn.hutool.poi.word.WordUtil;import java.io.File;public class GetWriterWithFileDemo { public static void main(String[] args) { File destFile = new File("/data/reports/monthly.docx"); // 文件模式:直接绑定输出文件 Word07Writer writer = WordUtil.getWriter(destFile); writer.addText("月度报告"); writer.addText("本月销售额较上月增长 12%。"); // flush() 无参,直接写到 destFile writer.flush(); writer.close(); System.out.println("文件已写入:" + destFile.getAbsolutePath()); }}
实战场景: 定时任务生成报告文件,路径固定,直接落磁盘:
public void generateDailyReport(String outputDir, String date) { File reportFile = new File(outputDir, "report_" + date + ".docx"); Word07Writer writer = WordUtil.getWriter(reportFile); writer.addText("日报 - " + date); // 写入报告内容... writer.flush(); writer.close();}
3. Word07Writer 核心操作
WordUtil 的两个方法都返回 Word07Writer,真正的写入操作全在这个类上。以下是最常用的几个方法。
写入文本段落 – addText()
import cn.hutool.poi.word.Word07Writer;import cn.hutool.poi.word.WordUtil;import java.awt.Font;import java.io.File;public class AddTextDemo { public static void main(String[] args) { Word07Writer writer = WordUtil.getWriter(new File("/tmp/text_demo.docx")); // 默认样式写入段落 writer.addText("这是一段默认样式的正文"); // 设置字体后写入(后续段落都会应用此字体,直到再次设置) writer.setFont(new Font("宋体", Font.BOLD, 16)); writer.addText("这是加粗的16号宋体标题"); // 切换回普通样式 writer.setFont(new Font("微软雅黑", Font.PLAIN, 12)); writer.addText("这是12号微软雅黑正文段落"); // 一次传多个字符串,每个字符串作为一段 writer.addText("第一段", "第二段", "第三段"); writer.flush(); writer.close(); }}
插入图片 – addPicture()
import cn.hutool.poi.word.Word07Writer;import cn.hutool.poi.word.WordUtil;import org.apache.poi.xwpf.usermodel.Document;import java.io.File;import java.io.FileInputStream;public class AddPictureDemo { public static void main(String[] args) throws Exception { Word07Writer writer = WordUtil.getWriter(new File("/tmp/pic_demo.docx")); writer.addText("图片示例:"); // 插入图片,指定宽高(单位:像素) try (FileInputStream fis = new FileInputStream("/data/logo.png")) { writer.addPicture(fis, Document.PICTURE_TYPE_PNG, "logo.png", 200, 100); } writer.addText("图片已插入上方"); writer.flush(); writer.close(); }}
写入表格 – addTable()
import cn.hutool.poi.word.Word07Writer;import cn.hutool.poi.word.WordUtil;import java.io.File;import java.util.Arrays;import java.util.List;public class AddTableDemo { public static void main(String[] args) { Word07Writer writer = WordUtil.getWriter(new File("/tmp/table_demo.docx")); writer.addText("销售数据汇总:"); // 构造二维数据(第一行作为表头) List<List<String>> tableData = Arrays.asList( Arrays.asList("商品名称", "销售数量", "销售金额"), Arrays.asList("手冲咖啡", "128", "3840.00"), Arrays.asList("拿铁", "256", "6400.00"), Arrays.asList("美式", "96", "1920.00") ); writer.addTable(tableData); writer.flush(); writer.close(); }}
快速使用指南
Maven 依赖
WordUtil 在 hutool-poi 模块中,同时需要 POI 依赖:
<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-poi</artifactId> <version>5.8.26</version></dependency><dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>5.2.3</version></dependency>
两种模式选择
// 输出路径确定,直接落磁盘,用文件模式Word07Writer writer = WordUtil.getWriter(new File("/data/output.docx"));writer.addText("内容");writer.flush(); // 无参,写到绑定文件writer.close();// 输出路径不确定或需要写到流(如 HTTP 下载),用内存模式Word07Writer writer = WordUtil.getWriter();writer.addText("内容");writer.flush(outputStream); // 传目标流writer.close();
完整实战案例:订单对账单 Word 文档生成
电商或门店系统里,财务人员经常需要导出某个时间段的订单对账单。格式通常是:标题、摘要信息、订单明细表格、最后附上合计行。这种文档用 Word 格式比 Excel 更正式,适合打印存档。
下面实现一个订单对账单生成工具,包含标题、摘要段落、明细表格,输出到指定文件:
import cn.hutool.poi.word.Word07Writer;import cn.hutool.poi.word.WordUtil;import java.awt.Font;import java.io.File;import java.util.ArrayList;import java.util.Arrays;import java.util.List;/** * 订单对账单 Word 文档生成器 */public class OrderStatementGenerator { static class OrderItem { String orderId; String productName; int quantity; double unitPrice; double totalAmount; OrderItem(String orderId, String productName, int quantity, double unitPrice) { this.orderId = orderId; this.productName = productName; this.quantity = quantity; this.unitPrice = unitPrice; this.totalAmount = quantity * unitPrice; } } /** * 生成对账单文档 * * @param orders 订单列表 * @param period 账期(如 "2024年3月") * @param outputFile 输出文件 */ public static void generate(List<OrderItem> orders, String period, File outputFile) { Word07Writer writer = WordUtil.getWriter(outputFile); // 1. 文档标题:大号加粗 writer.setFont(new Font("宋体", Font.BOLD, 22)); writer.addText("订单对账单"); // 2. 账期信息:正常字号 writer.setFont(new Font("宋体", Font.PLAIN, 12)); writer.addText("账期:" + period); writer.addText("生成时间:" + java.time.LocalDate.now()); writer.addText("订单总数:" + orders.size() + " 笔"); // 3. 表格标题 writer.setFont(new Font("宋体", Font.BOLD, 12)); writer.addText("订单明细:"); // 4. 构造表格数据(第一行是表头) List<List<String>> tableData = new ArrayList<>(); tableData.add(Arrays.asList("订单号", "商品名称", "数量", "单价(元)", "金额(元)")); double grandTotal = 0; for (OrderItem item : orders) { tableData.add(Arrays.asList( item.orderId, item.productName, String.valueOf(item.quantity), String.format("%.2f", item.unitPrice), String.format("%.2f", item.totalAmount) )); grandTotal += item.totalAmount; } // 合计行 tableData.add(Arrays.asList( "合计", "", "", "", String.format("%.2f", grandTotal) )); writer.setFont(new Font("宋体", Font.PLAIN, 11)); writer.addTable(tableData); // 5. 尾部说明 writer.setFont(new Font("宋体", Font.PLAIN, 10)); writer.addText("本对账单由系统自动生成,如有疑问请联系财务部门。"); // 6. 写入文件并释放资源 writer.flush(); writer.close(); System.out.println("对账单已生成:" + outputFile.getAbsolutePath()); } public static void main(String[] args) { List<OrderItem> orders = Arrays.asList( new OrderItem("ORD-20240301-001", "手冲咖啡", 2, 30.0), new OrderItem("ORD-20240301-002", "拿铁", 3, 25.0), new OrderItem("ORD-20240302-003", "美式", 1, 20.0), new OrderItem("ORD-20240302-004", "卡布奇诺", 2, 28.0), new OrderItem("ORD-20240303-005", "手冲咖啡", 1, 30.0) ); File output = new File("/data/statements/statement_2024_03.docx"); generate(orders, "2024年3月", output); }}
生成的文档依次包含:加粗大号标题”订单对账单”、账期和生成时间的摘要段落、订单明细表格(含合计行)、尾部说明。setFont 在每个区域切换一次字体样式,标题和正文、表格标题区分开,视觉层次更清晰。
整个逻辑只有 Writer 的构建、内容写入、flush、close 四个步骤,不需要关心 POI 底层的 XWPFDocument 对象,代码量比直接用 POI 少一半以上。
常见问题
Q:getWriter() 和 getWriter(File) 生成的文档格式有区别吗?A:没有区别。两者都生成 .docx 格式(Office Open XML,Word 2007 及以上可打开),区别只在输出目标:内存模式需要手动传流,文件模式绑定磁盘路径。生成的文档内容和格式完全一致。
Q:flush() 和 flush(OutputStream) 能混用吗?A:不能混用。文件模式下调用 flush(OutputStream) 会抛出异常,应该调用无参的 flush();内存模式下调用无参 flush() 同样会报错,必须传一个 OutputStream。使用前明确自己用的是哪种模式。
Q:addText 之后 setFont 还能改变已写入内容的字体吗?A:不能。setFont 只影响后续调用 addText 时新写入的段落,已经写入的段落字体不会回溯修改。需要在调用 addText 之前设置好字体。
Q:表格内容有中文,生成后显示乱码怎么排查?A:通常是字体问题,确认 setFont 时指定的字体在运行环境里存在(服务器上 Linux 默认不带微软雅黑)。备用方案是不调用 setFont,让文档使用默认字体,或者在服务器上安装对应字体文件。
总结
WordUtil 是一个极简的入口,两个工厂方法区别只在输出目标,真正的功能都在 Word07Writer 上:
|
|
|
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
生成文档的固定套路是:创建 Writer、setFont + addText 写标题、addTable 写数据、flush 输出、close 释放,顺序不变,内容按需填充。
运行效果:
END
夜雨聆风