基于EasyExcel实现Excel导出功能

目录

介绍

快速使用

分页查询并导出

官网参照

分页导出


介绍

EasyExcel 是阿里巴巴开源的一个基于 Java 的简单的、节省内存的 Excel 处理工具, Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。 easyexcel重写了poi对07版Excel的解析,一个3M的excel用POI sax解析依然需要100M左右内存,改用easyexcel可以降低到几M,并且再大的excel也不会出现内存溢出;03版依赖POI的sax模式,在上层做了模型转换的封装,让使用者更加简单方便。

本篇主要讲的是 EasyExcel 的导出功能,即写入功能,且分为单次写入和多次写入,想要了解更多可参考官方网站:EasyExcel官方网站

快速使用

  • 依赖引入
XML 复制代码
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>easyexcel</artifactId>
  <version>3.1.1</version>
</dependency>
  • 示例代码
java 复制代码
    /**
         * 
         * 结合官方提供资料完成快速开始
         * 文件下载(失败了会返回一个有部分数据的Excel)
         * <p>
         * 1. 创建excel对应的实体对象
         * <p>
         * 2. 设置返回的 参数
         * <p>
         * 3. 直接写,这里注意,finish的时候会自动关闭OutputStream,当然你外面再关闭流问题不大
         */
    @GetMapping("/ff/export")
    public void exportExcel(HttpServletResponse response) throws IOException {
        // 设置响应格式(Excel)
        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(), User.class)
                .sheet("测试用户列表") 
                .doWrite(getExportData()); 
    }

    /**
     * 生成导出的模拟数据
     * @return
     */
    public List<User> getExportData() {
        List<User> userList = new ArrayList<>();
        
        for (int i = 1; i <= 100; i++) {
            User user = new User();
            user.setName("用户" + i);
            user.setAge(20 + new Random().nextInt(30)); // 年龄20-50岁
            user.setEmail("user" + i + "@XX.com");
            userList.add(user);
        }
        
        return userList;
    }

    /**
     * 
     * Excel映射实体
     */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
    private static final long serialVersionUID = 1L;
	
    @ExcelProperty(value = "姓名", index = 0)
    private String name;

    @ExcelProperty(value = "年龄", index = 1)
    private Integer age;

    @ExcelProperty(value = "邮箱", index = 2)
    private String email;
}

分页查询并导出

官网参照

重复多次写入(写到单个或者多个 Sheet)中

java 复制代码
/**
     * 重复多次写入
     * <p>
     * 1. 创建excel对应的实体对象 参照{@link ComplexHeadData}
     * <p>
     * 2. 使用{@link ExcelProperty}注解指定复杂的头
     * <p>
     * 3. 直接调用二次写入即可
     */
    @Test
    public void repeatedWrite() {
        // 方法1: 如果写到同一个sheet
        String fileName = TestFileUtil.getPath() + "repeatedWrite" + System.currentTimeMillis() + ".xlsx";
        // 这里 需要指定写用哪个class去写
        try (ExcelWriter excelWriter = EasyExcel.write(fileName, DemoData.class).build()) {
            // 这里注意 如果同一个sheet只要创建一次
            WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
            // 去调用写入,这里我调用了五次,实际使用时根据数据库分页的总的页数来
            for (int i = 0; i < 5; i++) {
                // 分页去数据库查询数据 这里可以去数据库查询每一页的数据
                List<DemoData> data = data();
                excelWriter.write(data, writeSheet);
            }
        }

        // 方法2: 如果写到不同的sheet 同一个对象
        fileName = TestFileUtil.getPath() + "repeatedWrite" + System.currentTimeMillis() + ".xlsx";
        // 这里 指定文件
        try (ExcelWriter excelWriter = EasyExcel.write(fileName, DemoData.class).build()) {
            // 去调用写入,这里我调用了五次,实际使用时根据数据库分页的总的页数来。这里最终会写到5个sheet里面
            for (int i = 0; i < 5; i++) {
                // 每次都要创建writeSheet 这里注意必须指定sheetNo 而且sheetName必须不一样
                WriteSheet writeSheet = EasyExcel.writerSheet(i, "模板" + i).build();
                // 分页去数据库查询数据 这里可以去数据库查询每一页的数据
                List<DemoData> data = data();
                excelWriter.write(data, writeSheet);
            }
        }

        // 方法3 如果写到不同的sheet 不同的对象
        fileName = TestFileUtil.getPath() + "repeatedWrite" + System.currentTimeMillis() + ".xlsx";
        // 这里 指定文件
        try (ExcelWriter excelWriter = EasyExcel.write(fileName).build()) {
            // 去调用写入,这里我调用了五次,实际使用时根据数据库分页的总的页数来。这里最终会写到5个sheet里面
            for (int i = 0; i < 5; i++) {
                // 每次都要创建writeSheet 这里注意必须指定sheetNo 而且sheetName必须不一样。这里注意DemoData.class 可以每次都变,我这里为了方便 所以用的同一个class
                // 实际上可以一直变
                WriteSheet writeSheet = EasyExcel.writerSheet(i, "模板" + i).head(DemoData.class).build();
                // 分页去数据库查询数据 这里可以去数据库查询每一页的数据
                List<DemoData> data = data();
                excelWriter.write(data, writeSheet);
            }
        }

    }

