EasyExcel实现百万数据的导入导出

在日常的开发工作中,Excel 文件的读取和写入是非常常见的需求,特别是在后台管理系统中更为频繁,基于传统的方式操作excel通常比较繁琐,EasyExcel 库的出现为我们带来了更简单、更高效的解决方案。本文将介绍 EasyExcel 库的基本用法和一些常见应用场景,帮助开发者更好地利用 EasyExcel 提高工作效率。

代码地址:https://github.com/alibaba/easyexcel

官网地址:Easy Excel 官网 (alibaba.com)

百万数据导入

具体方案

  1. 单线程逐行解析、单线程单条数据逐条插入
  2. 单线程逐行解析、单线程批量插入
  3. 多线程解析、单线程批量插入
  4. 单线程逐行解析、异步线程批量插入
  5. 多线程解析、多线程批量插入

方案一:单线程解析,单线程逐条插入

  • 单线程逐行解析: 使用单个线程顺序地逐行读取  Excel  文件

  • 单线程单条数据逐条插入:使用单线程,每读取到一条数据,就立即执行单条插入操作

优点:实现简单,容易调试和维护;适合小数据量的场景。

缺点:效率极低,因为每次插入都要进行一次网络请求和磁盘I/O操作,且没有利用到批量操作的优势。在大量数据的情况下非常耗时。

// 单线程逐条解析
public void importExcel(MultipartFile file) throws IOException {
    EasyExcel.read(file.getInputStream(), Salaries.class, salariesListener).doReadAll();
}

EasyExcel解析完之后,都会回调ReadListener方法,所以我们在这里写Listener类去实现ReadListener,来对读取的数据进行处理。

@Component
@Slf4j
public class SalariesListener extends ServiceImpl<EmployeeMapper, EmployeeDO> implements ReadListener<EmployeeDO>, IService<EmployeeDO> {
    // 线程池
    private ExecutorService executorService = Executors.newFixedThreadPool(20);
    private ThreadLocal<ArrayList<Salaries>> salariesList = ThreadLocal.withInitial(ArrayList::new);
    private static AtomicInteger count = new AtomicInteger(1);
    private static final int batchSize = 10000;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void invoke(EmployeeDO employeeDO, AnalysisContext analysisContext) {
        //方案一:单线程逐行插入
        saveOne(data);
    }

    public void saveOne(EmployeeDO data){
        save(data);
        log.info("第" + count.getAndAdd(1) + "次插入1条数据");
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        // TODO
    }
}

方案二(推荐):单线程解析,单线程批量插入

  • 单线程逐行解析:使用一个线程逐行读取 Excel 文件的数据。

  • 解析线程批量插入:使用单线程,将读取到的数据按批次(一定数量的数据)执行批量插入操作

优点:实现相对简单。比单条插入效率高,减少了网络请求和磁盘I/O的次数

缺点:受限于单线程的处理能力,效率不如多线程,并且在解析过程中可能会消耗大量内存,因为需要先将数据读入内存再进行批量插入。

// 单线程逐条解析
public void importExcel(MultipartFile file) throws IOException {
    EasyExcel.read(file.getInputStream(), Salaries.class, salariesListener).doReadAll();
}
@Component
@Slf4j
public class SalariesListener extends ServiceImpl<EmployeeMapper, EmployeeDO> implements ReadListener<EmployeeDO>, IService<EmployeeDO> {
    // 线程池
    private ExecutorService executorService = Executors.newFixedThreadPool(20);
    private ThreadLocal<ArrayList<Salaries>> salariesList = ThreadLocal.withInitial(ArrayList::new);
    private static AtomicInteger count = new AtomicInteger(1);
    private static final int batchSize = 10000;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void invoke(EmployeeDO employeeDO, AnalysisContext analysisContext) {
        //方案二:单线程批量插入
        salariesList.get().add(data);
        if (salariesList.get().size() >= batchSize) {
            saveData();
        }
    }

