EasyExcel:高性能Java Excel处理框架的设计与应用

一、引言

在Java开发中,Excel文件的导入导出是常见的需求。传统的Apache POI虽然功能强大,但在处理大数据量时存在内存占用高、性能差的问题。阿里巴巴开源的EasyExcel框架正是为了解决这些痛点而生,它采用流式读取和写入,极大地降低了内存消耗,提升了处理效率。

本文将结合实际项目中的业务系统代码,深入讲解EasyExcel的设计理念、核心特性以及在实际业务场景中的应用。

二、技术栈介绍

本文所展示的EasyExcel应用方案基于以下技术栈构建:

2.1 核心框架

技术组件 版本 用途说明
Spring Boot 2.x 应用框架,提供依赖注入、AOP等核心功能
EasyExcel 3.x Excel处理框架,提供高性能的导入导出功能
MyBatis Plus 3.x ORM框架,简化数据库操作
Elasticsearch 7.x 分布式搜索引擎,用于海量数据的快速检索
Hutool 5.x Java工具类库,提供丰富的工具方法

2.2 数据存储

2.2.1 Elasticsearch(ES)

为什么选择Elasticsearch?

在百万级数据导出场景中,Elasticsearch展现出以下优势:

  1. 高性能查询:支持毫秒级响应,即使数据量达到千万级别
  2. 灵活的查询:支持复杂的条件查询、聚合分析
  3. 水平扩展:支持分布式部署,数据量增长时可通过增加节点扩展
  4. 全文检索:支持全文搜索、模糊匹配等高级功能

在项目中的应用:

@Service
public class DataExportService {
    
    @Autowired
    private IEsService esService;

    /**
     * 使用Elasticsearch分页查询数据并导出
     */
    public void exportFromEs(String taskId, Class clazz, NativeSearchQuery searchQuery) {
        // 分页查询
        int pageSize = 10000;
        long total = esService.searchPageList(clazz, PageRequest.of(0, 1), searchQuery).getTotal();
        long totalPages = (total + pageSize - 1) / pageSize;
        
        for (int page = 0; page < totalPages; page++) {
            // 从ES查询数据
            List records = esService.searchPageList(
                clazz, 
                PageRequest.of(page, pageSize), 
                searchQuery
            ).getRecords();
            
            // 写入Excel
            writeDataToExcel(records);
        }
    }
}

查询示例:

// 构建查询条件
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
    .withQuery(QueryBuilders.boolQuery()
        .must(QueryBuilders.termQuery("status", "ACTIVE"))
        .must(QueryBuilders.rangeQuery("createTime")
            .gte(startDate)
            .lte(endDate))
    )
    .withSort(SortBuilders.fieldSort("createTime")
        .order(SortOrder.DESC))
    .withPageable(PageRequest.of(0, 10000))
    .build();

// 执行查询
IPage<Product> page = esService.searchPageList(Product.class, PageRequest.of(0, 10000), searchQuery);

2.3 对象存储

2.3.1 MinIO

为什么选择MinIO?

  1. 高性能:支持高并发上传下载
  2. 兼容性:完全兼容Amazon S3 API
  3. 私有化部署:支持私有化部署,数据安全可控
  4. 成本优势:相比公有云存储,成本更低

在项目中的应用:

@Service
public class DataExportService {
    
    /**
     * 上传Excel文件到MinIO
     */
    public void uploadToMinIO(String filePath, String objectPath) {
        try (InputStream inputStream = new FileInputStream(filePath)) {
            // 上传到MinIO
            ObjectStorageUtil.upload(inputStream, objectPath);
            log.info("文件上传成功:{}", objectPath);
        } catch (Exception e) {
            log.error("文件上传失败:{}", objectPath, e);
            throw new BusinessException("文件上传失败");
        }
    }
}

2.4 缓存方案

2.4.1 Redis

为什么选择Redis?

  1. 高性能:内存存储,读写速度极快
  2. 数据持久化:支持RDB和AOF两种持久化方式
  3. 丰富的数据结构:支持String、List、Set、Hash等多种数据结构
  4. 分布式锁:支持分布式场景下的锁机制

在项目中的应用:

