Apache POI 是 Java 操作 Excel 的标准库,但原生 POI 存在致命痛点:创建工作簿/单元格需几十行模板代码、大数据量读写易 OOM、日期/数字格式处理易出错、样式配置繁琐、模板填充逻辑复杂……而 Hutool 的 cn.hutool.poi.excel 模块(核心是 ExcelUtil)正是针对这些痛点的“轻量化封装”——它基于 POI 简化了 90% 的模板代码,无需关注 Workbook/Sheet/Row 的底层操作,一行代码即可实现 Excel 导入导出,还内置大数据量读写、样式定制、模板填充等能力,让 Excel 操作从“繁琐配置”变为“开箱即用”。

本文将从原生 POI 痛点入手,拆解 ExcelUtil 的核心价值,并结合 6 个高频业务场景(简单导出/导入、大数据量读写、样式定制、模板填充、多sheet操作),对比“原生 POI 写法 vs Hutool 写法”,让你用最少的代码搞定复杂 Excel 操作。

面试必备之乐观锁与悲观锁

一、先搞懂:ExcelUtil 为什么能替代原生 POI 冗余代码?

原生 POI 操作 Excel 时,80% 的代码都是“重复模板”(创建工作簿、遍历行/单元格、处理空值/类型转换),ExcelUtil 把这些模板封装为一行方法,核心优化如下:

原生 POI 痛点Hutool ExcelUtil 优化方案
导出需手动创建 Workbook/Sheet/RowExcelUtil.write() 一键生成 Excel,自动创建层级结构
导入需遍历行/单元格+类型转换ExcelUtil.read() 一键读取 Excel 到 List/Map,自动类型转换
大数据量读写易 OOMBigExcelWriter/BigExcelReader 基于 SAX 流式读写,避免 OOM
样式配置繁琐(字体/颜色/对齐)ExcelWriter 内置样式方法,一行定制表头/单元格样式
模板导出需手动填充占位符ExcelUtil.fillTemplate() 一键填充 Excel 模板,支持列表/单值填充
多sheet操作需切换Sheet对象ExcelWriter 简化多sheet创建/写入,无需手动切换
空值/单元格类型异常难处理自动处理空单元格、日期/数字类型转换,避免类型异常

简单说:ExcelUtil 不改变 POI 的核心能力,只是把“底层对象创建、遍历、类型转换、样式配置”等冗余操作封装成工具方法,让你聚焦“要导出/导入什么数据”,而非“怎么操作 Excel 底层对象”。

二、6 大实战场景:从“原生 POI 模板代码”到“一行 Hutool 调用”

按“使用频率”排序,每个场景结合真实业务(如订单导出、用户导入、大数据量报表),附对比示例。

1. 简单 Excel 导出(单sheet,列表转Excel)

核心场景:将用户列表、订单列表导出为 Excel(.xlsx/.xls),原生需手动创建 Workbook、Sheet、Row、Cell,代码冗余;ExcelUtil 一行搞定。

原生 POI 写法(约 40 行代码,冗余且易出错):
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.List;

public class NativeExcelWrite {
    // 测试实体:用户
    static class User {
        private Long id;
        private String name;
        private Integer age;
        public User(Long id, String name, Integer age) {this.id = id;this.name = name;this.age = age;}
        // getter/setter
        public Long getId() {return id;}
        public String getName() {return name;}
        public Integer getAge() {return age;}
    }

    public static void main(String[] args) throws Exception {
        // 1. 准备数据
        List<User> userList = new ArrayList<>();
        userList.add(new User(1L, "张三", 25));
        userList.add(new User(2L, "李四", 20));
        userList.add(new User(3L, "王五", 30));

        // 2. 手动创建Workbook、Sheet
        Workbook workbook = new XSSFWorkbook();
        Sheet sheet = workbook.createSheet("用户列表");
        // 3. 创建表头行
        Row headerRow = sheet.createRow(0);
        headerRow.createCell(0).setCellValue("ID");
        headerRow.createCell(1).setCellValue("姓名");
        headerRow.createCell(2).setCellValue("年龄");
        // 4. 遍历数据,创建数据行
        int rowNum = 1;
        for (User user : userList) {
            Row row = sheet.createRow(rowNum++);
            // 手动设置单元格值,处理类型转换
            row.createCell(0).setCellValue(user.getId());
            row.createCell(1).setCellValue(user.getName());
            row.createCell(2).setCellValue(user.getAge());
        }
        // 5. 写入文件,手动关闭流
        try (FileOutputStream fos = new FileOutputStream("user_export.xlsx")) {
            workbook.write(fos);
        }
        workbook.close(); // 手动关闭,避免内存泄漏
    }
}