    public void saveOne(EmployeeDO data){
        if (!salariesList.get().isEmpty()) {
            saveBatch(salariesList.get(), salariesList.get().size());
            logger.info("第" + count.getAndAdd(1) + "次插入" + salariesList.get().size() + "条数据");
            salariesList.get().clear();
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        // TODO
    }
}

方案三:多线程解析,单线程批量插入

  • 多线程解析:每个Sheet对应一个线程进行解析,多个线程并行解析不同的Sheet。

  • 解析线程批量插入:使用单线程将读取到的数据积累到一定数量后,执行批量插入操作。

优点:多线程解析可以充分利用多核CPU的优势,提高解析速度;批量插入可以减少数据库的事务开销和I/O操作。

缺点:需要考虑多线程之间的数据传输和同步的问题,设计和实现较复杂。可能在解析和插入之间存在性能瓶颈。例如,若插入较快,则需要等待解析完成才可进行插入,因此可用异步线程的方式

// 多线程解析,一个sheet对应一个线程
public void importExcelAsync(MultipartFile file) {
    // 开20个线程分别处理20个sheet
    List < Callable < Object >> tasks = new ArrayList < > ();
    for (int i = 0; i < 20; i++) {
        int num = i;
        tasks.add(() - > {
            EasyExcel.read(file.getInputStream(), Salaries.class, salariesListener)
                .sheet(num)
                .doRead();
            return null;
        });
    }

    try {
        executorService.invokeAll(tasks);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}
@Component
@Slf4j
public class SalariesListener extends ServiceImpl<EmployeeMapper, EmployeeDO> implements ReadListener<EmployeeDO>, IService<EmployeeDO> {
    // 线程池
    private ExecutorService executorService = Executors.newFixedThreadPool(20);
    private ThreadLocal<ArrayList<Salaries>> salariesList = ThreadLocal.withInitial(ArrayList::new);
    private static AtomicInteger count = new AtomicInteger(1);
    private static final int batchSize = 10000;

    @Resource
    private SalariesListener listener;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void invoke(EmployeeDO employeeDO, AnalysisContext analysisContext) {
        //方案三:单线程批量插入
        salariesList.get().add(data);
        if (salariesList.get().size() >= batchSize) {
            saveData();
        }
    }

    public void saveOne(EmployeeDO data){
        if (!salariesList.get().isEmpty()) {
            saveBatch(salariesList.get(), salariesList.get().size());
            logger.info("第" + count.getAndAdd(1) + "次插入" + salariesList.get().size() + "条数据");
            salariesList.get().clear();
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        // TODO
    }
}

方案四(较推荐):单线程解析,异步批量插入

  • 单线程逐行解析:使用一个线程逐行读取 Excel 文件的数据。

  • 异步线程批量插入:另开一个线程将数据批量插入到数据库,解析线程可以继续执行后续的解析操作。(可以只使用一个线程,防止多线程插入导致锁竞争的问题)

优点:解析和插入操作分离,可以在解析的同时进行插入,提高效率。

缺点:受限于单线程解析速度,而且需要管理异步操作

// 单线程逐条解析
public void importExcel(MultipartFile file) throws IOException {
    EasyExcel.read(file.getInputStream(), Salaries.class, salariesListener).doReadAll();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void invoke(Salaries data, AnalysisContext context) {
    salariesList.get().add(data);
    if (salariesList.get().size() >= batchSize) {
          asyncSaveData1();//异步线程批量插入
    }
}

方案五(推荐):多线程解析,多线程批量插入

  • 多线程解析:每个Sheet对应一个线程进行解析,多个线程并行解析不同的Sheet。

  • 多线程批量插入:另开多个线程将数据批量插入到数据库。

优点:结合了多线程解析和多线程异步插入的优势,可以最大化利用系统资源,提高数据导入速度。

缺点:实现较为复杂。需要小心处理数据库的并发连接和事务管理,防止引入死锁和性能瓶颈。

// 多线程解析,一个sheet对应一个线程
public void importExcelAsync(MultipartFile file) {
    // 开20个线程分别处理20个sheet
    List < Callable < Object >> tasks = new ArrayList < > ();
    for (int i = 0; i < 20; i++) {
        int num = i;
        tasks.add(() - > {
            EasyExcel.read(file.getInputStream(), Salaries.class, salariesListener)
                .sheet(num)
                .doRead();
            return null;
        });
    }

    try {
        executorService.invokeAll(tasks);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}
@Component
@Slf4j
public class SalariesListener extends ServiceImpl<EmployeeMapper, EmployeeDO> implements ReadListener<EmployeeDO>, IService<EmployeeDO> {
    // 线程池
    private ExecutorService executorService = Executors.newFixedThreadPool(20);
    private ThreadLocal<ArrayList<Salaries>> salariesList = ThreadLocal.withInitial(ArrayList::new);
    private static AtomicInteger count = new AtomicInteger(1);
    private static final int batchSize = 10000;

    @Resource
    private SalariesListener listener;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void invoke(EmployeeDO employeeDO, AnalysisContext analysisContext) {
        //方案五:多线程批量插入
        salariesList.get().add(data);
        if (salariesList.get().size() >= batchSize) {
            asyncSaveData(); 
        }
    }

    public void saveOne(EmployeeDO data){
        if (!salariesList.get().isEmpty()) {
            ArrayList < Salaries > salaries = (ArrayList < Salaries > ) salariesList.get().clone();
            executorService.execute(new SaveTask(salaries, salariesListener));
            salariesList.get().clear();
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        // TODO
    }
}

方案选择

  • 数据量较小(如几万条或更少的数据):可以选择方案2(单线程逐行解析;单线程批量插入)或方案4(单线程逐行解析;异步线程批量插入),实现简单且性能足够。

  • 数据量较大(如几十万到百万条数据):推荐方案5(多线程解析,每个sheet对应一个线程;异步线程批量插入)。尽管实现复杂,但它能够充分利用系统资源,极大地提升导入效率。

在选择方案4时,需要特别注意以下几点:

  1. 复杂性增加:方案4引入了异步操作,这意味着需要管理异步线程的生命周期,处理线程同步和可能的并发问题。对于小数据量,这种额外的复杂性可能不值得。

  2. 性能开销:异步操作可能会带来额外的性能开销,尤其是在线程创建、上下文切换和管理异步队列等方面。如果数据量不大,这些开销可能超过其带来的性能提升。

  3. 资源利用:对于小数据量,单线程的解析和批量插入可能已经足够快,且能够有效利用系统资源。引入异步操作可能会导致资源管理变得更加复杂,而不一定能带来明显的性能改进。当Excel中只有一个sheet时,只能逐行解析,但是以下几点可以考虑:

  4. 单线程足够:如果数据量不大,单线程逐行解析配合批量插入已经能够提供足够快的导入速度。

  5. 简化实现:单线程方案(如方案2)实现起来更简单,更容易调试和维护。

  6. 性能测试:在没有进行性能测试之前,可能无法确定异步操作是否真的带来了性能上的提升。有时候,简单的同步方案就足够满足需求。

总之,方案4可能在数据量较大时更有优势,因为它可以更有效地利用系统资源,但在数据量不大时,这种优势可能不明显,反而增加了实现的复杂性和维护成本。因此,在选择方案时,需要根据实际的数据量、系统资源和性能需求来做出决策。但若是数据量较大,并且只有一个sheet,只能逐行解析时,较推荐使用方案4

在选择方案5时,需要特别注意以下几点:

  1. 线程池管理:使用Java的 ExecutorService 来管理解析和插入线程,防止线程过多导致资源耗尽。

  2. 批量插入大小:适当的批量大小可以提高性能,一般建议每次批量插入500到1000条数据。

  3. 数据库连接池:配置合理的数据库连接池(如HikariCP),以支持高并发的数据库操作。

  4. 错误处理和事务管理:确保对于每批数据操作有适当的错误处理和事务管理,以保证数据一致性。

综合考虑性能和实现复杂性,方案5 是处理百万级数据导入的最佳选择,前提是能合理管理多线程和数据库连接。

导入优化

从上面我们可以看出,在导入数据的时候需要创建一个回调监听器 DemoDataListener ,也就是针对每个DemoData 即每个Excel 都需要创建一个单独的回调监听器类。

// 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
@Slf4j
public class DemoDataListener implements ReadListener<DemoData> {
 
    /**
     * 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收
     */
    private static final int BATCH_COUNT = 100;
    /**
     * 缓存的数据
     */
    private List<DemoData> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
    /**
     * 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。
     */
    private DemoDataDAO demoDAO;
 
    public DemoDataListener() {
        // 这里是demo,所以随便new一个。实际使用如果到了spring,请使用下面的有参构造函数
        demoDAO = new DemoDAO();
    }
 
    /**
     * 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来
     *
     * @param demoDAO
     */
    public DemoDataListener(DemoDataDAO demoDAO) {
        this.demoDAO = demoDAO;
    }
 
    /**
     * 这个每一条数据解析都会来调用
     *
     * @param data    one row value. Is is same as {@link AnalysisContext#readRowHolder()}
     * @param context
     */
    @Override
    public void invoke(DemoData data, AnalysisContext context) {
        log.info("解析到一条数据:{}", JSON.toJSONString(data));
        cachedDataList.add(data);
        // 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
        if (cachedDataList.size() >= BATCH_COUNT) {
            saveData();
            // 存储完成清理 list
            cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
        }
    }
 
    /**
     * 所有数据解析完成了 都会来调用
     *
     * @param context
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        // 这里也要保存数据,确保最后遗留的数据也存储到数据库
        saveData();
        log.info("所有数据解析完成!");
    }
 
    /**
     * 加上存储数据库
     */
    private void saveData() {
        log.info("{}条数据,开始存储数据库!", cachedDataList.size());
        demoDAO.save(cachedDataList);
        log.info("存储数据库成功!");
    }
}

在使用EasyExcel读取Excel时就在想能够如何简化读取方式,而不是读取每个Excel都创建一个XXDataListener 的监听器类

首先,可以把DataListener加上泛型,共用一个DataListener<T>

看上面代码,只需要在new的时候再去new DataListener<DemoData>即可

但是,如果要传递Dao 和 并且每个Dao如何保存数据,而且保存数据前可能还需要对数据进行校验,也就存在以下问题:

  • 如何校验表头?

  • 如何处理表头数据?

  • 如何校验真实数据?

  • 如何处理真实数据?

最后想到了可以用java.util.function.Function(数据校验) + java.util.function.Consumer(数据存储) + 模板方法设计模式,创建一个共用的EasyExcel读取监听器,从而不在监听器中对数据进行处理,把处理都前置

EasyExcel 的监听器类 Listener 已经定义了每一步会做什么,如通过 invokeHead 方法一行一行读取表头数据,通过invoke 方法一行一行读取真实数据。

也就是说,我们已经知道了 Listener 类所需的关键步骤,即一行一行读取数据,而且确定了这些步骤的执行顺序,但表头数据的校验和处理,真实数据的校验和处理还无法确定;并且这些是由导入的具体数据决定的,不同的表会有不同的校验 和 处理方式。基于此,我们就可以想到用 模板方法模式

import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.util.ConverterUtils;
import lombok.extern.slf4j.Slf4j;
 
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
 
@Slf4j
public class EasyExcelListener<T> implements ReadListener<T> {
 
    private static final int defaultBatchSize = 5000;
 
    private final int batchSize;
 
    private List<Map<Integer, String>> headData;
 
    private List<T> realData;
 
    private final int headRowCount;
 
    private Function<Map<Integer, String>, String> headDataCheck;
 
    private final Consumer<List<Map<Integer, String>>> headDataConsumer;
 
    private Function<T, String> realDataCheck;
 
    private final Consumer<List<T>> realDataConsumer;
 
    private final List<String> errorList;
 
    /**
     * 构造函数
     *
     * @param headRowCount     表头行数
     * @param headDataCheck    表头数据校验
     * @param headDataConsumer 表头数据处理
     * @param realDataCheck    真实数据校验
     * @param realDataConsumer 真实数据处理
     */
    public EasyExcelListener(int headRowCount, Function<Map<Integer, String>, String> headDataCheck, Consumer<List<Map<Integer, String>>> headDataConsumer
            , Function<T, String> realDataCheck, Consumer<List<T>> realDataConsumer) {
 
        this.headRowCount = headRowCount;
        this.batchSize = defaultBatchSize;
        this.headDataCheck = headDataCheck;
        this.headDataConsumer = headDataConsumer;
        this.realDataCheck = realDataCheck;
        this.realDataConsumer = realDataConsumer;
        headData = new ArrayList<>(headRowCount);
        realData = new ArrayList<>(batchSize);
        errorList = new ArrayList<>();
    }
 
    public EasyExcelListener(int headRowCount, int batchSize, Function<Map<Integer, String>, String> headDataCheck, Consumer<List<Map<Integer, String>>> headDataConsumer
            , Function<T, String> realDataCheck, Consumer<List<T>> realDataConsumer) {
 
        this.headRowCount = headRowCount;
        this.batchSize = batchSize;
        this.headDataCheck = headDataCheck;
        this.headDataConsumer = headDataConsumer;
        this.realDataCheck = realDataCheck;
        this.realDataConsumer = realDataConsumer;
        headData = new ArrayList<>(headRowCount);
        realData = new ArrayList<>(batchSize);
        errorList = new ArrayList<>();
    }
 
    @Override
    public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
        //获取表头数据
        Map<Integer, String> map = ConverterUtils.convertToStringMap(headMap, context);
 
        //校验表头数据
        String checkRes = headDataCheck.apply(map);
        if (StrUtil.isNotBlank(checkRes)) {
            int rowIndex = context.readRowHolder().getRowIndex() + 1;
            errorList.add("行号为:" + rowIndex + checkRes);
            return;
        }
 
        if (!errorList.isEmpty()) {
            //有错误信息,就不会再处理,只做校验
            return;
        }
         
        //没有问题的数据添加进list中
        headData.add(map);
        if (context.readRowHolder().getRowIndex() == headRowCount - 1) {
            //全部读取完表头数据后,处理表头数据
            headDataConsumer.accept(headData);
        }
    }
 
    @Override
    public void invoke(T data, AnalysisContext context) {
        //判断真实数据是否符合要求
        String checkRes = realDataCheck.apply(data);
        if (StrUtil.isNotBlank(checkRes)) {
            int rowIndex = context.readRowHolder().getRowIndex() + 1;
            errorList.add("行号为:" + rowIndex + checkRes);
            return;
        }
 
        //没有报错的数据添加进list
        realData.add(data);
        if (realData.size() >= batchSize) {
            //处理真实数据
            realDataConsumer.accept(realData);
            //清空realData
            realData = new ArrayList<>(batchSize);
        }
    }
 
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        // 解析完所有excel行, 剩余的数据还需要进行处理
        realDataConsumer.accept(realData);
    }
 
    public List<Map<Integer, String>> getHeadData() {
        return headData;
    }
 
    public List<T> getRealData() {
        return realData;
    }
 
    public List<String> getErrorList() {
        return errorList;
    }
}

EasyExcel读取封装后的使用示例:

//构建 EasyExcelListener
EasyExcelListener < TaskImportDTO > taskImportDTOEasyExcelListener =
    new EasyExcelListener < > (
        4, //表头行数
        (map) - > { //表头数据校验
            return checkMap(map, startTime, taskMainDOList);
        }, headList - > {
            //表头数据处理    
            saveHeadData(headList);
        }, (data) - > {
            //真实数据校验
            return checkImportData(data);
        }, realDataList - > {
            //真实数据处理
            saveRealData(realDataList);
        }
    );
 
//读取数据
EasyExcelUtil.read(inputStream, TaskImportDTO.class, taskImportDTOEasyExcelListener)
    .headRowNumber(4) //表头行数
    .sheet()
    .doRead();

上面的 checkMap ,saveHeadData,checkImportData,saveRealData皆为针对本次导入数据自定义的校验和处理方法。

若还有其它导入需求,只需new EasyExcelListener 方法,并自定义自己的校验和处理逻辑即可,从而完成代码复用!

百万数据导出

具体方案:

  1. 查全表,写入一个sheet
  2. 查全表,写入多个sheet
  3. 分页查询,每页数据写入每个sheet
  4. 多线程分页查询,每页数据写入每个sheet

方案一:查全表,写入一个sheet

  • 查全表:一次性查询出整个表的数据。

  • 写入一个sheet:将所有查询结果写入同一个 Excel sheet 中。

优点:实现简单,逻辑清晰;适用于数据量小的情况。

缺点:

  • 性能瓶颈:一次性查询大量数据会占用大量内存。

  • Excel 单个 sheet 限制:Excel 对单个 sheet 有行数限制(通常是 1048576 行),超过这个限制会导致问题。

public void exportExcel1(HttpServletResponse response) throws IOException {
    setExportHeader(response);

    //查出所有数据
    List < Salaries > salaries = salariesMapper.selectList(null);
    EasyExcel.write(response.getOutputStream(), Salaries.class)
        .sheet() //写到一个sheet
        .doWrite(salaries);
}

方案二:查全表,写入多个sheet

  • 查全表:一次性查询出整个表的数据。

  • 写入多个sheet:根据行数限制,将数据分批写入多个 sheet 中。

优点:解决了单个 sheet 的行数限制问题。

缺点:

  • 一次性查询大量数据仍然会占用大量内存。

  • 需要在逻辑上处理如何分配数据到不同的 sheet 中,大大增加了实现的复杂度。

public void exportExcel2(HttpServletResponse response) throws IOException {
    setExportHeader(response);
    //查出所有数据
    List < Salaries > salaries = salariesMapper.selectList(null);

    try (ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream(), Salaries.class).build()) {
        //创建3个sheet
        WriteSheet writeSheet1 = EasyExcel.writerSheet(1, "模板1").build();
        WriteSheet writeSheet2 = EasyExcel.writerSheet(2, "模板2").build();
        WriteSheet writeSheet3 = EasyExcel.writerSheet(3, "模板3").build();

        //将查出的数据进行分割
        List < Salaries > data1 = salaries.subList(0, salaries.size() / 3);
        List < Salaries > data2 = salaries.subList(salaries.size() / 3, salaries.size() * 2 / 3);
        List < Salaries > data3 = salaries.subList(salaries.size() * 2 / 3, salaries.size());

        //写入3个sheet
        excelWriter.write(data1, writeSheet1);
        excelWriter.write(data2, writeSheet2);
        excelWriter.write(data3, writeSheet3);
    }
}

方案三(推荐):分页查询,每页数据写入每个sheet

  • 分页查询:将数据分页查询,每次查询一页数据。

  • 每页数据写入每个sheet:每页数据写入不同的 sheet。

优点:

  • 内存占用更均衡,不会一次性查询大量数据。

  • 适用于大数据量,可以逐页处理,不会超过单个 sheet 的行数限制。

缺点:

  • 实现稍复杂,需要处理分页逻辑和 sheet 切换。

  • 写入多个 sheet 可能会增加文件大小和处理时间。

public void exportExcel3(HttpServletResponse response) throws IOException {

    setExportHeader(response);

    try (ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream(), Salaries.class).build()) {
        //查询表数据条数
        Long count = salariesMapper.selectCount(null);
        Integer pages = 10; //定义分成10页数据
        Long size = count / pages; //每页条数

        for (int i = 0; i < pages; i++) {
            //pages 页条数据,就创建pages页个 sheet
            WriteSheet writeSheet = EasyExcel.writerSheet(i, "模板" + i).build();

            Page < Salaries > page = new Page < > ();
            page.setCurrent(i + 1);
            page.setSize(size);
            Page < Salaries > selectPage = salariesMapper.selectPage(page, null);

            excelWriter.write(selectPage.getRecords(), writeSheet);
        }
    }
}

方案四:多线程分页查询,每页数据写入每个sheet

  • 多线程分页查询:使用多线程并行进行分页查询,每个线程处理一部分数据。

  • 每页数据写入每个sheet:每个线程处理的数据写入不同的 sheet。

优点:

  • 并行处理,极大提高查询速度。

  • 内存使用更加高效,因为每个线程只处理自己的一部分数据。

  • 有助于充分利用多核 CPU 的能力。

缺点:

  • 需要合理管理线程池,避免资源竞争导致性能瓶颈。

  • 可能引入并发访问数据库的问题,需要小心处理数据库连接和事务。

public void exportExcel4(HttpServletResponse response) throws IOException, InterruptedException {

    setExportHeader(response);
    //查询表数据条数
    Long count = salariesMapper.selectCount(null);

    Integer pages = 20; //定义分成10页数据
    Long size = count / pages; //每页条数

    //创建pages个线程
    ExecutorService executorService = Executors.newFixedThreadPool(pages);
    CountDownLatch countDownLatch = new CountDownLatch(pages);

    Map < Integer, Page < Salaries >> pageMap = new HashMap < > ();
    for (int i = 0; i < pages; i++) {
        int finalI = i;
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                //多线程分页查询
                Page < Salaries > page = new Page < > ();
                page.setCurrent(finalI + 1);
                page.setSize(size);
                Page < Salaries > selectPage = salariesMapper.selectPage(page, null);

                pageMap.put(finalI, selectPage);
                countDownLatch.countDown();
            }
        });
    }

    countDownLatch.await();

    try (ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream(), Salaries.class).build()) {
        //easyexcel不支持并发写入多个sheet,只能一个sheet一个sheet的写
        for (Map.Entry < Integer, Page < Salaries >> entry: pageMap.entrySet()) {
            Integer num = entry.getKey();
            Page < Salaries > salariesPage = entry.getValue();
            WriteSheet writeSheet = EasyExcel.writerSheet(num, "模板" + num).build();
            //写入多个sheet
            excelWriter.write(salariesPage.getRecords(), writeSheet);
        }
    }

    // https://github.com/alibaba/easyexcel/issues/1040
}