@Service
public class TaskRecordService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    private static final String TASK_KEY_PREFIX = "export:task:";
    private static final long TASK_EXPIRE_TIME = 24 * 60 * 60; // 24小时

    /**
     * 保存任务状态到Redis
     */
    public void saveTaskRecord(TaskRecord taskRecord) {
        String key = TASK_KEY_PREFIX + taskRecord.getId();
        redisTemplate.opsForValue().set(key, taskRecord, TASK_EXPIRE_TIME, TimeUnit.SECONDS);
    }
    
    /**
     * 从Redis获取任务状态
     */
    public TaskRecord getTaskRecord(String taskId) {
        String key = TASK_KEY_PREFIX + taskId;
        return (TaskRecord) redisTemplate.opsForValue().get(key);
    }
    
    /**
     * 更新任务进度
     */
    public void updateTaskProgress(String taskId, int progress) {
        TaskRecord taskRecord = getTaskRecord(taskId);
        if (taskRecord != null) {
            taskRecord.setProgress(progress);
            saveTaskRecord(taskRecord);
        }
    }
}

2.5 线程池配置

@Configuration
public class AsyncConfig {
    
    /**
     * IO密集型任务线程池
     * 适用于Excel导入导出等IO密集型操作
     */
    @Bean("ioIntensiveTaskExecutor")
    public ThreadPoolTaskExecutor ioIntensiveTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        
        // 核心线程数:根据CPU核心数配置
        executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
        
        // 最大线程数:核心线程数的2倍
        executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors() * 2);
        
        // 队列容量
        executor.setQueueCapacity(200);
        
        // 线程名称前缀
        executor.setThreadNamePrefix("io-intensive-");
        
        // 拒绝策略:由调用线程处理
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        
        // 线程空闲时间
        executor.setKeepAliveSeconds(60);
        
        // 等待所有任务完成后再关闭线程池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(60);
        
        executor.initialize();
        return executor;
    }
}

2.6 技术架构图在这里插入图片描述

2.7 技术选型总结

需求场景 技术选型 选型理由
Excel处理 EasyExcel 高性能、低内存占用、易用性强
数据查询 Elasticsearch 支持海量数据、查询性能优异
文件存储 MinIO 高性能、兼容S3、成本可控
任务状态 Redis 高性能读写、支持过期策略
异步处理 ThreadPoolExecutor 线程复用、资源可控
应用框架 Spring Boot 生态完善、开发效率高

三、最终实现效果

通过EasyExcel框架,成功实现了百万级数据的高效导入导出,并提供了实时进度查看功能。以下是实际应用中的效果展示:

3.1 百万级数据导出

场景描述:系统需要导出100万条商品数据到Excel文件。

实现效果

  1. 任务提交:用户点击导出按钮,系统立即返回任务ID,无需等待

    导出任务创建成功
    任务ID:task-20250104-001
    任务状态:处理中
    
  2. 实时进度:用户可以通过任务ID实时查看导出进度

    任务进度查询
    任务ID:task-20250104-001
    总数据量:1,000,000
    已处理:250,000
    进度:25%
    状态:处理中
    
  3. 分页处理:系统自动将100万数据分批处理,每批1万条

    开始执行导出任务:task-20250104-001
    数据总量:1,000,000
    分页大小:10,000
    总页数:100
    
    处理批次:1/100,已写入:10,000条,进度:1%
    处理批次:2/100,已写入:20,000条,进度:2%
    ...
    处理批次:100/100,已写入:1,000,000条,进度:100%
    
  4. 文件生成:导出完成后,自动生成Excel文件并上传到MinIO

    任务[task-20250104-001]文件上传成功
    文件路径:/exports/products/20250104/product_list_20250104143025.xlsx
    文件大小:45.6MB
    
  5. 任务完成:用户可以下载导出的Excel文件

    任务完成
    任务ID:task-20250104-001
    状态:成功
    下载链接:/api/download/task-20250104-001
    

3.2 百万级数据导入

场景描述:用户上传包含100万条数据的Excel文件进行导入。