问题:代码冗余(40 行仅实现简单导出);需手动处理流关闭,易泄漏;无空值处理(如 age 为 null 会抛异常);不支持大数据量(数据量>1万行易 OOM)。

Hutool 写法(一行导出,约 10 行代码):
import cn.hutool.poi.excel.ExcelUtil;
import cn.hutool.poi.excel.ExcelWriter;
import java.util.ArrayList;
import java.util.List;

public class HutoolExcelWrite {
    static class User {
        private Long id;
        private String name;
        private Integer age;
        public User(Long id, String name, Integer age) {this.id = id;this.name = name;this.age = age;}
        // getter/setter
        public Long getId() {return id;}
        public String getName() {return name;}
        public Integer getAge() {return age;}
    }

    public static void main(String[] args) {
        // 1. 准备数据
        List<User> userList = new ArrayList<>();
        userList.add(new User(1L, "张三", 25));
        userList.add(new User(2L, "李四", 20));
        userList.add(new User(3L, "王五", 30));

        // 2. 一键导出:自动创建Workbook/Sheet,自动处理类型/空值,自动关闭流
        try (ExcelWriter writer = ExcelUtil.getWriter("user_export.xlsx")) {
            // 写入数据(表头自动从User字段名生成,也可自定义)
            writer.write(userList, true);
        } // 自动关闭writer,无需手动关闭流
    }
}

核心优势

  • 自动创建层级:无需手动创建 Workbook/Sheet/Row,ExcelUtil 自动处理;
  • 空值安全:字段为 null 时单元格为空,不抛异常;
  • 自动关流:实现 AutoCloseable,try-with-resources 自动关闭,避免内存泄漏;
  • 表头自定义:writer.addHeaderAlias("id", "用户ID") 可将表头“id”改为“用户ID”。

业务场景:接口返回用户列表导出为 Excel(如运营后台的“用户导出”功能)。

2. 简单 Excel 导入(读取 Excel 到列表)

核心场景:读取 Excel 文件中的用户数据到 List,原生需遍历行/单元格,手动转换类型;ExcelUtil 一键读取。

原生 POI 写法(约 35 行代码,类型转换繁琐):
import org.apache.poi.ss.usermodel.*;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.List;

public class NativeExcelRead {
    static class User {
        private Long id;
        private String name;
        private Integer age;
        // 构造器、getter/setter
        public User(Long id, String name, Integer age) {this.id = id;this.name = name;this.age = age;}
    }

    public static void main(String[] args) throws Exception {
        List<User> userList = new ArrayList<>();
        // 1. 手动创建Workbook,读取文件
        try (FileInputStream fis = new FileInputStream("user_export.xlsx");
             Workbook workbook = WorkbookFactory.create(fis)) {
            // 2. 获取第一个Sheet
            Sheet sheet = workbook.getSheetAt(0);
            // 3. 遍历行(跳过表头行)
            for (int rowNum = 1; rowNum <= sheet.getLastRowNum(); rowNum++) {
                Row row = sheet.getRow(rowNum);
                if (row == null) continue;
                // 4. 手动读取单元格,转换类型(易出错)
                Long id = (long) row.getCell(0).getNumericCellValue();
                String name = row.getCell(1).getStringCellValue();
                Integer age = (int) row.getCell(2).getNumericCellValue();
                // 5. 添加到列表
                userList.add(new User(id, name, age));
            }
        }
        System.out.println("读取到用户数:" + userList.size());
    }
}

