EasyExcel重磅发布!新一代Java Excel处理引擎让百万级数据处理如丝般顺滑

EasyExcel重磅发布!新一代Java Excel处理引擎让百万级数据处理如丝般顺滑

【免费下载链接】easyexcel 快速、简洁、解决大文件内存溢出的java处理Excel工具 【免费下载链接】easyexcel 项目地址: https://gitcode.com/gh_mirrors/ea/easyexcel

引言:你还在为Excel处理内存溢出烦恼吗?

在企业级应用开发中,Excel文件处理是一项常见且关键的任务。无论是数据导入导出、报表生成还是数据分析,都离不开高效可靠的Excel处理工具。然而,传统的Java Excel处理框架如Apache POI往往面临着严重的内存占用问题,一个几十兆的Excel文件可能需要数百兆甚至数G的内存才能处理,这不仅影响系统性能,还常常导致内存溢出(OOM)错误。

EasyExcel作为阿里巴巴开源的一款高性能Excel处理引擎,彻底改变了这一局面。最新发布的4.0.2版本在保持低内存占用优势的基础上,进一步提升了处理速度和功能丰富度,为Java开发者提供了一个既高效又易用的Excel处理解决方案。

读完本文,你将能够:

  • 了解EasyExcel相比传统Excel处理工具的核心优势
  • 掌握EasyExcel的基本使用方法和高级特性
  • 学会在各种场景下优化Excel处理性能
  • 解决百万级数据量Excel文件的导入导出难题
  • 理解EasyExcel的底层工作原理

一、EasyExcel:重新定义Java Excel处理

1.1 传统Excel处理工具的痛点

Java生态中,Apache POI和JXL是两款主流的Excel处理框架,但它们都存在一个严重的问题——内存占用过大。以下是一个简单的对比:

框架3M Excel文件解析内存占用75M Excel文件解析内存占用处理46万行数据耗时
Apache POI(普通模式)100-200MB1-2GB超过5分钟
Apache POI(SAX模式)50-100MB500MB-1GB2-3分钟
EasyExcel(标准模式)5-10MB16MB23秒
EasyExcel(极速模式)30-50MB100-150MB10秒以内

传统框架之所以内存占用大,是因为它们在处理Excel文件时会将整个文件内容加载到内存中,尤其是对于xlsx格式的文件,解压缩和存储都在内存中完成。这使得处理大型Excel文件时很容易出现OOM错误。

1.2 EasyExcel的革命性突破

EasyExcel通过重写POI对07版Excel的解析逻辑,实现了内存占用的指数级降低。其核心创新点包括:

  1. 增量解析:不将整个文件加载到内存,而是逐行读取、逐行处理
  2. 低内存模型:优化数据结构,最小化内存占用
  3. SAX解析模式:基于事件驱动的解析方式,避免DOM树构建
  4. 零反射调用:减少反射带来的性能损耗和内存占用

这些优化使得EasyExcel能够用16MB内存,在23秒内读取75M(46万行25列)的Excel文件,而开启极速模式后,耗时可以进一步缩短到10秒以内(内存占用会增加到100MB左右)。

mermaid

二、快速上手:5分钟掌握EasyExcel

2.1 引入依赖

使用EasyExcel非常简单,首先在你的Maven项目中引入依赖:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>4.0.2</version>
</dependency>

2.2 基本读取Excel

EasyExcel的API设计简洁直观,下面是一个基本的Excel读取示例:

// 定义数据模型
@Data
public class DemoData {
    @ExcelProperty("字符串标题")
    private String string;
    @ExcelProperty("日期标题")
    private Date date;
    @ExcelProperty("数字标题")
    private Double doubleData;
}

// 定义数据监听器
public class DemoDataListener implements ReadListener<DemoData> {
    private List<DemoData> list = new ArrayList<>();
    
    @Override
    public void invoke(DemoData data, AnalysisContext context) {
        list.add(data);
        // 每1000条数据处理一次,避免内存占用过大
        if (list.size() >= 1000) {
            processData();
            list.clear();
        }
    }
    
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        // 处理剩余数据
        processData();
    }
    
    private void processData() {
        // 实际业务处理逻辑
        System.out.println("处理了" + list.size() + "条数据");
    }
}