实现效果

  1. 文件上传:用户上传Excel文件,系统立即返回任务ID

    文件上传成功
    文件名:product_import_20250104.xlsx
    任务ID:import-task-20250104-001
    任务状态:待处理
    
  2. 异步处理:系统在后台异步解析Excel文件

    开始执行导入任务:import-task-20250104-001
    文件路径:/uploads/temp/product_import_20250104.xlsx
    
  3. 流式读取:使用EasyExcel流式读取,内存占用仅约100MB

    使用EasyExcel监听器读取数据
    批处理大小:1,000
    当前内存占用:98MB
    
  4. 实时进度:用户可以实时查看导入进度

    任务进度查询
    任务ID:import-task-20250104-001
    总数据量:1,000,000
    已处理:500,000
    进度:50%
    状态:处理中
    
  5. 批量入库:每读取1000条数据,批量写入数据库

    批量写入数据库:第1批,数量:1,000
    批量写入数据库:第2批,数量:1,000
    ...
    批量写入数据库:第1000批,数量:1,000
    
  6. 导入完成:导入完成后,生成导入报告

    导入任务完成
    任务ID:import-task-20250104-001
    总数据量:1,000,000
    成功:998,500
    失败:1,500
    耗时:2分35秒
    

3.3 性能对比

处理方式 数据量 内存占用 耗时 用户体验
传统POI同步 100万条 ~2GB 5分钟 需要等待,容易超时
EasyExcel同步 100万条 ~100MB 2分30秒 需要等待
EasyExcel异步 100万条 ~100MB 2分30秒 立即返回,可查看进度

3.4 核心优势

通过EasyExcel的异步处理机制,我们实现了以下核心优势:

  1. 非阻塞操作:用户提交任务后立即返回,无需等待
  2. 实时进度反馈:用户可以随时查看任务处理进度
  3. 低内存占用:百万级数据处理内存占用仅约100MB
  4. 高并发支持:支持多个任务同时处理,互不影响
  5. 异常容错:完善的异常处理机制,确保任务状态正确
  6. 资源自动清理:临时文件自动删除,避免磁盘空间浪费

四、EasyExcel的设计理念

4.1 核心设计思想

EasyExcel的设计核心在于**“内存友好""高性能”**。它通过以下技术手段实现了这一目标:

  1. 流式处理:采用SAX模式解析Excel,避免将整个文件加载到内存
  2. 对象映射:通过注解实现Java对象与Excel行列的自动映射
  3. 缓存优化:智能缓存策略,平衡内存使用与读取效率
  4. 异步处理:支持异步读写,提升大规模数据处理能力

4.2 架构设计

在这里插入图片描述

五、项目中的工具类封装

5.1 基础工具类 EasyExcelUtils

在项目中,我们封装了 EasyExcelUtils 作为基础工具类,提供同步导入导出的通用方法:

@Slf4j
public class EasyExcelUtils {
    
    /**
     * 同步导入Excel文件
     * @param in 输入流
     * @param aClass 实体类
     * @return 数据列表
     */
    public static <T> List<T> easyImport(InputStream in, Class<T> aClass) {
        return EasyExcel.read(in)
            .head(aClass)
            .charset(StandardCharsets.UTF_8)
            .sheet()
            .doReadSync();
    }

    /**
     * 指定Excel类型导入
     */
    public static <T> List<T> easyImport(InputStream in, Class<T> aClass, 
                                        int headRowNumber, ExcelTypeEnum excelType) {
        excelType = Objects.isNull(excelType) ? ExcelTypeEnum.XLSX : excelType;
        return EasyExcel.read(in)
            .head(aClass)
            .excelType(excelType)
            .charset(StandardCharsets.UTF_8)
            .headRowNumber(headRowNumber)
            .sheet()
            .doReadSync();
    }

    /**
     * 同步导出Excel文件
     * @param outputStream 输出流
     * @param dataList 数据列表
     * @param aClass 实体类
     */
    public static <T> void easyExport(OutputStream outputStream, 
                                      List<T> dataList, 
                                      Class<T> aClass) {
        ExcelWriter excelWriter = EasyExcel.write(outputStream, aClass)
            .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
            .needHead(true)
            .build();
        excelWriter.write(dataList, EasyExcel.writerSheet(0).head(aClass).build());
        excelWriter.finish();
    }

    /**
     * 写入Excel文件
     */
    public static <T> void easyWrite(String fileName, List<T> dataList, Class<T> aClass) {
        ExcelWriter excelWriter = EasyExcel.write(fileName, aClass)
            .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
            .needHead(true)
            .build();
        excelWriter.write(dataList, EasyExcel.writerSheet(0).head(aClass).build());
        excelWriter.finish();
    }
}

