在Java生态中处理Excel文件,我们通常面临两种选择:Apache POI和EasyExcel。POI作为老牌工具确实功能强大,但实际使用中我发现它有几个致命痛点。比如处理10万行数据时,内存占用直接飙到2GB,服务器频繁Full GC;再比如复杂样式代码写起来像在组装火箭,一个合并单元格要写十几行代码。
而EasyExcel作为阿里开源的组件,完美解决了这些问题。去年我接手一个学生档案管理系统,需要处理全省50万学生的数据导入。用POI的方案测试时,单次导入直接OOM崩溃。换成EasyExcel后,内存稳定在200MB以内,导入速度提升3倍。这主要得益于它的两大设计:
@ExcelProperty标注字段,自动完成类型转换看个直观对比:
java复制// POI读取代码片段
Workbook workbook = new XSSFWorkbook(inputStream);
Sheet sheet = workbook.getSheetAt(0);
for (Row row : sheet) {
Cell nameCell = row.getCell(0);
// 需要手动处理类型、空值、格式...
}
// EasyExcel等效代码
EasyExcel.read(inputStream, Student.class, new AnalysisEventListener() {
@Override
public void invoke(Student data, AnalysisContext context) {
// 自动完成对象转换
}
}).sheet().doRead();
实际项目中还会遇到更多痛点场景。比如教务系统要求导入时实时显示进度条,用传统方式得自己写多线程。而EasyExcel内置的AnalysisEventListener能轻松获取当前行数,配合WebSocket就能实现实时进度反馈。再比如导出时要求复杂表头合并,POI需要操作底层API,EasyExcel通过@ContentLoopMerge注解一行代码搞定。
推荐使用Spring Initializr创建项目骨架,我习惯用以下组合:
关键依赖如下:
xml复制<dependencies>
<!-- EasyExcel核心库 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.2</version>
</dependency>
<!-- 数据库相关 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- 其他省略... -->
</dependencies>
在application.yml中需要特别注意这些配置:
yaml复制spring:
servlet:
multipart:
max-file-size: 50MB # 调大文件上传限制
max-request-size: 50MB
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开发阶段开启SQL日志
有个容易踩的坑:当使用Swagger测试文件上传时,需要在配置类中添加:
java复制@Bean
public MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
factory.setMaxFileSize(DataSize.ofMegabytes(50));
factory.setMaxRequestSize(DataSize.ofMegabytes(50));
return factory.createMultipartConfig();
}
先创建学生实体类,注意注解的使用技巧:
java复制@Data
public class StudentImportDTO {
@ExcelProperty(value = "学生姓名", index = 0)
@NotBlank(message = "姓名不能为空")
private String name;
@ExcelProperty(value = "性别", index = 1)
@Pattern(regexp = "男|女", message = "性别必须为男或女")
private String sex;
@ExcelProperty(value = "学号", index = 2)
@Size(min = 10, max = 10, message = "学号必须10位")
private String studentId;
// 转换器示例:自动将字符串转为Date
@ExcelProperty(value = "出生日期", index = 3, converter = LocalDateStringConverter.class)
private LocalDate birthday;
}
这里有几个实用技巧:
converter属性处理特殊格式index属性确保即使Excel列顺序变化也能正确映射监听器是导入功能的核心,这里分享几个优化点:
java复制@Slf4j
public class StudentImportListener extends AnalysisEventListener<StudentImportDTO> {
// 每批处理500条
private static final int BATCH_SIZE = 500;
private List<StudentImportDTO> cachedList = new ArrayList<>(BATCH_SIZE);
@Override
public void invoke(StudentImportDTO data, AnalysisContext context) {
// 业务校验示例
if(data.getBirthday().isAfter(LocalDate.now())) {
throw new ExcelAnalysisException("出生日期不能晚于当前时间");
}
cachedList.add(data);
if (cachedList.size() >= BATCH_SIZE) {
saveBatch();
cachedList.clear();
}
}
@Transactional
public void saveBatch() {
// 使用MyBatis Plus的批量插入
studentService.saveBatch(cachedList.stream()
.map(this::convertToEntity)
.collect(Collectors.toList()));
}
// 其他省略...
}
文件上传接口需要特别注意异常处理:
java复制@PostMapping("/import")
public Result<?> importExcel(@RequestParam MultipartFile file) {
try {
EasyExcel.read(file.getInputStream(), StudentImportDTO.class,
new StudentImportListener())
.sheet()
.headRowNumber(2) // 跳过表头两行
.doRead();
return Result.success("导入成功");
} catch (ExcelAnalysisException e) {
log.error("数据校验失败", e);
return Result.fail(e.getMessage());
} catch (Exception e) {
log.error("系统异常", e);
return Result.fail("系统繁忙,请稍后重试");
}
}
实际项目中经常需要动态表头,可以通过构建模板实现:
java复制// 动态表头构建示例
List<List<String>> headList = new ArrayList<>();
headList.add(Collections.singletonList("学生基本信息"));
headList.add(Arrays.asList("姓名", "性别", "学号"));
List<StudentExportDTO> dataList = getExportData();
EasyExcel.write(response.getOutputStream())
.head(headList)
.sheet("学生列表")
.registerWriteHandler(new CustomStyleHandler())
.doWrite(dataList);
自定义样式处理器示例:
java复制public class CustomStyleHandler extends AbstractColumnWidthStyleStrategy {
@Override
protected void setColumnWidth(WriteSheetHolder writeSheetHolder,
List<WriteCellData<?>> cellDataList, Cell cell,
Head head, Integer relativeRowIndex, Boolean isHead) {
// 表头行高
if (isHead) {
Sheet sheet = writeSheetHolder.getSheet();
sheet.setDefaultRowHeight((short) 400);
}
// 列宽自适应
sheet.setColumnWidth(cell.getColumnIndex(), 20 * 256);
}
}
当数据量超过10万行时,需要特殊处理:
java复制// 分页查询+分批写入示例
try (ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream())
.build()) {
WriteSheet writeSheet = EasyExcel.writerSheet("学生数据").build();
int page = 1;
while (true) {
Page<Student> pageData = studentService.page(new Page<>(page, 50000));
if (pageData.getRecords().isEmpty()) break;
excelWriter.write(convertToDTOs(pageData.getRecords()), writeSheet);
page++;
}
}
在百万级数据导出场景下,我总结出这些优化点:
SXSSFWorkbook模式:java复制EasyExcel.write(fileName)
.inMemory(false) // 启用磁盘缓存
.build();
java复制.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
code复制-XX:+UseG1GC -Xms512m -Xmx2g
中文乱码问题:
java复制response.setHeader("Content-Disposition",
"attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
nginx复制charset utf-8;
内存泄漏问题:
try-with-resources语法样式不生效问题:
WriteHandler的执行顺序提供标准化模板下载能减少用户错误:
java复制@GetMapping("/template")
public void downloadTemplate(HttpServletResponse response) throws IOException {
response.setContentType("application/vnd.ms-excel");
response.setHeader("Content-disposition",
"attachment;filename=student_template.xlsx");
// 创建带示例数据的模板
List<StudentImportDTO> examples = List.of(
new StudentImportDTO("张三", "男", "20230001", LocalDate.now())
);
EasyExcel.write(response.getOutputStream(), StudentImportDTO.class)
.sheet("导入模板")
.registerWriteHandler(new TemplateStyleHandler())
.doWrite(examples);
}
处理复杂报表时经常需要多Sheet:
java复制try (ExcelWriter excelWriter = EasyExcel.write(file).build()) {
// Sheet1:基础信息
WriteSheet sheet1 = EasyExcel.writerSheet(0, "基本信息")
.head(StudentBasic.class)
.build();
excelWriter.write(getBasicData(), sheet1);
// Sheet2:成绩信息
WriteSheet sheet2 = EasyExcel.writerSheet(1, "成绩单")
.head(StudentScore.class)
.build();
excelWriter.write(getScoreData(), sheet2);
}
大文件处理时实时反馈很重要:
java复制// 前端建立WebSocket连接后
@PostMapping("/import")
public void importWithProgress(@RequestParam MultipartFile file,
@RequestParam String sessionId) {
// 获取WebSocket会话
SimpMessageSendingOperations messagingTemplate;
new Thread(() -> {
AnalysisEventListener listener = new StudentImportListener() {
@Override
public void invoke(Student data, AnalysisContext context) {
super.invoke(data, context);
// 发送进度
messagingTemplate.convertAndSend("/topic/progress/" + sessionId,
new Progress(context.readRowHolder().getRowIndex()));
}
};
EasyExcel.read(file.getInputStream(), Student.class, listener)
.sheet()
.doRead();
}).start();
}