方案选择:

对于百万级数据的导出,建议选择方案 3 或方案 4:

  • 方案 3(分页查询,每页数据写入每个sheet):这个方案在实现上适中,能够有效解决单个 sheet 的行数限制问题,并且在内存使用上较为均衡。虽然实现稍复杂,但性能和资源利用率较好,适合大多数场景。

  • 方案 4(多线程分页查询;每页数据写入每个sheet):这个方案适用于需要极高性能和速度的场景。通过多线程并行处理,可以极大提高导出效率,但实现复杂度较高,需要处理好线程同步和资源共享问题。如果系统资源(如 CPU 和内存)充足,并且能够合理管理多线程,这是性能最优的选择。

综上所述,方案 3 是一个兼顾实现复杂度和性能的选择,而方案 4 可以在资源充足(同样是全表放入内存中)且需要高性能的情况下使用。根据具体需求和系统资源,选择最合适的方案。

EasyExcel替代产品

EasyExcel的作者创建了新项目:FastExcel。

开源地址:https://github.com/CodePhiliaX/fastexcel

作者选择为它起名为 FastExcel,以突出这个框架在处理 Excel 文件时的高性能表现,而不仅仅是简单易用。

FastExcel 将始终坚持免费开源,并采用最开放的 MIT 协议,使其适用于任何商业化场景。这为开发者和企业提供了极大的自由度和灵活性。FastExcel 的一些显著特点包括:

  • 完全兼容原 EasyExcel 的所有功能和特性,这使得用户可以无缝过渡。

  • 从 EasyExcel 迁移到 FastExcel 只需简单地更换包名和 Maven 依赖即可完成升级。

  • 在功能上,比 EasyExcel 提供更多创新和改进。

  • FastExcel 1.0.0 版本新增了读取 Excel 指定行数和将 Excel 转换为 PDF 的功能。