设计亮点

  • 使用泛型支持任意实体类
  • 自动设置列宽策略 LongestMatchColumnWidthStyleStrategy
  • 支持多种Excel格式(XLSX、XLS、CSV)
  • 统一的字符编码处理(UTF-8)

5.2 业务层工具类 EasyExcelBizUtils

针对业务需求,我们封装了 EasyExcelBizUtils,支持根据配置动态选择读取方式:

@Slf4j
public class EasyExcelBizUtils {

    /**
     * 根据配置导入Excel文件
     * @param in 输入流
     * @param aClass 实体类
     * @param config 文件解析配置
     * @return 数据列表
     */
    public static <T> List<T> easyImport(InputStream in, 
                                         Class<T> aClass, 
                                         FileParseConfig config) {
        log.info("开始解析Excel文件,配置:{}", JSONUtil.toJsonStr(config));
        
        ExcelReadDataTypeDTO excelReadDataType = 
            ExcelReadDataTypeMapstruct.INSTANCE.toExcelReadDataType(config);
        ExcelReadDataType readDataType = 
            ExcelReadDataType.fromName(excelReadDataType.getReadDataType());
        ExcelTypeEnum excelTypeEnum = getExcelTypeEnum(config);
        
        List<T> result;
        if (Objects.requireNonNull(readDataType) == ExcelReadDataType.HEAD_END_OFFSET) {
            result = EasyExcelUtils.easyImport(in,
                aClass,
                excelReadDataType.getHeadRowNumber(),
                excelReadDataType.getDataStartIndex(),
                excelReadDataType.getDataEndOffset(),
                config.getCharsetName(),
                excelTypeEnum);
        } else {
            result = EasyExcelUtils.easyImport(in, aClass);
        }
        
        log.info("Excel文件解析完成,读取方式:{},数据量:{}", 
            readDataType.getDesc(), CollUtil.isEmpty(result) ? 0 : result.size());
        return result;
    }

    @NotNull
    private static ExcelTypeEnum getExcelTypeEnum(FileParseConfig config) {
        ExcelTypeEnum excelTypeEnum = ExcelTypeEnum.XLSX;
        if (Objects.nonNull(config.getFileExtension())) {
            switch (config.getFileExtension()) {
                case "xlsx" -> excelTypeEnum = ExcelTypeEnum.XLSX;
                case "xls" -> excelTypeEnum = ExcelTypeEnum.XLS;
                case "csv" -> excelTypeEnum = ExcelTypeEnum.CSV;
            }
        }
        return excelTypeEnum;
    }
}

应用场景

  • 不同渠道的Excel文件格式可能不同(表头行数、数据起始行等)
  • 通过配置灵活适配各种文件格式
  • 支持部分数据读取(跳过表头、忽略尾部汇总行)

六、核心特性

6.1 内存优化

对比测试:读取100万行数据的Excel文件

框架 内存占用 耗时
Apache POI ~2GB 30s
EasyExcel ~100MB 15s

6.2 注解驱动

EasyExcel提供了丰富的注解,简化开发:

@Data
public class ProductExportDTO {
    
    @ExcelProperty(value = "商品编号", index = 0)
    private String productCode;
    
    @ExcelProperty(value = "商品名称", index = 1)
    private String productName;
    
    @ExcelProperty(value = "规格型号", index = 2)
    private String specification;
    
    @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
    @ExcelProperty(value = "创建时间", index = 3)
    private Date createTime;
    
    @NumberFormat("#.##")
    @ExcelProperty(value = "销售价格", index = 4)
    private BigDecimal salePrice;
    
    @NumberFormat("#.##")
    @ExcelProperty(value = "库存数量", index = 5)
    private Integer stockQuantity;
    
    @ExcelProperty(value = "商品状态", index = 6)
    private String productStatus;
}

6.3 监听器模式

通过监听器实现灵活的数据处理:

public class RangeReadListener<T> extends AnalysisEventListener<T> {
    private final int startIndex;
    private final String endEndKeyword;
    private final List<T> dataList = new ArrayList<>();
    private boolean shouldStop = false;

