Java表格处理详解以及结合实际项目使用

发布于:2025-08-30 ⋅ 阅读:(22) ⋅ 点赞:(0)

Java表格处理详解以及结合项目使用

目录

Java表格处理概述

1. 表格处理的重要性

在企业级应用中,表格处理是一个非常重要的功能模块,主要用于:

  • 数据导入: 批量导入业务数据,如学生信息、用户数据等
  • 数据导出: 导出业务数据为Excel格式,便于用户查看和分析
  • 数据交换: 不同系统间的数据交换和迁移
  • 报表生成: 生成各种业务报表和统计信息
  • 模板管理: 提供标准化的数据模板

2. 表格文件格式

Java主要处理的表格格式包括:

  • Excel 97-2003: .xls 格式,使用HSSF API
  • Excel 2007+: .xlsx 格式,使用XSSF API
  • CSV: 逗号分隔值文件
  • TSV: 制表符分隔值文件

主流表格处理框架对比

1. Apache POI

1.1 基本介绍

Apache POI是Apache软件基金会的开源项目,提供了完整的Java API来操作Microsoft Office文档。

1.2 优势特点
  • 功能完整: 支持所有Excel功能,包括样式、公式、图表等
  • 成熟稳定: 经过多年发展,社区活跃,文档完善
  • 灵活性强: 可以精确控制每个单元格的格式和内容
  • 官方支持: Apache官方项目,更新维护及时
1.3 劣势不足
  • 内存占用大: 大文件处理时内存消耗较高
  • API复杂: 需要编写较多代码来实现简单功能
  • 性能一般: 大数据量处理性能相对较低

2. EasyPoi

2.1 基本介绍

EasyPoi是基于POI的封装工具,简化了Excel的导入导出操作,提供了注解驱动的开发方式。

2.2 优势特点
  • 注解驱动: 使用注解配置,代码简洁
  • 功能丰富: 支持导入导出、模板导出、图片导出等
  • 易于使用: 学习成本低,开发效率高
  • 集成方便: 与Spring Boot集成良好
2.3 劣势不足
  • 功能限制: 某些复杂场景下功能受限
  • 性能问题: 大数据量处理时存在稳定性问题
  • 定制性差: 高度封装导致定制化能力有限

3. EasyExcel

3.1 基本介绍

EasyExcel是阿里巴巴开源的Excel处理工具,基于POI但进行了大量优化,特别适合大数据量处理。

3.2 优势特点
  • 性能优秀: 采用流式读写,内存占用低
  • 大数据支持: 专门针对大数据量场景优化
  • 异步处理: 支持异步读写,提高性能
  • 功能完整: 支持导入导出、样式设置等
3.3 劣势不足
  • 学习成本: 相对POI需要学习新的API
  • 生态相对: 相比POI生态相对较小
  • 版本兼容: 不同版本间可能存在兼容性问题

4. 框架对比总结

框架 适用场景 性能 易用性 功能完整性 学习成本
Apache POI 复杂Excel操作、样式控制 中等 中等 最高
EasyPoi 简单导入导出、快速开发 中等 中等
EasyExcel 大数据量处理、性能要求高 中等 中等

项目实际使用分析

1. 项目依赖配置

基于对项目代码的分析,该项目采用了混合使用的策略:

1.1 父POM依赖
<!-- EasyPoi依赖 -->
<dependency>
    <groupId>cn.afterturn</groupId>
    <artifactId>easypoi-spring-boot-starter</artifactId>
    <version>4.4.0</version>
</dependency>

<!-- EasyExcel依赖 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>3.3.2</version>
</dependency>
1.2 使用策略分析
  • EasyPoi: 主要用于中小数据量的导入导出,开发效率高
  • EasyExcel: 用于大数据量场景,如学生名单导出、批量数据处理
  • 原生POI: 在特定场景下使用,如复杂样式控制、模板处理

2. 项目使用场景分布