// 读取Excel文件
public class SimpleReadDemo {
    public static void main(String[] args) {
        String fileName = "demo.xlsx";
        // 这里需要指定读用哪个class去读,然后读取第一个sheet
        EasyExcel.read(fileName, DemoData.class, new DemoDataListener())
                 .sheet()
                 .doRead();
    }
}

2.3 基本写入Excel

写入Excel同样简单:

// 数据模型与读取时相同,使用@ExcelProperty注解映射列

public class SimpleWriteDemo {
    public static void main(String[] args) {
        // 生成输出文件名
        String fileName = "output" + System.currentTimeMillis() + ".xlsx";
        
        // 写入数据
        EasyExcel.write(fileName, DemoData.class)
                 .sheet("模板")  // 指定sheet名称
                 .doWrite(data());  // 写入数据列表
    }
    
    // 生成测试数据
    private static List<DemoData> data() {
        List<DemoData> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            DemoData data = new DemoData();
            data.setString("字符串" + i);
            data.setDate(new Date());
            data.setDoubleData(0.56 + i);
            list.add(data);
        }
        return list;
    }
}

2.4 Web环境中的应用

在Web开发中,经常需要实现Excel文件的上传和下载功能,EasyExcel对此提供了良好的支持:

@RestController
public class ExcelController {
    
    /**
     * 文件下载
     */
    @GetMapping("download")
    public void download(HttpServletResponse response) throws IOException {
        // 设置响应头
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        // 处理中文文件名
        String fileName = URLEncoder.encode("测试数据", "UTF-8").replaceAll("\\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
        
        // 直接写入响应流
        EasyExcel.write(response.getOutputStream(), DownloadData.class)
                 .sheet("模板")
                 .doWrite(data());
    }
    
    /**
     * 文件上传
     */
    @PostMapping("upload")
    @ResponseBody
    public String upload(@RequestParam("file") MultipartFile file) throws IOException {
        EasyExcel.read(file.getInputStream(), UploadData.class, new UploadDataListener())
                 .sheet()
                 .doRead();
        return "success";
    }
}

三、核心特性解析

3.1 注解驱动开发

EasyExcel提供了一系列注解,简化Excel与Java对象之间的映射关系:

注解作用
@ExcelProperty指定当前字段对应excel中的哪一列,可以根据名字或Index匹配
@ExcelIgnore忽略该字段,不参与Excel读写
@DateTimeFormat日期转换格式,参照java.text.SimpleDateFormat
@NumberFormat数字转换格式,参照java.text.DecimalFormat
@ExcelIgnoreUnannotated忽略未添加@ExcelProperty注解的字段

示例:

@Data
@ExcelIgnoreUnannotated // 忽略未添加@ExcelProperty的字段
public class ComplexData {
    @ExcelProperty(index = 0, value = "姓名") // 指定索引和列名
    private String name;
    
    @ExcelProperty("年龄") // 只指定列名
    private Integer age;
    
    @ExcelProperty("生日")
    @DateTimeFormat("yyyy-MM-dd HH:mm:ss") // 日期格式化
    private Date birthday;
    
    @ExcelProperty("工资")
    @NumberFormat("#,###.00") // 数字格式化
    private Double salary;
    
    private String address; // 会被忽略,因为添加了@ExcelIgnoreUnannotated注解
}

3.2 事件监听机制

EasyExcel采用事件驱动模型处理数据,通过ReadListener接口可以在数据读取过程中进行处理:

public interface ReadListener<T> {
    // 每读取一行数据都会调用此方法
    void invoke(T data, AnalysisContext context);
    
    // 所有数据读取完成后调用
    void doAfterAllAnalysed(AnalysisContext context);
    
    // 读取表头时调用
    default void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {}
    
    // 配置是否自动关闭输入流
    default boolean needAutoCloseStream() { return true; }
    
    // 异常处理
    default void onException(Exception exception, AnalysisContext context) throws Exception {
        throw exception;
    }
}

自定义监听器时,可以实现增量处理逻辑,避免一次性加载过多数据到内存:

public class BatchProcessListener<T> implements ReadListener<T> {
    private List<T> batchList = new ArrayList<>(1000); // 批处理大小
    private int batchSize = 1000;
    private final Consumer<List<T>> processAction;
    
    public BatchProcessListener(Consumer<List<T>> processAction) {
        this.processAction = processAction;
    }
    
