一、前言

EasyExcel是阿里开源的一款 Excel导入导出工具,具有处理速度快、占用内存小、使用方便的特点,底层逻辑也是基于 apache poi 进行二次开发的,目前的应用也是非常广!相比 EasyPoi,EasyExcel 的处理数据性能非常高,读取 75M (46W行25列) 的Excel,仅需使用 64M 内存,耗时 20s,极速模式还可以更快!

二、方案实践-简单导出

1.引入依赖

<dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>3.0.5</version>
        </dependency>

EasyExcel 的导出导入支持两种方式进行处理

  • 第一种是通过实体类注解方式来生成文件和反解析文件数据映射成对象
  • 第二种是通过动态参数化生成文件和反解析文件数据

下面我们以用户信息的导出导入为例,分别介绍两种处理方式。

2.创建用户实体

package com.example.dataproject.domain;

import com.alibaba.excel.annotation.ExcelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

/**
 * @author qx
 * @date 2024/8/15
 * @des 用户实体
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserEntity {

    @ExcelProperty(value = "姓名")
    private String name;

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

    @ExcelProperty(value = "性别")
    private String gender;


}

3.创建测试类

package com.example.dataproject.test;

import com.alibaba.excel.EasyExcel;
import com.example.dataproject.domain.UserEntity;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * @author qx
 * @date 2024/8/15
 * @des EasyExcel测试
 */
public class EasyExcelDemo {
    public static void main(String[] args) throws FileNotFoundException {
        List<UserEntity> userEntityList = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            String gender = i % 2 == 0 ? "男" : "女";
            UserEntity user = new UserEntity("aa-" + i, 20 + i, gender);
            userEntityList.add(user);
        }
        //定义文件输出位置
        FileOutputStream outputStream = new FileOutputStream(new File("D:" + File.separator + "easyexcel" + File.separator + "user.xlsx"));
        EasyExcel.write(outputStream, UserEntity.class).sheet("用户信息").doWrite(userEntityList);
    }
}

4.启动程序 查看文件目录

SpringBoot+EasyExcel轻松实现百万级数据导入导出_SpringBoot

SpringBoot+EasyExcel轻松实现百万级数据导入导出_动态导入导出_02

查看excel内容如下:

SpringBoot+EasyExcel轻松实现百万级数据导入导出_EasyExcel_03

三、方案实践-简单导入

这种简单固定表头的 Excel 文件,如果想要读取文件数据,操作也很简单。

以上面的导出文件为例,使用 EasyExcel 提供的EasyExcel工具类,即可来实现文件内容数据的快速读取,示例代码如下:

1.首先创建读取实体类

package com.example.dataproject.domain;

import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;

/**
 * @author qx
 * @date 2024/8/15
 * @des 用户数据读取实体
 */
@Data
public class UserReadEntity {

    @ExcelProperty(index = 0)
    private String name;

    @ExcelProperty(index = 1)
    private Integer age;

    @ExcelProperty(index = 2)
    private String gender;

}

2.创建测试类读取excel数据

package com.example.dataproject.test;

import com.alibaba.excel.EasyExcel;
import com.example.dataproject.domain.UserReadEntity;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.List;

/**
 * @author qx
 * @date 2024/8/15
 * @des excel数据读取
 */
public class EasyExcelReadDemo {
    public static void main(String[] args) throws FileNotFoundException {
        FileInputStream inputStream = new FileInputStream(new File("D:" + File.separator + "easyexcel" + File.separator + "user.xlsx"));
        List<UserReadEntity> userList = EasyExcel.read(inputStream).head(UserReadEntity.class).sheet().doReadSync();
        System.out.println(userList);
    }
}

3.启动程序 进行测试

我们发现控制台日志已经打印出了excel里面的数据

SpringBoot+EasyExcel轻松实现百万级数据导入导出_动态导入导出_04

四、动态自由导出导入

在实际使用开发中,我们不可能每来一个 excel 导入导出需求,就编写一个实体类,很多业务需求需要根据不同的字段来动态导入导出,没办法基于实体类注解的方式来读取文件或者写入文件。