问题:类型转换繁琐(数字转 Long/Integer 易出错);空单元格处理易抛异常;日期/字符串混合单元格需手动判断类型。

Hutool 写法(一行读取,约 8 行代码):
import cn.hutool.poi.excel.ExcelUtil;
import cn.hutool.poi.excel.ExcelReader;
import java.util.List;

public class HutoolExcelRead {
    static class User {
        private Long id;
        private String name;
        private Integer age;
        // 无参构造器(必须)、getter/setter
        public User() {}
        // getter/setter
        public Long getId() {return id;}
        public void setId(Long id) {this.id = id;}
        public String getName() {return name;}
        public void setName(String name) {this.name = name;}
        public Integer getAge() {return age;}
        public void setAge(Integer age) {this.age = age;}
    }

    public static void main(String[] args) {
        // 1. 一键读取:自动跳过表头,转换为User列表,自动处理类型/空值
        try (ExcelReader reader = ExcelUtil.getReader("user_export.xlsx")) {
            // 读取所有数据到User列表(表头匹配字段名)
            List<User> userList = reader.readAll(User.class);
            System.out.println("读取到用户数:" + userList.size());
        }
    }
}

核心优势

  • 自动类型转换:数字→Long/Integer、日期→Date/LocalDateTime,无需手动处理;
  • 表头匹配:自动将表头(如“用户ID”)与实体字段(如 id)映射(可通过 addHeaderAlias 自定义);
  • 空值处理:空单元格对应字段为 null,不抛异常;
  • 灵活读取:支持读取指定行/列、读取为 Map 列表(无需实体类)。

业务场景:运营上传 Excel 批量导入用户数据(如“批量新增用户”功能)。

3. 大数据量 Excel 读写(避免 OOM,支持 10 万+ 行)

核心场景:导出/导入 10 万+ 行订单数据,原生 POI 因加载全部数据到内存易 OOM;ExcelUtil 的 BigExcelWriter/BigExcelReader 基于 SAX 流式读写,仅加载当前行数据,避免 OOM。

原生 POI 写法(需手动实现 SAX 解析,约 50 行代码):
// 原生POI大数据量导出(XSSFWorkbook 易OOM,需用 SXSSFWorkbook)
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.ss.usermodel.*;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.List;

public class NativeBigExcelWrite {
    static class Order {
        private Long id;
        private String orderNo;
        private Double amount;
        public Order(Long id, String orderNo, Double amount) {this.id = id;this.orderNo = orderNo;this.amount = amount;}
        // getter/setter
        public Long getId() {return id;}
        public String getOrderNo() {return orderNo;}
        public Double getAmount() {return amount;}
    }

    public static void main(String[] args) throws Exception {
        // 模拟10万条订单数据
        List<Order> orderList = new ArrayList<>();
        for (int i = 0; i < 100000; i++) {
            orderList.add(new Order((long) i, "ORDER" + i, 100.0 + i));
        }

        // 1. 创建SXSSFWorkbook(流式写入,仅缓存100行)
        SXSSFWorkbook workbook = new SXSSFWorkbook(100); // 超出100行自动刷盘
        Sheet sheet = workbook.createSheet("订单列表");
        // 2. 创建表头
        Row headerRow = sheet.createRow(0);
        headerRow.createCell(0).setCellValue("订单ID");
        headerRow.createCell(1).setCellValue("订单号");
        headerRow.createCell(2).setCellValue("金额");
        // 3. 流式写入数据
        int rowNum = 1;
        for (Order order : orderList) {
            Row row = sheet.createRow(rowNum++);
            row.createCell(0).setCellValue(order.getId());
            row.createCell(1).setCellValue(order.getOrderNo());
            row.createCell(2).setCellValue(order.getAmount());
        }
        // 4. 写入文件,手动刷盘/关闭
        try (FileOutputStream fos = new FileOutputStream("big_order.xlsx")) {
            workbook.write(fos);
        }
        workbook.dispose(); // 释放临时文件
        workbook.close();
    }
}

问题:需手动使用 SXSSFWorkbook,配置刷盘行数;代码冗余;导入需手动实现 SAX 解析,复杂度极高。