2.1 数据导入场景
  • 学生信息批量导入
  • 用户角色批量导入
  • 审核数据批量导入
  • 学籍信息批量导入
2.2 数据导出场景
  • 学生名单导出
  • 审核结果导出
  • 统计报表导出
  • 摇号结果导出

具体代码用例

1. EasyPoi使用示例

1.1 实体类注解配置
public class UserRoleImportVo {
  
    @Excel(name = "*身份证号", orderNum = "1", width = 20)
    private String idCard;
  
    @Excel(name = "*姓名", orderNum = "2", width = 10)
    private String name;
  
    @Excel(name = "*手机号", orderNum = "3", width = 20)
    private String phone;
  
    @Excel(name = "*角色", orderNum = "4", width = 20)
    private String role;
  
    @Excel(name = "*组织", orderNum = "5", width = 20)
    private String organization;
  
    @Excel(name = "学段", orderNum = "6", width = 20)
    private String phase;
  
    @Excel(name = "*学校/机构", orderNum = "7", width = 30)
    private String school;
  
    @Excel(name = "错误信息", orderNum = "8", width = 30)
    private String errorMsg;
  
    // getter和setter方法
}
1.2 数据导入实现
@PostMapping("/import")
public ResultVO importUserRoles(@RequestPart("file") MultipartFile file) {
    try {
        // 设置导入参数
        ImportParams params = new ImportParams();
        params.setTitleRows(0);        // 标题行数
        params.setHeadRows(1);         // 表头行数
        params.setNeedVerify(true);    // 是否需要校验
      
        // 执行导入
        ExcelImportResult<UserRoleImportVo> result = 
            ExcelImportUtil.importExcelMore(file.getInputStream(), UserRoleImportVo.class, params);
      
        List<UserRoleImportVo> successList = result.getList();
        List<UserRoleImportVo> failList = result.getFailList();
      
        // 处理导入结果
        if (!failList.isEmpty()) {
            // 生成错误报告
            String errorReport = generateErrorReport(failList);
            return ResultUtils.error("导入完成,但有部分数据失败", errorReport);
        }
      
        // 批量保存成功数据
        userRoleService.batchSave(successList);
      
        return ResultUtils.success("导入成功,共导入" + successList.size() + "条数据");
      
    } catch (Exception e) {
        log.error("用户角色导入失败", e);
        return ResultUtils.error("导入失败:" + e.getMessage());
    }
}
1.3 数据导出实现
@GetMapping("/export")
public void exportUserRoles(HttpServletResponse response, 
                           @RequestParam(required = false) String keyword) {
    try {
        // 查询数据
        List<UserRoleExportVo> dataList = userRoleService.queryForExport(keyword);
      
        // 设置响应头
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setHeader("Content-Disposition", 
            "attachment;filename=user_roles_" + DateUtils.getCurrentDate() + ".xlsx");
      
        // 执行导出
        ExportParams params = new ExportParams("用户角色列表", "用户角色");
        params.setStyle(ExcelExportStyleDefaultImpl.class);
      
        Workbook workbook = ExcelExportUtil.exportExcel(params, UserRoleExportVo.class, dataList);
        workbook.write(response.getOutputStream());
        workbook.close();
      
    } catch (Exception e) {
        log.error("用户角色导出失败", e);
        throw new RuntimeException("导出失败");
    }
}

2. EasyExcel使用示例