他们计划在未来推出更多新特性,以不断提升用户体验和工具实用性。FastExcel 致力于成为处理 Excel 文件的最佳选择。

主要特性:

  • 高性能读写: FastExcel 专注于性能优化,能够高效处理大规模的 Excel 数据。相比一些传统的 Excel 处理库,它能显著降低内存占用。

  • 简单易用: 该库提供了简洁直观的 API,使得开发者可以轻松集成到项目中,无论是简单的 Excel 操作还是复杂的数据处理都能快速上手。

  • 流式操作: FastExcel 支持流式读取,将一次性加载大量数据的问题降到最低。这种设计方式在处理数十万甚至上百万行的数据时尤为重要。

当前 FastExcel 底层使用 poi 作为基础包,如果项目中已经有 poi 相关组件,需要手动排除 poi 的相关 jar 包。

如果使用 Maven 进行项目构建,请在 pom.xml 文件中引入以下配置:

<dependency>    
 <groupId>cn.idev.excel</groupId>    
 <artifactId>fastexcel</artifactId>    
 <version>1.0.0</version>
</dependency>

EasyExcel 与 FastExcel 的区别:

  • FastExcel 支持所有 EasyExcel 的功能,但是 FastExcel 的性能更好,更稳定。

  • FastExcel 与 EasyExcel 的 API 完全一致,可以无缝切换。

  • FastExcel 会持续的更新,修复 bug,优化性能,增加新功能。EasyExcel 如何升级到 FastExcel