Hutool 写法(一键流式读写,约 12 行代码):
import cn.hutool.poi.excel.BigExcelWriter;
import cn.hutool.poi.excel.ExcelUtil;
import java.util.ArrayList;
import java.util.List;

public class HutoolBigExcelWrite {
    static class Order {
        private Long id;
        private String orderNo;
        private Double amount;
        public Order(Long id, String orderNo, Double amount) {this.id = id;this.orderNo = orderNo;this.amount = amount;}
        // getter/setter
        public Long getId() {return id;}
        public String getOrderNo() {return orderNo;}
        public Double getAmount() {return amount;}
    }

    public static void main(String[] args) {
        // 模拟10万条订单数据
        List<Order> orderList = new ArrayList<>();
        for (int i = 0; i < 100000; i++) {
            orderList.add(new Order((long) i, "ORDER" + i, 100.0 + i));
        }

        // 1. 创建BigExcelWriter(流式写入,自动避免OOM)
        try (BigExcelWriter writer = ExcelUtil.getBigWriter("big_order.xlsx")) {
            // 自定义表头
            writer.addHeaderAlias("id", "订单ID");
            writer.addHeaderAlias("orderNo", "订单号");
            writer.addHeaderAlias("amount", "金额");
            // 2. 流式写入数据(自动刷盘,仅缓存少量行)
            writer.write(orderList, true);
        } // 自动释放资源,无需手动dispose
    }
}

核心优势

  • 无需手动配置 SXSSFWorkbook:BigExcelWriter 内置流式写入,自动处理刷盘;
  • 低内存占用:仅加载当前行数据,10 万+ 行数据内存占用 < 100MB;
  • 导入同理:BigExcelReader 流式读取,一行代码读取大数据量 Excel。

业务场景:财务报表导出(10 万+ 行交易记录)、大数据量订单导入。

4. 带样式的 Excel 导出(自定义表头/单元格样式)

核心场景:导出的 Excel 需要定制样式(如表头红色、居中、加粗,金额列保留 2 位小数),原生 POI 样式配置繁琐;ExcelUtil 一行定制样式。

原生 POI 写法(样式配置冗余,约 40 行代码):
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.List;

public class NativeExcelStyle {
    static class User {
        private Long id;
        private String name;
        private Double score;
        public User(Long id, String name, Double score) {this.id = id;this.name = name;this.score = score;}
        // getter/setter
        public Long getId() {return id;}
        public String getName() {return name;}
        public Double getScore() {return score;}
    }

    public static void main(String[] args) throws Exception {
        List<User> userList = new ArrayList<>();
        userList.add(new User(1L, "张三", 95.5));
        userList.add(new User(2L, "李四", 88.0));

        Workbook workbook = new XSSFWorkbook();
        Sheet sheet = workbook.createSheet("成绩表");

        // 1. 自定义表头样式(加粗、红色、居中)
        CellStyle headerStyle = workbook.createCellStyle();
        Font headerFont = workbook.createFont();
        headerFont.setBold(true);
        headerFont.setColor(Font.COLOR_RED);
        headerStyle.setFont(headerFont);
        headerStyle.setAlignment(HorizontalAlignment.CENTER);

        // 2. 创建表头
        Row headerRow = sheet.createRow(0);
        Cell cell0 = headerRow.createCell(0);
        cell0.setCellValue("用户ID");
        cell0.setCellStyle(headerStyle);
        Cell cell1 = headerRow.createCell(1);
        cell1.setCellValue("姓名");
        cell1.setCellStyle(headerStyle);
        Cell cell2 = headerRow.createCell(2);
        cell2.setCellValue("成绩");
        cell2.setCellStyle(headerStyle);

        // 3. 自定义数据样式(成绩列保留2位小数)
        CellStyle dataStyle = workbook.createCellStyle();
        DataFormat format = workbook.createDataFormat();
        dataStyle.setDataFormat(format.getFormat("0.00"));

        // 4. 写入数据
        int rowNum = 1;
        for (User user : userList) {
            Row row = sheet.createRow(rowNum++);
            row.createCell(0).setCellValue(user.getId());
            row.createCell(1).setCellValue(user.getName());
            Cell scoreCell = row.createCell(2);
            scoreCell.setCellValue(user.getScore());
            scoreCell.setCellStyle(dataStyle); // 应用小数样式
        }

        // 5. 写入文件
        try (FileOutputStream fos = new FileOutputStream("score_style.xlsx")) {
            workbook.write(fos);
        }
        workbook.close();
    }
}