2.1 大数据量导出
@GetMapping("/exportLarge")
public void exportLargeData(HttpServletResponse response) {
    try {
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setHeader("Content-Disposition", 
            "attachment;filename=large_data_" + DateUtils.getCurrentDate() + ".xlsx");
      
        // 使用EasyExcel的流式写入
        EasyExcel.write(response.getOutputStream(), StudentExportVo.class)
                .sheet("学生信息")
                .doWrite(() -> {
                    // 分页查询数据,避免内存溢出
                    return studentService.queryForExportInBatches();
                });
              
    } catch (Exception e) {
        log.error("大数据量导出失败", e);
        throw new RuntimeException("导出失败");
    }
}
2.2 自定义样式导出
@GetMapping("/exportWithStyle")
public void exportWithCustomStyle(HttpServletResponse response) {
    try {
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setHeader("Content-Disposition", 
            "attachment;filename=styled_data.xlsx");
      
        // 自定义样式
        WriteCellStyle headerStyle = new WriteCellStyle();
        headerStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
        headerStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND);
      
        WriteCellStyle contentStyle = new WriteCellStyle();
        contentStyle.setFillForegroundColor(IndexedColors.WHITE.getIndex());
      
        // 应用样式
        EasyExcel.write(response.getOutputStream(), StudentExportVo.class)
                .registerWriteHandler(new HorizontalCellStyleStrategy(headerStyle, contentStyle))
                .sheet("学生信息")
                .doWrite(studentService.queryAll());
              
    } catch (Exception e) {
        log.error("样式导出失败", e);
        throw new RuntimeException("导出失败");
    }
}

3. 原生POI使用示例

3.1 复杂模板处理
public void processExcelTemplate(MultipartFile file, HttpServletResponse response) {
    try {
        // 读取模板文件
        Workbook workbook = new XSSFWorkbook(file.getInputStream());
        Sheet sheet = workbook.getSheetAt(0);
      
        // 填充数据到指定位置
        fillDataToTemplate(sheet, getBusinessData());
      
        // 设置响应头
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setHeader("Content-Disposition", "attachment;filename=processed_template.xlsx");
      
        // 输出文件
        workbook.write(response.getOutputStream());
        workbook.close();
      
    } catch (Exception e) {
        log.error("模板处理失败", e);
        throw new RuntimeException("模板处理失败");
    }
}

private void fillDataToTemplate(Sheet sheet, List<BusinessData> dataList) {
    int startRow = 5; // 数据开始行
  
    for (int i = 0; i < dataList.size(); i++) {
        BusinessData data = dataList.get(i);
        Row row = sheet.createRow(startRow + i);
      
        // 填充各个字段
        createCell(row, 0, data.getName());
        createCell(row, 1, data.getAge());
        createCell(row, 2, data.getScore());
      
        // 设置样式
        applyRowStyle(row);
    }
}

private void createCell(Row row, int colIndex, Object value) {
    Cell cell = row.createCell(colIndex);
    if (value instanceof String) {
        cell.setCellValue((String) value);
    } else if (value instanceof Integer) {
        cell.setCellValue((Integer) value);
    } else if (value instanceof Double) {
        cell.setCellValue((Double) value);
    }
}

4. 项目实际工具类

4.1 ExcelUtils工具类
@Component
public class ExcelUtils {
  
    /**
     * 导出数据到指定文件
     */
    public static void exportExcel(File file, ExportSheetVO export) {
        ExportParams params = new ExportParams(
            export.getTitle(),
            Optional.ofNullable(export.getSheetName()).orElse("文件导出"), 
            ExcelType.XSSF
        );
      
        Workbook workbook = ExcelExportUtil.exportExcel(
            params, 
            export.getPojoClass(),
            Optional.ofNullable(export.getDataSet()).orElse(new ArrayList<>())
        );
      
        try (FileOutputStream fos = new FileOutputStream(file)) {
            workbook.write(fos);
        } catch (Exception e) {
            throw new RuntimeException("导出失败", e);
        }
    }
  
    /**
     * 读取Excel数据
     */
    public static <T> List<T> readExcel(String pathName, Integer titleRows, 
                                       Integer headerRows, Class<T> pojoClass) {
        ImportParams params = new ImportParams();
        params.setTitleRows(titleRows);
        params.setHeadRows(headerRows);
      
        try {
            return ExcelImportUtil.importExcel(new File(pathName), pojoClass, params);
        } catch (Exception e) {
            throw new RuntimeException("读取Excel失败", e);
        }
    }
  