    public RangeReadListener(int startIndex, String endEndKeyword) {
        this.startIndex = startIndex;
        this.endEndKeyword = endEndKeyword;
        log.info("初始化监听器,起始行:{},结束关键字:{}", startIndex, endEndKeyword);
    }

    @Override
    public void invoke(T data, AnalysisContext context) {
        int currentIndex = context.readRowHolder().getRowIndex();
        
        if (shouldStop) {
            return;
        }
        
        if (currentIndex >= startIndex) {
            dataList.add(data);
        }
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        log.info("数据解析完成,有效数据行数:{}", dataList.size());
    }
}

七、同步与异步处理详解

7.1 同步处理

7.1.1 同步处理的特点

同步处理是指程序按照代码顺序依次执行,每个操作必须等待前一个操作完成后才能开始。在EasyExcel中,同步处理具有以下特点:

  • 简单直观:代码逻辑清晰,易于理解和维护
  • 顺序执行:严格按照读取顺序处理数据
  • 资源占用:处理期间占用主线程资源
  • 适用场景:数据量较小、处理逻辑简单、对实时性要求不高
7.1.2 项目中的同步处理应用场景

场景1:文件格式转换并导入

@Service
public class FileConvertServiceImpl implements IFileConvertService {

    @Value("${file.export}")
    private String tempExportFilePath;

    /**
     * 处理文件转换并导入
     */
    private <T> TaskRecord getTaskRecord(String fileName, 
                                         String sourceType, 
                                         List<T> datas, 
                                         Class<T> tClass) throws Exception {
        String fileExtension = ExcelTypeEnum.XLSX.getValue();
        String tempFilePath = tempExportFilePath + fileName + fileExtension;
        
        log.info("开始转换文件:{},数据量:{}", fileName, datas.size());
        
        // 同步写入Excel文件
        EasyExcelUtils.easyWrite(tempFilePath, datas, tClass);
        
        // 读取并上传
        InputStream inputStream = new FileInputStream(tempFilePath);
        String relativePath = FileUploadUtils.uploadFile(
            BusinessType.PRODUCT, 
            sourceType, 
            UploadTaskType.DATA_IMPORT, 
            inputStream, 
            null,
            fileName + fileExtension
        );
        
        TaskRecordResult recordResult = TaskRecordResult.builder()
            .fileExtension(fileExtension)
            .fileVersionId(relativePath)
            .filePath(relativePath)
            .fileName(fileName + fileExtension)
            .build();
            
        log.info("文件转换完成:{}", fileName);
            
        return TaskRecordConverter.buildTaskRecord(
            UploadTaskType.DATA_IMPORT.getCode(), 
            fileName,
            BusinessType.PRODUCT.getCode(), 
            sourceType, 
            recordResult
        );
    }
}

7.2 异步处理

7.2.1 异步处理的特点

异步处理是指程序不等待当前操作完成就继续执行后续操作,通过多线程或事件驱动机制实现并发处理。在EasyExcel中,异步处理具有以下特点:

  • 高性能:充分利用多核CPU,提升处理效率
  • 非阻塞:主线程不会被长时间占用
  • 资源优化:合理控制线程池,避免资源耗尽
  • 适用场景:大数据量处理、复杂业务逻辑、需要提升用户体验
7.2.2 项目中的异步处理应用场景

场景:大数据量异步导出

@Service
@Slf4j
public class DataExportService {
    
    @Autowired
    private ThreadPoolTaskExecutor ioIntensiveTaskExecutor;

    @Value("${file.export}")
    private String tempExportFilePath;
    
    @Autowired
    private ITaskRecordService taskRecordService;

    /**
     * 异步导出Excel文件
     */
    public String asyncExportXlsx(ExportTaskType taskType,
                                  String fileName,
                                  IEsService esService,
                                  Class clazz,
                                  Class headClazz,
                                  NativeSearchQuery searchQuery) {
        log.info("创建异步导出任务,类型:{}", taskType.getDesc());
        
        // 创建导出任务记录
        String taskId = this.createExportTask(
            taskType, 
            BusinessType.DATA, 
            ChannelType.ONLINE, 
            fileName, 
            esService, 
            clazz, 
            searchQuery
        );
        
        log.info("导出任务创建成功,任务ID:{}", taskId);
        
        // 异步执行导出任务
        ioIntensiveTaskExecutor.execute(() -> {
            createXlsxAndUpload(taskId, esService, clazz, headClazz, searchQuery);
        });
        
        return taskId;
    }

