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处理方案。