    /**
     * 检查Excel表头
     */
    public static boolean checkExcelHeaders(MultipartFile file, List<String> headers) {
        try {
            InputStream inputStream = file.getInputStream();
            List<String> actualHeaders = new ArrayList<>();
          
            Workbook workbook = new XSSFWorkbook(inputStream);
            Sheet sheet = workbook.getSheetAt(0);
            Row row = sheet.getRow(0);
          
            for (int i = 0; i < row.getLastCellNum(); i++) {
                Cell cell = row.getCell(i);
                if (StringUtils.isNotEmpty(cell.getStringCellValue())) {
                    actualHeaders.add(cell.getStringCellValue());
                }
            }
          
            return headers.size() == actualHeaders.size() && 
                   headers.containsAll(actualHeaders);
                 
        } catch (Exception e) {
            log.error("检查Excel表头失败", e);
            return false;
        }
    }
}
4.2 ExportUtils导出工具类
@Slf4j
public class ExportUtils {
  
    /**
     * 大数据量导出方法 - 使用纯POI API
     */
    public static <T> void exportLargeDataSimple(HttpServletResponse response, 
                                               String fileName, String title, 
                                               String sheetName, Class<T> clazz, 
                                               List<T> dataList) {
        try {
            long start = System.currentTimeMillis();
          
            // 设置响应头
            response.setHeader("content-Type", 
                "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
            fileName = URLEncoder.encode(fileName + "-" + getDate("yyyyMMddHHmmss"), "UTF-8") + ".xlsx";
            response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
            response.setCharacterEncoding("UTF-8");
          
            // 使用SXSSFWorkbook,内存友好
            try (SXSSFWorkbook workbook = new SXSSFWorkbook(100)) {
                Sheet sheet = workbook.createSheet(sheetName);
              
                // 获取@Excel注解的字段
                List<FieldInfo> fieldInfos = getExcelFields(clazz);
              
                // 创建表头
                createHeaderRow(sheet, fieldInfos, title);
              
                // 分批写入数据,避免内存溢出
                int batchSize = 5000;
                int currentRowIndex = 1;
              
                for (int i = 0; i < dataList.size(); i += batchSize) {
                    int endIndex = Math.min(i + batchSize, dataList.size());
                    List<T> batch = dataList.subList(i, endIndex);
                  
                    // 写入批次数据
                    for (T data : batch) {
                        Row row = sheet.createRow(currentRowIndex++);
                        fillRowData(row, data, fieldInfos);
                    }
                  
                    // 清理内存
                    if (i % (batchSize * 10) == 0) {
                        System.gc();
                    }
                }
              
                // 输出文件
                workbook.write(response.getOutputStream());
            }
          
            long end = System.currentTimeMillis();
            log.info("导出Excel耗时:{}ms", end - start);
          
        } catch (Exception e) {
            log.error("导出失败", e);
            throw new RuntimeException("导出失败");
        }
    }
  
    /**
     * 获取Excel字段信息
     */
    private static <T> List<FieldInfo> getExcelFields(Class<T> clazz) {
        java.lang.reflect.Field[] fields = clazz.getDeclaredFields();
        List<FieldInfo> fieldInfos = new ArrayList<>();
      
        for (java.lang.reflect.Field field : fields) {
            if (field.isAnnotationPresent(cn.afterturn.easypoi.excel.annotation.Excel.class)) {
                cn.afterturn.easypoi.excel.annotation.Excel excel = 
                    field.getAnnotation(cn.afterturn.easypoi.excel.annotation.Excel.class);
                field.setAccessible(true);
              
                String orderStr = excel.orderNum();
                int orderNum = 0;
                try {
                    orderNum = Integer.parseInt(orderStr);
                } catch (NumberFormatException e) {
                    orderNum = 0;
                }
              
                fieldInfos.add(new FieldInfo(field, excel.name(), orderNum));
            }
        }
      
        fieldInfos.sort((a, b) -> Integer.compare(a.getOrderNum(), b.getOrderNum()));
        return fieldInfos;
    }
}

重难点分析

1. 大数据量处理

难点描述
  • 内存溢出: 一次性加载大量数据到内存
  • 性能问题: 处理速度慢,用户体验差
  • 稳定性差: 长时间运行容易崩溃
解决方案
// 1. 使用流式处理
public void exportLargeDataStream(HttpServletResponse response, List<Data> dataList) {
    try (SXSSFWorkbook workbook = new SXSSFWorkbook(100)) { // 内存中只保留100行
        Sheet sheet = workbook.createSheet("数据");
      
        // 分批处理
        int batchSize = 5000;
        for (int i = 0; i < dataList.size(); i += batchSize) {
            int endIndex = Math.min(i + batchSize, dataList.size());
            List<Data> batch = dataList.subList(i, endIndex);
          
            // 处理批次数据
            processBatch(sheet, batch, i / batchSize);
          
            // 定期清理内存
            if (i % (batchSize * 10) == 0) {
                System.gc();
            }
        }
      
        workbook.write(response.getOutputStream());
    }
}

// 2. 异步处理
@Async
public CompletableFuture<String> exportLargeDataAsync(List<Data> dataList) {
    String fileName = "export_" + System.currentTimeMillis() + ".xlsx";
    String filePath = "/tmp/" + fileName;
  
    try (SXSSFWorkbook workbook = new SXSSFWorkbook(100)) {
        // 异步处理逻辑
        processDataAsync(workbook, dataList);
        workbook.write(new FileOutputStream(filePath));
      
        return CompletableFuture.completedFuture(fileName);
    } catch (Exception e) {
        return CompletableFuture.failedFuture(e);
    }
}

2. 数据验证和错误处理

难点描述
  • 数据格式: Excel中数据格式不统一
  • 数据完整性: 必填字段缺失、数据重复等
  • 错误反馈: 如何向用户提供清晰的错误信息
解决方案
// 1. 数据验证器
@Component
public class ExcelDataValidator {
  
