乐于分享
好东西不私藏

导出 Word 报表总踩坑?用它快速生成文本、表格、样式

导出 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 上:

方法
说明
WordUtil.getWriter()
内存模式,输出到流,适合 HTTP 下载
WordUtil.getWriter(File destFile)
文件模式,直接落磁盘,适合定时任务和后台生成
Word07Writer.addText(CharSequence…)
写入一段或多段文字
Word07Writer.setFont(Font font)
设置后续段落的字体样式
Word07Writer.addPicture(…)
插入图片,指定格式和宽高
Word07Writer.addTable(Iterable)
传入二维数据生成表格
Word07Writer.flush()
写出到绑定文件(文件模式)
Word07Writer.flush(OutputStream)
写出到指定流(内存模式)
Word07Writer.close()
释放资源,每次使用后必须调用

生成文档的固定套路是:创建 Writer、setFont + addText 写标题、addTable 写数据、flush 输出、close 释放,顺序不变,内容按需填充。

运行效果

END

你的关注将是我持续更新的动力
↓↓↓