因此,基于EasyExcel提供的动态参数化生成文件和动态监听器读取文件方法,我们可以单独封装一套动态导出导出工具类,省的我们每次都需要重新编写大量重复工作。

1.动态导出工具类

package com.example.dataproject.test;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * @author qx
 * @date 2024/8/15
 * @des
 */
public class DynamicEasyExcelExportUtils {
    private static final Logger log = LoggerFactory.getLogger(DynamicEasyExcelExportUtils.class);

    private static final String DEFAULT_SHEET_NAME = "sheet1";

    /**
     * 动态生成导出模版(单表头)
     *
     * @param headColumns 列名称
     * @return excel文件流
     */
    public static byte[] exportTemplateExcelFile(List<String> headColumns) {
        List<List<String>> excelHead = Lists.newArrayList();
        headColumns.forEach(columnName -> {
            excelHead.add(Lists.newArrayList(columnName));
        });
        byte[] stream = createExcelFile(excelHead, new ArrayList<>());
        return stream;
    }

    /**
     * 动态生成模版(复杂表头)
     *
     * @param excelHead 列名称
     * @return
     */
    public static byte[] exportTemplateExcelFileCustomHead(List<List<String>> excelHead) {
        byte[] stream = createExcelFile(excelHead, new ArrayList<>());
        return stream;
    }

    /**
     * 动态导出文件(通过map方式计算)
     *
     * @param headColumnMap 有序列头部
     * @param dataList      数据体
     * @return
     */
    public static byte[] exportExcelFile(LinkedHashMap<String, String> headColumnMap, List<Map<String, Object>> dataList) {
        //获取列名称
        List<List<String>> excelHead = new ArrayList<>();
        if (MapUtils.isNotEmpty(headColumnMap)) {
            //key为匹配符,value为列名,如果多级列名用逗号隔开
            headColumnMap.entrySet().forEach(entry -> {
                excelHead.add(Lists.newArrayList(entry.getValue().split(",")));
            });
        }
        List<List<Object>> excelRows = new ArrayList<>();
        if (MapUtils.isNotEmpty(headColumnMap) && CollectionUtils.isNotEmpty(dataList)) {
            for (Map<String, Object> dataMap : dataList) {
                List<Object> rows = new ArrayList<>();
                headColumnMap.entrySet().forEach(headColumnEntry -> {
                    if (dataMap.containsKey(headColumnEntry.getKey())) {
                        Object data = dataMap.get(headColumnEntry.getKey());
                        rows.add(data);
                    }
                });
                excelRows.add(rows);
            }
        }
        byte[] stream = createExcelFile(excelHead, excelRows);
        return stream;
    }


    /**
     * 生成文件(自定义头部排列)
     *
     * @param rowHeads
     * @param excelRows
     * @return
     */
    public static byte[] customerExportExcelFile(List<List<String>> rowHeads, List<List<Object>> excelRows) {
        //将行头部转成easyexcel能识别的部分
        List<List<String>> excelHead = transferHead(rowHeads);
        return createExcelFile(excelHead, excelRows);
    }

    /**
     * 生成文件
     *
     * @param excelHead
     * @param excelRows
     * @return
     */
    private static byte[] createExcelFile(List<List<String>> excelHead, List<List<Object>> excelRows) {
        try {
            if (CollectionUtils.isNotEmpty(excelHead)) {
                ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                EasyExcel.write(outputStream).registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                        .head(excelHead)
                        .sheet(DEFAULT_SHEET_NAME)
                        .doWrite(excelRows);
                return outputStream.toByteArray();
            }
        } catch (Exception e) {
            log.error("动态生成excel文件失败,headColumns:" + excelHead + ",excelRows:" + excelRows, e);
        }
        return null;
    }