问题:样式配置代码占比 70%;每个单元格需手动应用样式;样式对象创建过多易导致 Excel 体积过大。

Hutool 写法(一行定制样式,约 15 行代码):
import cn.hutool.poi.excel.ExcelUtil;
import cn.hutool.poi.excel.ExcelWriter;
import cn.hutool.poi.excel.style.StyleUtil;
import org.apache.poi.ss.usermodel.*;
import java.util.ArrayList;
import java.util.List;

public class HutoolExcelStyle {
    static class User {
        private Long id;
        private String name;
        private Double score;
        public User(Long id, String name, Double score) {this.id = id;this.name = name;this.score = score;}
        // getter/setter
        public Long getId() {return id;}
        public String getName() {return name;}
        public Double getScore() {return score;}
    }

    public static void main(String[] args) {
        List<User> userList = new ArrayList<>();
        userList.add(new User(1L, "张三", 95.5));
        userList.add(new User(2L, "李四", 88.0));

        try (ExcelWriter writer = ExcelUtil.getWriter("score_style.xlsx")) {
            // 1. 自定义表头样式(加粗、红色、居中)
            CellStyle headerStyle = StyleUtil.createHeadStyle(writer.getWorkbook());
            headerStyle.getFont().setColor(Font.COLOR_RED); // 表头文字红色
            writer.setHeaderStyle(headerStyle);

            // 2. 自定义成绩列样式(保留2位小数)
            CellStyle scoreStyle = StyleUtil.createCellStyle(writer.getWorkbook());
            scoreStyle.setDataFormat(writer.getWorkbook().createDataFormat().getFormat("0.00"));
            writer.setStyle(2, scoreStyle); // 第3列(索引2)应用小数样式

            // 3. 自定义表头别名
            writer.addHeaderAlias("id", "用户ID");
            writer.addHeaderAlias("name", "姓名");
            writer.addHeaderAlias("score", "成绩");

            // 4. 写入数据
            writer.write(userList, true);
        }
    }
}

核心优势

  • 样式工具简化:StyleUtil 内置常用样式(表头样式、居中样式),无需手动创建 Font/CellStyle;
  • 批量应用样式:setStyle(columnIndex, style) 给整列应用样式,无需逐个单元格设置;
  • 样式复用:ExcelUtil 自动复用样式对象,减少 Excel 体积。

业务场景:财务报表导出(金额列保留 2 位小数、表头高亮)、成绩表导出(及格/不及格标色)。

5. 模板导出(基于 Excel 模板填充数据)

核心场景:基于预设的 Excel 模板(含占位符如 ${name}、列表占位符 {{$list}})填充数据,原生需手动查找占位符单元格并替换;ExcelUtil 一键填充。

模板文件(template.xlsx)内容:
字段
报表名称${reportName}
生成时间${createTime}
序号姓名
{{$list}}{{name}}
原生 POI 写法(手动查找占位符,约 40 行代码):
import org.apache.poi.ss.usermodel.*;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class NativeExcelTemplate {
    static class User {
        private String name;
        private Integer age;
        public User(String name, Integer age) {this.name = name;this.age = age;}
        // getter/setter
        public String getName() {return name;}
        public Integer getAge() {return age;}
    }

    public static void main(String[] args) throws Exception {
        // 1. 读取模板文件
        try (FileInputStream fis = new FileInputStream("template.xlsx");
             Workbook workbook = WorkbookFactory.create(fis)) {
            Sheet sheet = workbook.getSheetAt(0);

            // 2. 填充单值占位符
            for (Row row : sheet) {
                for (Cell cell : row) {
                    if (cell.getCellType() == CellType.STRING) {
                        String value = cell.getStringCellValue();
                        if ("${reportName}".equals(value)) {
                            cell.setCellValue("用户报表");
                        } else if ("${createTime}".equals(value)) {
                            cell.setCellValue(new Date().toString());
                        }
                    }
                }
            }

            // 3. 填充列表占位符(需手动插入行,复杂度极高)
            List<User> userList = new ArrayList<>();
            userList.add(new User("张三", 25));
            userList.add(new User("李四", 20));
            // 省略:手动查找{{$list}}位置,插入行,替换{{name}}/{{age}}...

            // 4. 写入文件
            try (FileOutputStream fos = new FileOutputStream("template_result.xlsx")) {
                workbook.write(fos);
            }
        }
    }
}