    /**
     * 生成Excel文件并上传到对象存储
     */
    public void createXlsxAndUpload(String taskId,
                                    IEsService esService,
                                    Class clazz,
                                    Class headClazz,
                                    NativeSearchQuery searchQuery) {
        log.info("开始执行导出任务:{}", taskId);
        
        TaskRecord taskRecord = taskRecordService.getById(taskId);
        String tempFilePath = tempExportFilePath + taskRecord.getFileName();
        
        // 准备更新对象
        TaskRecord updateRecord = new TaskRecord();
        updateRecord.setId(taskId);
        
        try {
            // 创建临时目录
            File dir = new File(tempExportFilePath);
            if (!dir.exists()) {
                dir.mkdirs();
            }
            
            // 初始化Excel写入器
            File file = new File(tempFilePath);
            ExcelWriter excelWriter = EasyExcel.write(file).build();
            WriteSheet writeSheet = EasyExcel.writerSheet("sheet").head(headClazz).build();
            
            // 查询总数据量
            int pageSize = 10000;
            long total = esService.searchPageList(clazz, PageRequest.of(0, 1), searchQuery).getTotal();
            
            log.info("任务[{}]数据总量:{}", taskId, total);
            
            // 更新任务状态为处理中
            updateRecord.setStatus(TaskStatus.PROCESSING.getCode());
            updateRecord.setTotalDataNum(total);
            taskRecordService.updateTaskRecord(updateRecord);
            
            if (total > pageSize) {
                // 分页处理大数据量
                processLargeData(taskId, esService, clazz, searchQuery, excelWriter, writeSheet, updateRecord, pageSize, total);
            } else {
                // 处理小数据量
                processSmallData(taskId, esService, clazz, headClazz, searchQuery, excelWriter, writeSheet, updateRecord);
            }
            
            // 完成Excel写入
            excelWriter.finish();
            
            // 上传文件到对象存储
            String downloadUrl = uploadToMinIO(tempFilePath, taskId);
            
            // 更新任务状态为成功
            updateRecord.setStatus(TaskStatus.SUCCESS.getCode());
            updateRecord.setProgress(100);
            updateRecord.setDownloadUrl(downloadUrl);
            taskRecordService.updateTaskRecord(updateRecord);
            
            log.info("任务[{}]执行完成,下载链接:{}", taskId, downloadUrl);
            
        } catch (Exception e) {
            log.error("任务[{}]执行失败", taskId, e);
            
            // 更新任务状态为失败
            updateRecord.setStatus(TaskStatus.FAIL.getCode());
            updateRecord.setErrorMsg(e.getMessage());
            taskRecordService.updateTaskRecord(updateRecord);
        } finally {
            // 删除临时文件
            deleteTempFile(tempFilePath);
        }
    }
    
    /**
     * 处理大数据量
     */
    private void processLargeData(String taskId,
                                  IEsService esService,
                                  Class clazz,
                                  NativeSearchQuery searchQuery,
                                  ExcelWriter excelWriter,
                                  WriteSheet writeSheet,
                                  TaskRecord updateRecord,
                                  int pageSize,
                                  long total) {
        long totalPages = (total + pageSize - 1) / pageSize;
        log.info("任务[{}]分页处理,总页数:{}", taskId, totalPages);
        
        for (int page = 0; page < totalPages; page++) {
            List records = esService.searchPageList(
                clazz, 
                PageRequest.of(page, pageSize), 
                searchQuery
            ).getRecords();
            
            // 写入Excel
            writeDataToExcel(updateRecord, records, excelWriter, writeSheet);
            
            // 计算进度
            long processedNum = (page + 1) * pageSize;
            if (processedNum > total) {
                processedNum = total;
            }
            int progress = (int) (processedNum * 100.0 / total);
            
            // 更新进度
            updateRecord.setProgress(progress);
            updateRecord.setProcessedDataNum(processedNum);
            
            if (progress == 100) {
                updateRecord.setStatus(TaskStatus.SUCCESS.getCode());
            }
            
            taskRecordService.updateTaskRecord(updateRecord);
            
            log.info("任务[{}]进度:{}/{} ({}%)", taskId, processedNum, total, progress);
        }
    }
    