分页导出

  • 模拟数据导出
  • 示例代码(主要是实现写到不同的sheet 同一个对象这种方式)
java 复制代码
package com.ffliuli.business;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.ffliuli.entity.User;
import org.springframework.util.CollectionUtils;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
 *
 * 分页读取数据可避免OOM的发生
 */
@Service
public class BigDataExportBusiness {

    private static final int MAX_ROWS_SHEET = 1000000;
	
    private static final int PAGE_SIZE = 3000;
	
    private static final long TOTAL_DATA_COUNT = 3000000; 

    /**
     * 导出
     */
    public void exportBigData(HttpServletResponse response) throws IOException {
		
        setExcelResponseHeader(response, "300万用户列表测试");

        try (OutputStream outputStream = response.getOutputStream()) {
            // 初始化ExcelWriter,绑定输出流和数据模型
            ExcelWriter excelWriter = EasyExcel.write(outputStream, User.class).build();
            // 初始化Sheet参数,Sheet序号
            int currentSheetNo = 0;
            // 当前Sheet已写入行数
            int currentSheetRowCount = 0;
            WriteSheet currentWriteSheet = createWriteSheet(currentSheetNo, "用户列表-" + (currentSheetNo + 1));

            int pageNum = 1; 
            while (true) {

                List<User> pageDataList = getPageUserData(pageNum, PAGE_SIZE);
                if (CollectionUtils.isEmpty(pageDataList)) {
                    break; 
                }
                // 检查当前Sheet是否已满,满则创建新Sheet
                if (currentSheetRowCount + pageDataList.size() > MAX_ROWS_SHEET) {
					
                    currentSheetNo++;
					
                    currentWriteSheet = createWriteSheet(currentSheetNo, "用户列表-" + (currentSheetNo + 1));
					
                    currentSheetRowCount = 0;
                }

                excelWriter.write(pageDataList, currentWriteSheet);

                currentSheetRowCount += pageDataList.size();
                // 进入下一页查询
                pageNum++;
            }
            
            // 完成写入并释放资源
            excelWriter.finish();
        }
    }

    /**
     *
     * 设置Excel下载响应头
     */
    private void setExcelResponseHeader(HttpServletResponse response, String fileName) throws IOException {
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name())
                .replaceAll("\\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + encodedFileName + ".xlsx");
        response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
        response.setHeader("Pragma", "no-cache");
    }

    /**
     *
     * 创建新的WriteSheet
     */
    private WriteSheet createWriteSheet(int sheetNo, String sheetName) {
        return EasyExcel.writerSheet(sheetNo, sheetName).build();
    }