    public BatchProcessListener(int batchSize, Consumer<List<T>> processAction) {
        this.batchSize = batchSize;
        this.batchList = new ArrayList<>(batchSize);
        this.processAction = processAction;
    }
    
    @Override
    public void invoke(T data, AnalysisContext context) {
        batchList.add(data);
        if (batchList.size() >= batchSize) {
            processAction.accept(batchList);
            batchList.clear();
        }
    }
    
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        if (!batchList.isEmpty()) {
            processAction.accept(batchList);
        }
    }
}

// 使用方式
EasyExcel.read(fileName, DemoData.class, new BatchProcessListener<>(dataList -> {
    // 处理一批数据
    System.out.println("处理了" + dataList.size() + "条数据");
})).sheet().doRead();

3.3 灵活的配置体系

EasyExcel的所有配置都是继承的,Workbook的配置会被Sheet继承。这种设计使得配置更加灵活:

// Workbook级别的配置会被所有Sheet继承
ExcelReaderBuilder readerBuilder = EasyExcel.read(fileName, DemoData.class)
    .autoTrim(true) // 自动trim字符串
    .converter(new CustomConverter()); // 自定义转换器

// Sheet级别配置,会覆盖Workbook的同名配置
readerBuilder.sheet()
    .headRowNumber(2) // 指定表头行数为2
    .registerReadListener(new DemoDataListener())
    .doRead();

// 多Sheet读取
readerBuilder.sheet(0).doRead(); // 读取第一个Sheet
readerBuilder.sheet("Sheet2").doRead(); // 读取名为"Sheet2"的Sheet

3.4 数据转换与格式化

EasyExcel内置了丰富的数据转换器,支持各种Java类型与Excel单元格数据之间的转换。同时,你也可以自定义转换器:

// 自定义转换器示例
public class CustomDateConverter implements Converter<LocalDateTime> {
    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    
    @Override
    public Class<LocalDateTime> supportJavaTypeKey() {
        return LocalDateTime.class;
    }
    
    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return CellDataTypeEnum.STRING;
    }
    
    @Override
    public LocalDateTime convertToJavaData(ReadConverterContext<?> context) {
        String cellValue = context.getReadCellData().getStringValue();
        if (StringUtils.isEmpty(cellValue)) {
            return null;
        }
        return LocalDateTime.parse(cellValue, FORMATTER);
    }
    
    @Override
    public WriteCellData<?> convertToExcelData(WriteConverterContext<LocalDateTime> context) {
        LocalDateTime value = context.getValue();
        if (value == null) {
            return new WriteCellData<>("");
        }
        return new WriteCellData<>(FORMATTER.format(value));
    }
}

// 使用自定义转换器
EasyExcel.write(fileName, CustomData.class)
    .converter(new CustomDateConverter())
    .sheet()
    .doWrite(data());

四、性能优化:从优秀到卓越

4.1 极速模式

EasyExcel提供了极速模式,通过牺牲少量内存换取更高的处理速度:

// 开启极速模式
EasyExcel.read(fileName, DemoData.class, new DemoDataListener())
    .excelType(ExcelTypeEnum.XLSX)
    .autoCloseStream(true)
    .useDefaultListener(false)
    .sheet()
    .headRowNumber(1)
    .doRead();

极速模式的原理是减少对象创建和垃圾回收,通过预分配内存和复用对象来提高性能。适用于对处理速度要求高,且内存资源相对充足的场景。

4.2 大数据量处理策略

对于百万级甚至千万级数据量的Excel处理,需要结合以下策略:

  1. 分批处理:设置合理的批处理大小,避免一次性加载过多数据
  2. 异步处理:将数据处理逻辑异步化,避免阻塞IO
  3. 多线程处理:利用多线程并行处理不同Sheet或数据块
  4. 数据库批量插入:使用JDBC批量插入代替单条插入
// 多线程处理示例
public class MultiThreadListener extends AnalysisEventListener<DemoData> {
    private final BlockingQueue<List<DemoData>> queue = new ArrayBlockingQueue<>(10);
    private final List<Thread> workers = new ArrayList<>();
    private List<DemoData> batchList = new ArrayList<>(1000);
    