    /**
     * 处理小数据量
     */
    private void processSmallData(String taskId,
                                  IEsService esService,
                                  Class clazz,
                                  Class headClazz,
                                  NativeSearchQuery searchQuery,
                                  ExcelWriter excelWriter,
                                  WriteSheet writeSheet,
                                  TaskRecord updateRecord) {
        List records = esService.searchPageList(
            clazz, 
            PageRequest.of(0, 10000), 
            searchQuery
        ).getRecords();
        
        writeDataToExcel(updateRecord, records, excelWriter, writeSheet);
        
        log.info("任务[{}]数据处理完成,数量:{}", taskId, records.size());
        
        // 更新进度
        updateRecord.setProgress(100);
        updateRecord.setProcessedDataNum((long) records.size());
        updateRecord.setStatus(TaskStatus.SUCCESS.getCode());
    }
    
    /**
     * 删除临时文件
     */
    private void deleteTempFile(String filePath) {
        try {
            FileUtils.deleteFile(filePath);
            log.info("临时文件已删除:{}", filePath);
        } catch (Exception e) {
            log.warn("删除临时文件失败:{}", filePath, e);
        }
    }

    /**
     * 写入Excel数据
     */
    private static void writeDataToExcel(TaskRecord taskRecord,
                                   List records,
                                   ExcelWriter excelWriter,
                                   WriteSheet writeSheet) {
        // 根据任务类型进行数据转换
        if (ExportTaskType.EXPORT_PRODUCT.getCode().equals(taskRecord.getTaskType())) {
            // 产品数据转换
            List exportDatas = convertToProductDTO(records);
            excelWriter.write(exportDatas, writeSheet);
        } else if (ExportTaskType.EXPORT_CUSTOMER.getCode().equals(taskRecord.getTaskType())) {
            // 客户数据转换
            List exportDatas = convertToCustomerDTO(records);
            excelWriter.write(exportDatas, writeSheet);
        } else if (ExportTaskType.EXPORT_ORDER.getCode().equals(taskRecord.getTaskType())) {
            // 订单数据转换
            List exportDatas = convertToOrderDTO(records);
            excelWriter.write(exportDatas, writeSheet);
        } else {
            // 默认直接写入
            excelWriter.write(records, writeSheet);
        }
    }
    
    /**
     * 转换为产品导出DTO
     */
    private static List convertToProductDTO(List records) {
        // 实现数据转换逻辑
        return records;
    }
    
    /**
     * 转换为客户导出DTO
     */
    private static List convertToCustomerDTO(List records) {
        // 实现数据转换逻辑
        return records;
    }
    
    /**
     * 转换为订单导出DTO
     */
    private static List convertToOrderDTO(List records) {
        // 实现数据转换逻辑
        return records;
    }
}

设计亮点

  1. 任务状态管理:通过 TaskRecord 记录任务状态、进度、处理数量等信息
  2. 分页处理:大数据量采用分页查询,避免内存溢出
  3. 进度实时更新:每处理完一批数据就更新进度,用户可以实时查看
  4. 异常处理:完善的异常处理机制,确保任务状态正确更新
  5. 资源清理:finally块中确保临时文件被删除
  6. 文件上传:导出完成后自动上传到对象存储
  7. 代码优化:将大方法拆分为多个小方法,提高可读性和可维护性

7.3 同步与异步对比

对比维度 同步处理 异步处理
执行方式 顺序执行,阻塞主线程 并发执行,非阻塞
内存占用 较低 较高(多线程)
处理速度 较慢 较快(充分利用多核)
代码复杂度 简单 复杂(需要处理并发)
适用数据量 小数据量(<10万行) 大数据量(>10万行)
用户体验 需要等待 立即返回,后台处理
异常处理 简单 复杂(需要处理并发异常)
资源利用 单线程 多线程,资源利用率高

7.4 最佳实践

7.4.1 线程池配置
@Configuration
public class AsyncConfig {
    
    /**
     * IO密集型任务线程池
     * 适用于Excel导入导出等IO密集型操作
     */
    @Bean("ioIntensiveTaskExecutor")
    public ThreadPoolTaskExecutor ioIntensiveTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        
        // 核心线程数:根据CPU核心数配置
        executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
        