简单示例:读取 Excel 文件 下面是读取 Excel 文档的例子:

// 实现 ReadListener 接口,设置读取数据的操作  
public class DemoDataListener implements ReadListener<DemoData> {  
    @Override  
    public void invoke(DemoData data, AnalysisContext context) {  
        System.out.println("解析到一条数据" + JSON.toJSONString(data));  
    }  
  
    @Override  
    public void doAfterAllAnalysed(AnalysisContext context) {  
        System.out.println("所有数据解析完成!");  
    }  
}  
  
public static void main(String[] args) {  
    String fileName = "demo.xlsx";  
    // 读取 Excel 文件  
    FastExcel.read(fileName, DemoData.class, new DemoDataListener()).sheet().doRead();  
}

简单示例:创建 Excel 文件 下面是一个创建 Excel 文档的简单例子:

// 示例数据类  
public class DemoData {  
    @ExcelProperty("字符串标题")  
    private String string;  
    @ExcelProperty("日期标题")  
    private Date date;  
    @ExcelProperty("数字标题")  
    private Double doubleData;  
    @ExcelIgnore  
    private String ignore;  
}  
  
// 填充要写入的数据  
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);  
        list.add(data);  
    }  
    return list;  
}  
  
public static void main(String[] args) {  
    String fileName = "demo.xlsx";  
    // 创建一个名为“模板”的 sheet 页,并写入数据  
    FastExcel.write(fileName, DemoData.class).sheet("模板").doWrite(data());
}