    public ValidationResult validateStudentData(StudentImportVo data) {
        ValidationResult result = new ValidationResult();
      
        // 必填字段验证
        if (StringUtils.isBlank(data.getName())) {
            result.addError("姓名不能为空");
        }
      
        if (StringUtils.isBlank(data.getIdCard())) {
            result.addError("身份证号不能为空");
        } else if (!IdCardValidator.isValid(data.getIdCard())) {
            result.addError("身份证号格式不正确");
        }
      
        // 数据格式验证
        if (data.getAge() != null && (data.getAge() < 3 || data.getAge() > 25)) {
            result.addError("年龄必须在3-25岁之间");
        }
      
        // 业务规则验证
        if (!validateBusinessRules(data)) {
            result.addError("不符合业务规则");
        }
      
        return result;
    }
}

// 2. 批量验证和错误收集
public ExcelImportResult<StudentImportVo> importWithValidation(MultipartFile file) {
    ImportParams params = new ImportParams();
    params.setNeedVerify(true);
    params.setVerifyHandler(new StudentVerifyHandler());
  
    try {
        ExcelImportResult<StudentImportVo> result = 
            ExcelImportUtil.importExcelMore(file.getInputStream(), StudentImportVo.class, params);
      
        // 处理验证结果
        List<StudentImportVo> successList = new ArrayList<>();
        List<StudentImportVo> failList = new ArrayList<>();
      
        for (StudentImportVo data : result.getList()) {
            ValidationResult validation = validator.validateStudentData(data);
            if (validation.isValid()) {
                successList.add(data);
            } else {
                data.setErrorMsg(String.join("; ", validation.getErrors()));
                failList.add(data);
            }
        }
      
        // 生成错误报告
        if (!failList.isEmpty()) {
            generateErrorReport(failList);
        }
      
        return result;
      
    } catch (Exception e) {
        throw new RuntimeException("导入失败", e);
    }
}

3. 样式和格式控制

难点描述
  • 样式复杂: 需要设置多种样式,如颜色、字体、边框等
  • 格式统一: 保持导出文件格式的一致性
  • 性能影响: 样式设置对性能的影响
解决方案
// 1. 样式工厂模式
@Component
public class ExcelStyleFactory {
  