    /**
     *
     * 模拟数据库数据
     */
    private List<User> getPageUserData(int pageNum, int pageSize) {
        List<User> userList = new ArrayList<>(pageSize);
        Random random = new Random();

        // 计算当前页数据的起止索引
        long startIndex = (pageNum - 1L) * pageSize + 1;
        long endIndex = Math.min(startIndex + pageSize - 1, TOTAL_DATA_COUNT);

        if (startIndex > TOTAL_DATA_COUNT) {
            return userList;
        }

        // 生成当前页模拟数据
        for (long i = startIndex; i <= endIndex; i++) {
            User user = new User();
            user.setName("用户" + i);
            user.setAge(20 + random.nextInt(30)); 
            user.setEmail("user" + i + "@XX.com");
            userList.add(user);
        }
        return userList;
    }
}
// 以上代码仅仅是示例代码可对其进行扩展,如加上前端请求时,传输过来的条件进行分页查询后再写入excel进而导出。
// 要使其更加贴合实战项目需进行升级改造,可从不同场景进行切入。

内存使用情况

结尾

总之,基于 EasyExcel 实现 Excel 导出功能的技术分享就完整收尾了。本文从 EasyExcel 的核心价值切入 ------ 相较于 Apache POI、jxl 等传统框架,它通过重写 07 版 Excel 解析逻辑,将 3M 文件的内存占用从 100M 级降至几 M,彻底解决了大数据导出的 OOM 痛点;随后逐步落地实战:从引入 3.1.1 版本依赖、定义带@ExcelProperty注解的 User 映射实体,到编写单次导出接口(含响应格式配置、中文文件名编码处理),再到深入分页导出的三种核心场景(同 Sheet 多次写入、不同 Sheet 同对象写入、不同 Sheet 不同对象写入),最后通过 300 万用户数据导出的案例,演示了如何用 "分页查询(每页 3000 条)+ 单 Sheet 百万行上限拆分" 实现高效稳定的大数据导出,且从监控数据可见,节省堆内存,CPU 消耗极低。

在实际项目中落地时,建议根据数据量级灵活选择方案:千级以内小数据量可直接用单次写入简化代码;百万级大数据务必优先采用分页查询 + 多 Sheet 拆分的模式,同时保留setExcelResponseHeader中的缓存控制配置,避免浏览器下载异常。如果遇到复杂表头、单元格合并、数据格式自定义等场景,或者对文中分页逻辑有更优优化思路,欢迎在评论区留言交流 。

读者后续可以继续拓展 EasyExcel 的相关内容,比如 Excel 导入功能、异常重试机制、复杂模板填充等实战场景。若本文对你的开发有帮助,不妨点赞收藏。感谢你的耐心阅读,技术之路漫长,同行者常伴,期待与你下次交流~~~

相关推荐
悟能不能悟6 小时前
idea运行tomcat的日志文件放到哪里了
java·tomcat·intellij-idea
hixiong1236 小时前
C# OpencvSharp使用lpd_yunet进行车牌检测
开发语言·opencv·计算机视觉·c#
Lj2_jOker6 小时前
QT 给Qimage数据赋值,显示异常,像素对齐的坑
开发语言·前端·qt
吴名氏.6 小时前
细数Java中List的10个坑
java·开发语言·数据结构·list
初学者,亦行者6 小时前
Rayon并行迭代器:原理、实践与性能优化
java·开发语言·spring·rust
我想进大厂7 小时前
Python---数据容器(Set 集合)
开发语言·python
毕设源码-赖学姐7 小时前
【开题答辩全过程】以 二手交易系统的设计与实现为例,包含答辩的问题和答案
java·eclipse
whltaoin7 小时前
【Spring Boot 注解解析】Bean 生命周期注解深度解析:@PostConstruct 与 @PreDestroy 面试高频考点 + 实战案例
java·spring boot·面试·bean生命周期
蒲公英源码7 小时前
教务管理系统源码
java·mysql