    /**
     * 将行头部转成easyexcel能识别的部分
     *
     * @param rowHeads
     * @return
     */
    public static List<List<String>> transferHead(List<List<String>> rowHeads) {
        //将头部列进行反转
        List<List<String>> realHead = new ArrayList<>();
        if (CollectionUtils.isNotEmpty(rowHeads)) {
            Map<Integer, List<String>> cellMap = new LinkedHashMap<>();
            //遍历行
            for (List<String> cells : rowHeads) {
                //遍历列
                for (int i = 0; i < cells.size(); i++) {
                    if (cellMap.containsKey(i)) {
                        cellMap.get(i).add(cells.get(i));
                    } else {
                        cellMap.put(i, Lists.newArrayList(cells.get(i)));
                    }
                }
            }
            //将列一行一行加入realHead
            cellMap.entrySet().forEach(item -> realHead.add(item.getValue()));
        }
        return realHead;
    }

    /**
     * 导出文件测试
     *
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        //导出包含数据内容的文件(方式一)
        LinkedHashMap<String, String> headColumnMap = Maps.newLinkedHashMap();
        headColumnMap.put("className", "班级");
        headColumnMap.put("name", "学生信息,姓名");
        headColumnMap.put("sex", "学生信息,性别");
        List<Map<String, Object>> dataList = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            Map<String, Object> dataMap = Maps.newHashMap();
            dataMap.put("className", "一年级");
            dataMap.put("name", "张三" + i);
            dataMap.put("sex", "男");
            dataList.add(dataMap);
        }
        byte[] stream1 = exportExcelFile(headColumnMap, dataList);
        FileOutputStream outputStream1 = new FileOutputStream(new File("D:" + File.separator + "easyexcel" + File.separator + "user1.xlsx"));
        outputStream1.write(stream1);
        outputStream1.close();


        //导出包含数据内容的文件(方式二)
        //头部,第一层
        List<String> head1 = new ArrayList<>();
        head1.add("第一行头部列1");
        head1.add("第一行头部列1");
        head1.add("第一行头部列1");
        head1.add("第一行头部列1");
        //头部,第二层
        List<String> head2 = new ArrayList<>();
        head2.add("第二行头部列1");
        head2.add("第二行头部列1");
        head2.add("第二行头部列2");
        head2.add("第二行头部列2");
        //头部,第三层
        List<String> head3 = new ArrayList<>();
        head3.add("第三行头部列1");
        head3.add("第三行头部列2");
        head3.add("第三行头部列3");
        head3.add("第三行头部列4");

        //封装头部
        List<List<String>> allHead = new ArrayList<>();
        allHead.add(head1);
        allHead.add(head2);
        allHead.add(head3);

        //封装数据体
        //第一行数据
        List<Object> data1 = Lists.newArrayList(1, 1, 1, 1);
        //第二行数据
        List<Object> data2 = Lists.newArrayList(2, 2, 2, 2);
        List<List<Object>> allData = Lists.newArrayList(data1, data2);

        byte[] stream2 = customerExportExcelFile(allHead, allData);
        FileOutputStream outputStream2 = new FileOutputStream(new File("D:" + File.separator + "easyexcel" + File.separator + "user2.xlsx"));
        outputStream2.write(stream2);
        outputStream2.close();


    }
}

2.启动程序 进行测试

SpringBoot+EasyExcel轻松实现百万级数据导入导出_SpringBoot_05

SpringBoot+EasyExcel轻松实现百万级数据导入导出_EasyExcel_06

我们分别查看user1.xlsx和user2.xlsx。

SpringBoot+EasyExcel轻松实现百万级数据导入导出_SpringBoot_07

SpringBoot+EasyExcel轻松实现百万级数据导入导出_EasyExcel_08

3.动态导入

先创建一个文件读取监听器

package com.example.dataproject.test;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * @author qx
 * @date 2024/8/15
 * @des
 */
public class DynamicEasyExcelListener extends AnalysisEventListener<Map<Integer, String>> {
    private static final Logger LOGGER = LoggerFactory.getLogger(DynamicEasyExcelListener.class);
    /**
     * 表头数据(存储所有的表头数据)
     */
    private List<Map<Integer, String>> headList = new ArrayList<>();
    /**
     * 数据体
     */
    private List<Map<Integer, String>> dataList = new ArrayList<>();