    private final Map<String, CellStyle> styleCache = new ConcurrentHashMap<>();
  
    public CellStyle getHeaderStyle(Workbook workbook) {
        return styleCache.computeIfAbsent("header", key -> createHeaderStyle(workbook));
    }
  
    public CellStyle getContentStyle(Workbook workbook) {
        return styleCache.computeIfAbsent("content", key -> createContentStyle(workbook));
    }
  
    public CellStyle getErrorStyle(Workbook workbook) {
        return styleCache.computeIfAbsent("error", key -> createErrorStyle(workbook));
    }
  
    private CellStyle createHeaderStyle(Workbook workbook) {
        CellStyle style = workbook.createCellStyle();
        Font font = workbook.createFont();
        font.setBold(true);
        font.setColor(IndexedColors.WHITE.getIndex());
      
        style.setFont(font);
        style.setFillForegroundColor(IndexedColors.BLUE.getIndex());
        style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        style.setBorderTop(BorderStyle.THIN);
        style.setBorderBottom(BorderStyle.THIN);
        style.setBorderLeft(BorderStyle.THIN);
        style.setBorderRight(BorderStyle.THIN);
        style.setAlignment(HorizontalAlignment.CENTER);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
      
        return style;
    }
}

// 2. 条件样式
public class ConditionalStyleHandler implements WriteHandler {
  
    @Override
    public void afterCellDispose(WriteSheetHolder writeSheetHolder, 
                                WriteTableHolder writeTableHolder, 
                                List<CellData> cellDataList, 
                                Cell cell, 
                                Head head, 
                                Integer relativeRowIndex, 
                                Boolean isHead) {
      
        if (isHead) {
            return;
        }
      
        // 根据数据内容设置样式
        String cellValue = cell.getStringCellValue();
        if (StringUtils.isNotEmpty(cellValue)) {
            if (cellValue.contains("错误")) {
                cell.setCellStyle(getErrorStyle(writeSheetHolder.getSheet().getWorkbook()));
            } else if (cellValue.contains("警告")) {
                cell.setCellStyle(getWarningStyle(writeSheetHolder.getSheet().getWorkbook()));
            }
        }
    }
}

4. 并发和性能优化

难点描述
  • 并发冲突: 多用户同时导出时的资源竞争
  • 性能瓶颈: 单线程处理大数据量效率低
  • 资源管理: 内存和CPU资源的合理分配
解决方案
// 1. 线程池管理
@Configuration
public class ExcelExportConfig {
  
    @Bean("excelExportExecutor")
    public ExecutorService excelExportExecutor() {
        return new ThreadPoolExecutor(
            5,                      // 核心线程数
            10,                     // 最大线程数
            60L,                    // 空闲时间
            TimeUnit.SECONDS,       // 时间单位
            new LinkedBlockingQueue<>(100),  // 工作队列
            new ThreadFactoryBuilder().setNameFormat("excel-export-%d").build(),
            new ThreadPoolExecutor.CallerRunsPolicy()  // 拒绝策略
        );
    }
}

// 2. 异步导出服务
@Service
public class AsyncExportService {
  
    @Autowired
    @Qualifier("excelExportExecutor")
    private ExecutorService executorService;
  
    public CompletableFuture<String> exportAsync(ExportRequest request) {
        return CompletableFuture.supplyAsync(() -> {
            try {
                return processExport(request);
            } catch (Exception e) {
                log.error("异步导出失败", e);
                throw new CompletionException(e);
            }
        }, executorService);
    }
  