EasyExcel是阿里巴巴开源的一款Java库,用于简化大数据量的Excel文件导入和导出操作。在进行批量导入时,如果出现错误数据,可能是由于以下几个原因: 1. 数据格式不匹配:Excel文件中的数据格式(如日期、数字或字符串)可能与你定义的数据字段类型不匹配,导致解析失败。 2. 数据校验规则:如果你在代码中设置了数据验证规则,比如邮箱格式、电话号码格式等,不符合规则的值会引发错误。 3. 行列结构问题:如果Excel文件的结构与你的数据模型不一致,比如缺少字段或多了字段,都会导致导入失败。 4. 文件损坏或编码问题:Excel文件可能存在损坏或使用了非UTF-8等不支持的编码,这会导致解析过程出错。 5. 限制或异常:EasyExcel在处理大量数据时,可能会因为内存限制或其他内部异常而抛出错误。 为了解决这些问题,你可以尝试以下步骤: - **检查数据**:预览Excel文件的内容,确保数据格式正确且没有缺失或冗余的数据。 - **配置映射**:调整EasyExcel的列映射,确保每个字段都对应正确的字段名和数据类型。 - **异常处理**:添加适当的异常处理代码,捕获并记录错误,以便于定位问题。 - **分批导入**:如果数据量非常大,可以考虑分批导入,减少单次加载的数据量。 - **检查版本**:确认使用的EasyExcel版本是否兼容你的项目需求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值