技术背景与选型思路
在企业级应用中,面对大规模Excel文件的导入导出需求(如百万条记录),传统方式容易引发内存溢出或性能瓶颈。为解决这一问题,采用EasyPoi框架结合SAX解析模式和流式写入机制,可有效实现高吞吐、低内存占用的数据处理。
EasyPoi通过Java注解简化了POI操作,支持实体映射、数据校验、单元格样式配置等功能,并针对大数据场景提供了
importExcelBySax和
exportBigExcel等核心方法,适用于金融、通信等行业的大批量数据交互场景。
核心依赖引入
使用Maven管理项目依赖,需添加以下组件:
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-base</artifactId>
<version>4.1.0</version>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-web</artifactId>
<version>4.1.0</version>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-annotation</artifactId>
<version>4.1.0</version>
</dependency>
其中,
easypoi-annotation提供注解支持,
easypoi-base封装底层读写逻辑,
easypoi-web增强Web层集成能力。
数据模型定义
通过
@Excel注解绑定字段与Excel列关系,示例如下:
@Data
public class PhoneRecord {
@Excel(name = "手机号", width = 20)
private String mobile;
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
PhoneRecord that = (PhoneRecord) obj;
return Objects.equals(mobile, that.mobile);
}
@Override
public int hashCode() {
return Objects.hash(mobile);
}
}
该类用于表示免打扰号码列表中的每一行数据,利用HashSet进行去重操作时依赖正确的
equals与
hashCode实现。
高效工具类设计
封装通用导入导出逻辑,提升代码复用性。
大文件导入(SAX模式)
采用事件驱动的SAX解析器避免全量加载,显著降低JVM堆内存压力:
public static Set<Object> importLargeExcel(MultipartFile file, Class<?> entityClass) {
Set<Object> result = new HashSet<>();
ImportParams params = new ImportParams();
params.setHeadRows(1);
params.setNeedVerify(true);
try (InputStream is = file.getInputStream()) {
ExcelImportUtil.importExcelBySax(is, entityClass, params, new IReadHandler() {
@Override
public void handler(Object data) throws Exception {
result.add(data);
}
@Override
public void doAfterAll() {}
});
} catch (Exception e) {
throw new RuntimeException("文件解析失败", e);
}
return result;
}
海量数据导出(分页流式输出)
利用
exportBigExcel按需加载数据批次,防止OOM:
public static byte[] exportMassiveData(Collection<?> data, Class<?> clazz) {
ExportParams exportParams = new ExportParams();
exportParams.setMaxNum(1_000_000); // 单Sheet最大行数
exportParams.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportBigExcel(exportParams, clazz,
(queryParam, currentRow) -> {
return currentRow == 0 ? new ArrayList<>(data) : null;
}, 2);
try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
workbook.write(bos);
return bos.toByteArray();
} catch (IOException e) {
throw new RuntimeException("导出生成失败", e);
}
}
此实现仅在首次调用返回完整数据集,后续返回null以终止迭代。
控制器接口实现
展示如何整合业务逻辑完成过滤并导出结果。
@RestController
@RequestMapping("/api/blacklist")
public class BlacklistController {
@Autowired
private BlacklistService blacklistService;
@PostMapping("/filter-upload")
public byte[] handleFileUpload(@RequestParam("file") MultipartFile file) {
long start = System.currentTimeMillis();
// 步骤1:解析上传文件
Set<PhoneRecord> uploadSet = (Set<PhoneRecord>) ExcelUtils.importLargeExcel(file, PhoneRecord.class);
log.info("文件解析完成,共{}条,耗时{}ms", uploadSet.size(), System.currentTimeMillis() - start);
start = System.currentTimeMillis();
// 步骤2:加载数据库已存在号码
Set<PhoneRecord> storedSet = blacklistService.loadAllAsSet();
log.info("历史数据加载完成,共{}条,耗时{}ms", storedSet.size(), System.currentTimeMillis() - start);
start = System.currentTimeMillis();
// 步骤3:差集计算 —— 排除已存在的号码
uploadSet.removeAll(storedSet);
log.info("过滤后剩余{}条新号码,耗时{}ms", uploadSet.size(), System.currentTimeMillis() - start);
// 步骤4:返回导出字节数组
return ExcelUtils.exportMassiveData(uploadSet, PhoneRecord.class);
}
@GetMapping("/template")
public void downloadTemplate(HttpServletResponse response) throws IOException {
ExcelUtils.exportExcel(response, "黑名单导入模板", Collections.emptyList(), PhoneRecord.class);
}
}
上述流程实现了从上传、比对到导出的闭环处理,适用于黑名单更新、白名单校验等高频场景。
性能优化建议
- 内存控制:对于超大规模数据,建议启用分片处理或将中间结果落盘。
- 并发安全:若涉及多线程读写,应使用ConcurrentHashMap或加锁机制保护共享资源。
- 异常恢复:关键任务应记录处理进度,支持断点续传。
- 前端提示:导出耗时较长时,可通过WebSocket推送状态更新。