        // 最大线程数:核心线程数的2倍
        executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors() * 2);
        
        // 队列容量
        executor.setQueueCapacity(200);
        
        // 线程名称前缀
        executor.setThreadNamePrefix("io-intensive-");
        
        // 拒绝策略:由调用线程处理
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        
        // 线程空闲时间
        executor.setKeepAliveSeconds(60);
        
        // 等待所有任务完成后再关闭线程池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(60);
        
        executor.initialize();
        return executor;
    }
}
7.4.2 任务状态管理
@Data
public class TaskRecord {
    private String id;
    private String taskType;
    private String fileExtension;
    private String businessType;
    private String channelType;
    private String filePath;
    private String fileName;
    private String status;
    private Integer progress;
    private Long processedDataNum;
    private Long totalDataNum;
    private Date createTime;
    private Date updateTime;
}

public enum TaskStatus {
    PENDING("待处理"),
    PROCESSING("处理中"),
    SUCCESS("成功"),
    FAIL("失败");
    
    private final String desc;
}

八、其他最佳实践

8.1 性能优化建议

  1. 合理设置批处理大小
private static final int BATCH_SIZE = 1000; // 根据实际情况调整
  1. 使用异步处理
CompletableFuture.runAsync(() -> {
    EasyExcel.read(filePath, Data.class, listener).sheet().doRead();
});
  1. 避免频繁IO操作
// 使用缓冲区
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(filePath));
EasyExcel.read(bis, Data.class, listener).sheet().doRead();

8.2 异常处理

public class SafeDataListener extends AnalysisEventListener<Data> {
    @Override
    public void invoke(Data data, AnalysisContext context) {
        try {
            processData(data);
        } catch (Exception e) {
            log.error("数据处理异常:{}", data, e);
            saveErrorData(data, e.getMessage());
        }
    }
}

8.3 数据校验

@Data
public class ValidatedData {
    @ExcelProperty(value = "手机号", index = 0)
    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
    private String phone;
    
    @ExcelProperty(value = "邮箱", index = 1)
    @Email(message = "邮箱格式不正确")
    private String email;
    
    @ExcelProperty(value = "年龄", index = 2)
    @Min(value = 0, message = "年龄不能小于0")
    @Max(value = 150, message = "年龄不能大于150")
    private Integer age;
}

九、总结

EasyExcel作为一款优秀的Excel处理框架,凭借其出色的内存管理和高性能处理能力,在大数据量场景下展现出巨大优势。通过注解驱动和监听器模式,大大简化了开发复杂度,提升了开发效率。

在实际项目中,我们基于 Spring Boot + EasyExcel + Elasticsearch + MinIO + Redis 的技术栈,实现了完整的百万级数据导入导出方案:

  • EasyExcel:提供高性能的Excel处理能力
  • Elasticsearch:支持海量数据的快速检索
  • MinIO:提供高性能的文件存储服务
  • Redis:提供任务状态的实时缓存
  • Spring Boot:提供统一的依赖管理和应用框架

通过封装 EasyExcelUtilsEasyExcelBizUtils 工具类,实现了灵活的Excel导入导出功能。同时,通过 DataExportService 实现了完整的异步导出机制,支持百万级数据的高效导出,并提供实时进度查看功能。

在实际项目中,EasyExcel特别适用于:

  • 大数据量的导入导出场景
  • 复杂报表的生成
  • 定时数据同步任务
  • 模板化报表输出
  • 文件格式转换并导入

同步处理和异步处理各有优势,在实际项目中应根据具体场景选择:

  • 同步处理:适用于数据量小、逻辑简单、对实时性要求不高的场景(如文件格式转换导入)
  • 异步处理:适用于数据量大、逻辑复杂、需要提升用户体验的场景(如百万级数据导出)

通过合理配置线程池、管理任务状态、选择合适的处理策略,可以充分发挥EasyExcel的性能优势,实现高效的Excel数据处理。

选择EasyExcel,让你的Excel处理更加高效、稳定!


参考资源

  • EasyExcel官方文档:https://easyexcel.opensource.alibaba.com/
  • GitHub仓库:https://github.com/alibaba/easyexcel
  • Elasticsearch官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
  • MinIO官方文档:https://min.io/docs/minio/linux/index.html
  • Redis官方文档:https://redis.io/documentation
Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