问题:列表占位符填充需手动插入行,逻辑复杂;占位符匹配易遗漏;不支持复杂数据结构(如嵌套对象)。

Hutool 写法(一键填充模板,约 10 行代码):
import cn.hutool.core.date.DateUtil;
import cn.hutool.poi.excel.ExcelUtil;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class HutoolExcelTemplate {
    static class User {
        private String name;
        private Integer age;
        public User(String name, Integer age) {this.name = name;this.age = age;}
        // getter/setter
        public String getName() {return name;}
        public Integer getAge() {return age;}
    }

    public static void main(String[] args) {
        // 1. 准备数据:单值+列表
        Map<String, Object> data = new HashMap<>();
        data.put("reportName", "用户报表");
        data.put("createTime", DateUtil.now());

        List<User> userList = new ArrayList<>();
        userList.add(new User("张三", 25));
        userList.add(new User("李四", 20));
        data.put("list", userList);

        // 2. 一键填充模板,生成结果文件
        ExcelUtil.fillTemplate("template.xlsx", data, "template_result.xlsx");
    }
}

核心优势

  • 自动匹配占位符:支持 ${单值}{{$list}} 列表占位符,无需手动查找;
  • 支持复杂数据:列表元素可为实体类/Map,自动匹配 {{字段名}}
  • 无需手动操作行:自动插入列表行,处理行高/样式继承。

业务场景:合同模板填充、报表模板导出(固定格式+动态数据)。

6. 多 Sheet 导出/导入

核心场景:导出/导入包含多个 Sheet 的 Excel(如“用户表”+“订单表”),原生需手动切换 Sheet;ExcelUtil 简化多 Sheet 操作。

Hutool 写法(多 Sheet 导出,约 15 行代码):
import cn.hutool.poi.excel.ExcelUtil;
import cn.hutool.poi.excel.ExcelWriter;
import java.util.ArrayList;
import java.util.List;

public class HutoolMultiSheet {
    static class User {private Long id;private String name;public User(Long id, String name) {this.id = id;this.name = name;}public Long getId() {return id;}public String getName() {return name;}}
    static class Order {private Long id;private String orderNo;public Order(Long id, String orderNo) {this.id = id;this.orderNo = orderNo;}public Long getId() {return id;}public String getOrderNo() {return orderNo;}}

    public static void main(String[] args) {
        // 准备数据
        List<User> userList = new ArrayList<>();
        userList.add(new User(1L, "张三"));
        List<Order> orderList = new ArrayList<>();
        orderList.add(new Order(1L, "ORDER1"));

        try (ExcelWriter writer = ExcelUtil.getWriter("multi_sheet.xlsx")) {
            // 1. 写入第一个Sheet(用户表)
            writer.setSheet("用户表");
            writer.addHeaderAlias("id", "用户ID");
            writer.write(userList, true);

            // 2. 切换到第二个Sheet(订单表)
            writer.setSheet("订单表");
            writer.addHeaderAlias("id", "订单ID");
            writer.write(orderList, true);
        }

        // 多Sheet导入:读取指定Sheet
        try (ExcelReader reader = ExcelUtil.getReader("multi_sheet.xlsx")) {
            // 读取“用户表”Sheet
            List<User> users = reader.setSheet("用户表").readAll(User.class);
            // 读取“订单表”Sheet
            List<Order> orders = reader.setSheet("订单表").readAll(Order.class);
            System.out.println("用户数:" + users.size() + ",订单数:" + orders.size());
        }
    }
}