    /**
     * 这里会一行行的返回头
     *
     * @param headMap
     * @param context
     */
    @Override
    public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
        LOGGER.info("解析到一条头数据:{}", headMap);
        //存储全部表头数据
        headList.add(headMap);
    }

    /**
     * 这个每一条数据解析都会来调用
     *
     * @param data    one row value. Is is same as {@link AnalysisContext#readRowHolder()}
     * @param context
     */
    @Override
    public void invoke(Map<Integer, String> data, AnalysisContext context) {
        LOGGER.info("解析到一条数据:{}", data);
        dataList.add(data);
    }

    /**
     * 所有数据解析完成了 都会来调用
     *
     * @param context
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        // 这里也要保存数据,确保最后遗留的数据也存储到数据库
        LOGGER.info("所有数据解析完成!");
    }

    public List<Map<Integer, String>> getHeadList() {
        return headList;
    }

    public List<Map<Integer, String>> getDataList() {
        return dataList;
    }
}

然后创建动态导入工具类

package com.example.dataproject.test;

import com.alibaba.excel.EasyExcelFactory;
import com.alibaba.excel.util.IoUtils;
import com.google.common.collect.Lists;
import org.springframework.util.CollectionUtils;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * @author qx
 * @date 2024/8/15
 * @des
 */
public class DynamicEasyExcelImportUtils {
    /**
     * 动态获取全部列和数据体,默认从第一行开始解析数据
     *
     * @param stream
     * @return
     */
    public static List<Map<String, String>> parseExcelToView(byte[] stream) {
        return parseExcelToView(stream, 1);
    }

    /**
     * 动态获取全部列和数据体
     *
     * @param stream         excel文件流
     * @param parseRowNumber 指定读取行
     * @return
     */
    public static List<Map<String, String>> parseExcelToView(byte[] stream, Integer parseRowNumber) {
        DynamicEasyExcelListener readListener = new DynamicEasyExcelListener();
        EasyExcelFactory.read(new ByteArrayInputStream(stream)).registerReadListener(readListener).headRowNumber(parseRowNumber).sheet(0).doRead();
        List<Map<Integer, String>> headList = readListener.getHeadList();
        if (CollectionUtils.isEmpty(headList)) {
            throw new RuntimeException("Excel未包含表头");
        }
        List<Map<Integer, String>> dataList = readListener.getDataList();
        if (CollectionUtils.isEmpty(dataList)) {
            throw new RuntimeException("Excel未包含数据");
        }
        //获取头部,取最后一次解析的列头数据
        Map<Integer, String> excelHeadIdxNameMap = headList.get(headList.size() - 1);
        //封装数据体
        List<Map<String, String>> excelDataList = Lists.newArrayList();
        for (Map<Integer, String> dataRow : dataList) {
            Map<String, String> rowData = new LinkedHashMap<>();
            excelHeadIdxNameMap.entrySet().forEach(columnHead -> {
                rowData.put(columnHead.getValue(), dataRow.get(columnHead.getKey()));
            });
            excelDataList.add(rowData);
        }
        return excelDataList;
    }

    /**
     * 文件导入测试
     *
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        FileInputStream inputStream = new FileInputStream(new File("D:" + File.separator + "easyexcel" + File.separator + "user1.xlsx"));
        byte[] stream = IoUtils.toByteArray(inputStream);
        List<Map<String, String>> dataList = parseExcelToView(stream, 2);
        System.out.println(dataList);
        inputStream.close();
    }
}

启动查询测试,从控制台日志来看我们已经成功从excel导入了数据。

SpringBoot+EasyExcel轻松实现百万级数据导入导出_SpringBoot_09

五、小结

在实际的业务开发过程中,根据参数动态实现 Excel 的导出导入还是非常广的。

当然,EasyExcel 的功能还不只上面介绍的那些内容,还有基于模版进行 excel的填充,web 端 restful 的导出导出。