    public MultiThreadListener(int threadCount) {
        // 创建处理线程池
        for (int i = 0; i < threadCount; i++) {
            Thread worker = new Thread(() -> {
                try {
                    while (true) {
                        List<DemoData> dataList = queue.take();
                        if (dataList == null) { // 结束标志
                            break;
                        }
                        processData(dataList); // 处理数据
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
            worker.start();
            workers.add(worker);
        }
    }
    
    @Override
    public void invoke(DemoData data, AnalysisContext context) {
        batchList.add(data);
        if (batchList.size() >= 1000) {
            queue.offer(new ArrayList<>(batchList)); // 放入队列
            batchList.clear();
        }
    }
    
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        try {
            if (!batchList.isEmpty()) {
                queue.offer(new ArrayList<>(batchList));
            }
            // 发送结束标志
            for (Thread worker : workers) {
                queue.offer(null);
            }
            // 等待所有工作线程完成
            for (Thread worker : workers) {
                worker.join();
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
    
    private void processData(List<DemoData> dataList) {
        // 数据处理逻辑
    }
}

4.3 内存占用优化

除了使用EasyExcel本身,还可以通过以下JVM参数进一步优化内存占用:

-Xms16m -Xmx64m -XX:+UseG1GC -XX:MaxGCPauseMillis=200

这些参数设置较小的堆内存并使用G1垃圾收集器,可以在保证性能的同时,最小化内存占用。

五、高级应用场景

5.1 复杂表头处理

EasyExcel支持复杂的多级表头:

@Data
public class ComplexHeadData {
    @ExcelProperty({"主标题", "字符串标题"})
    private String string;
    
    @ExcelProperty({"主标题", "日期标题"})
    private Date date;
    
    @ExcelProperty({"主标题", "数字标题"})
    private Double doubleData;
}

// 写入复杂表头Excel
String fileName = "complexHead" + System.currentTimeMillis() + ".xlsx";
EasyExcel.write(fileName, ComplexHeadData.class)
         .sheet("复杂表头")
         .doWrite(data());

生成的Excel表头效果如下:

主标题
字符串标题日期标题数字标题
实际数据实际数据实际数据

5.2 Excel模板填充

EasyExcel支持基于模板的Excel生成,这在制作报表时非常有用:

// 模板填充示例
String templateFileName = "template.xlsx";
String fileName = "fill" + System.currentTimeMillis() + ".xlsx";

// 方案1: 简洁写法(推荐)
EasyExcel.write(fileName, FillData.class)
         .withTemplate(templateFileName)
         .sheet()
         .doFill(data());

// 方案2: 分多次填充,会使用文件缓存(推荐)
ExcelWriter excelWriter = EasyExcel.write(fileName).withTemplate(templateFileName).build();
WriteSheet writeSheet = EasyExcel.writerSheet().build();

// 第一次填充
excelWriter.fill(data1(), writeSheet);
// 第二次填充
FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
excelWriter.fill(data2(), fillConfig, writeSheet);

excelWriter.finish();

5.3 样式自定义

EasyExcel提供了丰富的样式自定义功能,可以通过WriteHandler接口实现:

// 自定义单元格样式
public class CustomCellStyleHandler 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) {
        // 单元格创建后操作
    }
    
    @Override
    public void afterCellDataConverted(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder,
                                       CellData cellData, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
        // 单元格数据转换后操作
    }
    
    @Override
    public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder,
                                 List<CellData> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
        // 设置表头样式
        if (isHead) {
            Workbook workbook = writeSheetHolder.getWorkbook();
            CellStyle cellStyle = workbook.createCellStyle();
            Font font = workbook.createFont();
            font.setBold(true); // 加粗
            font.setFontHeightInPoints((short) 12); // 字体大小
            cellStyle.setFont(font);
            cellStyle.setFillForegroundColor(IndexedColors.LIGHT_BLUE.getIndex()); // 背景色
            cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
            cell.setCellStyle(cellStyle);
        }
    }
}

// 使用自定义样式
EasyExcel.write(fileName, DemoData.class)
         .registerWriteHandler(new CustomCellStyleHandler())
         .sheet("模板")
         .doWrite(data());

六、EasyExcel架构深度剖析

6.1 核心接口与类

EasyExcel的核心架构基于以下几个关键组件:

  1. ExcelReader/ExcelWriter:Excel读写的入口类
  2. ExcelAnalyser:Excel解析器,协调整个解析过程
  3. ExcelReadExecutor:Excel读取执行器,负责具体的读取逻辑
  4. AnalysisContext:解析上下文,保存解析过程中的状态信息
  5. ReadListener:读取监听器,处理解析得到的数据
  6. Converter:数据转换器,负责Excel数据与Java对象之间的转换

mermaid

6.2 工作流程解析

EasyExcel的读取流程可以概括为:

  1. 创建ExcelReader实例,指定文件、数据模型和监听器
  2. 构建AnalysisContext上下文对象,保存解析过程中的状态
  3. 根据Excel类型(xls或xlsx)创建相应的ExcelReadExecutor
  4. 执行解析,通过SAX事件驱动方式逐行读取Excel内容
  5. 每解析一行数据,通过ReadListener回调处理数据
  6. 所有数据解析完成后,调用doAfterAllAnalysed方法
  7. 释放资源,完成整个解析过程

mermaid

七、最佳实践与常见问题

7.1 性能优化 checklist

  •  使用最新版本的EasyExcel,通常包含性能优化
  •  合理设置批处理大小,避免频繁IO或内存溢出
  •  必要时开启极速模式,平衡内存和速度
  •  避免在监听器中进行复杂计算或网络请求
  •  使用自定义转换器代替默认转换器,减少不必要的处理
  •  大数据量时使用异步处理和多线程
  •  导出大量数据时考虑分页或流式处理

7.2 常见问题解决方案

Q1: 如何处理Excel中的合并单元格?

A1: EasyExcel提供了@ContentLoopMerge@HeadLoopMerge注解处理合并单元格:

@Data
public class MergeData {
    @ExcelProperty("姓名")
    private String name;
    
    @ExcelProperty("部门")
    @ContentLoopMerge(eachRow = 2) // 内容行每2行合并
    private String department;
    
    @ExcelProperty("工号")
    private String id;
}
Q2: 如何读取Excel中的图片?

A2: 通过ReadListenerinvokeHead方法可以获取图片数据:

public class ImageReadListener implements ReadListener<DemoData> {
    private List<ImageData> images = new ArrayList<>();
    
    @Override
    public void invoke(DemoData data, AnalysisContext context) {
        // 处理数据
    }
    
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        // 处理图片
        for (ImageData image : images) {
            byte[] imageData = image.getData();
            // 保存图片等操作
        }
    }
    
    @Override
    public void extra(CellExtra extra, AnalysisContext context) {
        if (extra.getType() == CellExtraTypeEnum.IMAGE) {
            images.add((ImageData) extra);
        }
    }
}
Q3: 如何实现Excel文件的加密和解密?

A3: EasyExcel支持读写加密Excel文件:

// 读取加密Excel
EasyExcel.read(fileName, DemoData.class, new DemoDataListener())
         .password("password")
         .sheet()
         .doRead();

// 写入加密Excel
EasyExcel.write(fileName, DemoData.class)
         .password("password")
         .sheet()
         .doWrite(data());

八、总结与展望

EasyExcel作为一款高性能的Java Excel处理引擎,通过创新的内存优化技术,彻底解决了传统Excel处理工具内存占用过大的问题。其简洁易用的API设计和丰富的功能特性,使得开发者可以轻松应对各种Excel处理场景。

无论是简单的Excel导入导出,还是百万级数据量的批量处理,EasyExcel都能提供卓越的性能表现。16MB内存处理75MB Excel文件的能力,让Java开发者不再为Excel处理的性能问题而烦恼。

随着数据量的不断增长和业务复杂度的提升,EasyExcel团队也在持续优化和创新。未来,我们可以期待更多高级特性的加入,如更智能的内存管理、更丰富的数据可视化功能以及更强大的数据分析能力。

如果你还在为Excel处理的性能问题而困扰,不妨尝试一下EasyExcel,相信它会给你带来惊喜!

附录:学习资源

  • 官方文档:https://easyexcel.opensource.alibaba.com/
  • GitHub仓库:https://gitcode.com/gh_mirrors/ea/easyexcel
  • 常见问题:https://easyexcel.opensource.alibaba.com/docs/qa/
  • API文档:https://easyexcel.opensource.alibaba.com/docs/current/api/

如果你觉得EasyExcel对你的工作有帮助,请给项目点个Star支持一下开源事业!

【免费下载链接】easyexcel 快速、简洁、解决大文件内存溢出的java处理Excel工具 【免费下载链接】easyexcel 项目地址: https://gitcode.com/gh_mirrors/ea/easyexcel

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值