    private String processExport(ExportRequest request) {
        // 导出处理逻辑
        String fileName = "export_" + System.currentTimeMillis() + ".xlsx";
        String filePath = "/tmp/exports/" + fileName;
      
        try (SXSSFWorkbook workbook = new SXSSFWorkbook(100)) {
            // 处理导出逻辑
            processExportData(workbook, request);
            workbook.write(new FileOutputStream(filePath));
            return fileName;
        } catch (Exception e) {
            throw new RuntimeException("导出处理失败", e);
        }
    }
}

// 3. 进度监控
public class ExportProgressMonitor {
  
    private final Map<String, ExportProgress> progressMap = new ConcurrentHashMap<>();
  
    public void updateProgress(String taskId, int current, int total) {
        ExportProgress progress = progressMap.computeIfAbsent(taskId, 
            k -> new ExportProgress());
        progress.setCurrent(current);
        progress.setTotal(total);
        progress.setPercentage((int) ((double) current / total * 100));
        progress.setUpdateTime(new Date());
    }
  
    public ExportProgress getProgress(String taskId) {
        return progressMap.get(taskId);
    }
  
    public void removeProgress(String taskId) {
        progressMap.remove(taskId);
    }
}

最佳实践建议

1. 框架选择策略

1.1 根据数据量选择
// 小数据量(<1000条):使用EasyPoi
if (dataSize < 1000) {
    return exportWithEasyPoi(dataList, response);
}

// 中等数据量(1000-10000条):使用EasyExcel
else if (dataSize < 10000) {
    return exportWithEasyExcel(dataList, response);
}

// 大数据量(>10000条):使用原生POI + 流式处理
else {
    return exportWithNativePoi(dataList, response);
}
1.2 根据功能复杂度选择
// 简单导入导出:EasyPoi
if (isSimpleImportExport()) {
    return useEasyPoi();
}

// 需要样式控制:EasyExcel
else if (needStyleControl()) {
    return useEasyExcel();
}

// 复杂模板处理:原生POI
else if (needTemplateProcessing()) {
    return useNativePoi();
}

2. 性能优化建议

// 1. 使用缓存
@Component
public class ExcelStyleCache {
  
    private final Map<String, CellStyle> styleCache = new ConcurrentHashMap<>();
  
    public CellStyle getStyle(String key, Supplier<CellStyle> styleSupplier) {
        return styleCache.computeIfAbsent(key, k -> styleSupplier.get());
    }
}

// 2. 批量处理
public void processDataInBatches(List<Data> dataList, int batchSize) {
    for (int i = 0; i < dataList.size(); i += batchSize) {
        int endIndex = Math.min(i + batchSize, dataList.size());
        List<Data> batch = dataList.subList(i, endIndex);
      
        // 处理批次数据
        processBatch(batch);
      
        // 定期清理内存
        if (i % (batchSize * 10) == 0) {
            System.gc();
        }
    }
}

// 3. 异步处理
@Async
public CompletableFuture<String> processLargeFileAsync(MultipartFile file) {
    return CompletableFuture.supplyAsync(() -> {
        // 异步处理逻辑
        return processFile(file);
    });
}

3. 错误处理最佳实践

// 1. 统一异常处理
@ControllerAdvice
public class ExcelExceptionHandler {
  
    @ExceptionHandler(ExcelImportException.class)
    public ResponseEntity<ErrorResponse> handleExcelImportException(ExcelImportException e) {
        ErrorResponse error = new ErrorResponse();
        error.setMessage("Excel导入失败");
        error.setDetails(e.getErrors());
        error.setTimestamp(new Date());
      
        return ResponseEntity.badRequest().body(error);
    }
  