核心优势

  • 一键切换 Sheet:setSheet(name/index) 无需手动创建/获取 Sheet;
  • 样式复用:不同 Sheet 可复用样式配置;
  • 导入灵活:支持按名称/索引读取指定 Sheet。

业务场景:综合报表导出(包含多个维度的数据 Sheet)。

三、避坑指南:这 6 个细节 90% 的人会踩

1. 大数据量导出未用 BigExcelWriter 导致 OOM

  • 误区:用 ExcelUtil.getWriter() 导出 10 万+ 行数据,以为自动处理 OOM;
  • 真相:普通 ExcelWriter 基于 XSSFWorkbook,大数据量需用 ExcelUtil.getBigWriter()
  • 解决:数据量 > 1000 行时,优先使用 BigExcelWriter

2. 导入实体类缺少无参构造器导致失败

  • 误区:导入到实体类时,实体类只有有参构造器,读取失败;
  • 真相:ExcelUtil 通过反射创建实体,必须有无参构造器(手动写或用 Lombok @NoArgsConstructor);
  • 解决:给导入的实体类添加无参构造器。

3. 日期格式解析异常

  • 误区:Excel 中的日期单元格读取为数字/字符串,转换失败;
  • 解决:① 手动指定日期格式:reader.setDateFormat("yyyy-MM-dd");② 实体类日期字段用 Date/LocalDateTime

4. 模板导出占位符匹配失败

  • 误区:模板中的占位符如 ${reportName} 有空格(如 ${ reportName }),导致匹配不到;
  • 真相:占位符前后不能有空格,需严格匹配 ${字段名}/{{字段名}}
  • 解决:清理模板占位符的多余空格。

5. 流未关闭导致文件被占用

  • 误区:未用 try-with-resources 包裹 ExcelWriter/Reader,导致文件句柄泄漏;
  • 解决:所有 ExcelWriter/Reader 都放在 try-with-resources 中,自动关闭流。

6. 表头别名与字段名不匹配

  • 误区:addHeaderAlias("userId", "用户ID"),但实体字段是 id,导致数据为空;
  • 真相:addHeaderAlias(实体字段名, 表头名),顺序不能反;
  • 解决:确认别名顺序为“字段名→表头名”,如 addHeaderAlias("id", "用户ID")

四、核心方法速查表(按场景分类)

场景工具类方法示例
简单 Excel 导出ExcelUtil.getWriter()ExcelUtil.getWriter("file.xlsx").write(list, true)
大数据量导出ExcelUtil.getBigWriter()ExcelUtil.getBigWriter("big_file.xlsx").write(list, true)
简单 Excel 导入ExcelUtil.getReader()ExcelUtil.getReader("file.xlsx").readAll(User.class)
大数据量导入ExcelUtil.getBigReader()ExcelUtil.getBigReader("big_file.xlsx").readRow(rowHandler)
模板导出ExcelUtil.fillTemplate()ExcelUtil.fillTemplate("template.xlsx", data, "result.xlsx")
多 Sheet 切换ExcelWriter/Reader.setSheet()writer.setSheet("用户表")
表头别名自定义ExcelWriter.addHeaderAlias()writer.addHeaderAlias("id", "用户ID")
样式定制StyleUtil.createHeadStyle()writer.setHeaderStyle(StyleUtil.createHeadStyle(workbook))

总结

Hutool ExcelUtil 的核心价值是**“封装 POI 的底层模板代码,解决 Excel 操作的 OOM、类型转换、样式配置、模板填充等痛点”**,它让 Excel 操作从“几十行模板代码”简化为“一行核心调用”,同时兼容原生 POI 的所有能力(如复杂样式、自定义解析)。

核心用法记住 3 点:

  1. 导出:小数据量用 ExcelUtil.getWriter(),大数据量用 getBigWriter()
  2. 导入:实体类必须有无参构造器,日期字段指定格式;
  3. 模板:占位符严格匹配,列表用 {{$list}} + {{字段名}}

更多推荐