    @ExceptionHandler(ExcelExportException.class)
    public ResponseEntity<ErrorResponse> handleExcelExportException(ExcelExportException e) {
        ErrorResponse error = new ErrorResponse();
        error.setMessage("Excel导出失败");
        error.setDetails(e.getMessage());
        error.setTimestamp(new Date());
      
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
    }
}

// 2. 错误信息收集
public class ExcelErrorCollector {
  
    private final List<ExcelError> errors = new ArrayList<>();
  
    public void addError(int row, int col, String message) {
        errors.add(new ExcelError(row, col, message));
    }
  
    public void addError(int row, String field, String message) {
        errors.add(new ExcelError(row, field, message));
    }
  
    public boolean hasErrors() {
        return !errors.isEmpty();
    }
  
    public List<ExcelError> getErrors() {
        return new ArrayList<>(errors);
    }
  
    public String generateErrorReport() {
        if (errors.isEmpty()) {
            return "无错误";
        }
      
        StringBuilder report = new StringBuilder();
        report.append("发现 ").append(errors.size()).append(" 个错误:\n");
      
        for (ExcelError error : errors) {
            report.append("第").append(error.getRow()).append("行");
            if (error.getField() != null) {
                report.append(",字段:").append(error.getField());
            }
            report.append(":").append(error.getMessage()).append("\n");
        }
      
        return report.toString();
    }
}

4. 监控和日志

// 1. 性能监控
@Aspect
@Component
public class ExcelPerformanceMonitor {
  
    private static final Logger log = LoggerFactory.getLogger(ExcelPerformanceMonitor.class);
  
    @Around("@annotation(MonitorExcel)")
    public Object monitorExcelOperation(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        String methodName = joinPoint.getSignature().getName();
      
        try {
            Object result = joinPoint.proceed();
          
            long endTime = System.currentTimeMillis();
            long duration = endTime - startTime;
          
            log.info("Excel操作 {} 执行完成,耗时:{}ms", methodName, duration);
          
            // 记录性能指标
            recordPerformanceMetrics(methodName, duration);
          
            return result;
          
        } catch (Exception e) {
            long endTime = System.currentTimeMillis();
            long duration = endTime - startTime;
          
            log.error("Excel操作 {} 执行失败,耗时:{}ms,错误:{}", 
                methodName, duration, e.getMessage(), e);
          
            throw e;
        }
    }
}

// 2. 操作日志
@Component
public class ExcelOperationLogger {
  
    private static final Logger log = LoggerFactory.getLogger(ExcelOperationLogger.class);
  
    public void logImportOperation(String fileName, int totalRows, int successRows, 
                                  int failRows, String operator) {
        log.info("Excel导入操作 - 文件:{},总行数:{},成功:{},失败:{},操作人:{}", 
            fileName, totalRows, successRows, failRows, operator);
    }
  
    public void logExportOperation(String fileName, int totalRows, String operator) {
        log.info("Excel导出操作 - 文件:{},总行数:{},操作人:{}", 
            fileName, totalRows, operator);
    }
  
    public void logError(String operation, String fileName, String error, String operator) {
        log.error("Excel操作失败 - 操作:{},文件:{},错误:{},操作人:{}", 
            operation, fileName, error, operator);
    }
}

总结

本项目采用了混合使用的Excel处理策略,具有以下特点:

1. 技术栈完整

  • EasyPoi: 中小数据量快速开发
  • EasyExcel: 大数据量性能优化
  • 原生POI: 复杂场景精确控制

2. 性能优化到位

  • 流式处理避免内存溢出
  • 分批处理提高处理效率
  • 异步处理改善用户体验

3. 错误处理完善

  • 数据验证和错误收集
  • 详细的错误报告生成
  • 异常情况的优雅处理

4. 扩展性良好

  • 支持多种导出格式
  • 可定制的样式控制
  • 灵活的配置选项

这种设计既保证了开发效率,又满足了不同场景的性能需求,是一个值得参考的Excel处理方案。


网站公告

今日签到

点亮在社区的每一天
去签到