前言:
在 Java 中处理 Excel 的导入(读取)和导出(写入),目前业界最主流、最好用的库是 EasyExcel(阿里巴巴开源)。
相比传统的 Apache POI ,EasyExcel 最大的优势是:节省内存。
- POI :一次性把整个 Excel 加载到内存,文件大一点(如 10MB+)就容易 OutOfMemoryError。
- EasyExcel:采用"一行一行解析"的模式,无论文件多大,内存占用都极低。
第一步:引入依赖
在你的 pom.xml 中添加 EasyExcel 依赖:
xml
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.4</version> <!-- 建议使用最新稳定版 -->
</dependency>
第二步:Excel 导入(读取)
导入的核心逻辑 是:定义一个 DTO 类映射 Excel 列 -> 监听器读取数据 -> 批量存入数据库。
1. 定义接收数据的 DTO
使用 @ExcelProperty 注解指定 Excel 中的列名或索引。
java
package com.example.todo.dto;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
@Data
public class ImportTodoDTO {
// index = 0 表示第一列,或者用 value = "任务标题" 匹配表头名称
@ExcelProperty(value = "任务标题", index = 0)
private String title;
@ExcelProperty(value = "是否完成", index = 1)
private Boolean completed;
@ExcelProperty(value = "截止时间", index = 2)
private String deadlineStr; // 建议先用 String 接,再手动转换,避免格式报错
}
2. 创建监听器 (Listener)
这是 EasyExcel 的核心。它会在读取每一行时触发方法。我们需要在这里收集数据,并分批插入数据库,防止一次性插入太多导致性能问题。
java
package com.example.todo.listener;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.example.todo.dto.ImportTodoDTO;
import com.example.todo.entity.Todo;
import com.example.todo.mapper.TodoMapper;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
@Slf4j
public class TodoImportListener extends AnalysisEventListener<ImportTodoDTO> {
// 每批处理的数据量
private static final int BATCH_COUNT = 100;
// 缓存列表
private List<ImportTodoDTO> cachedDataList = new ArrayList<>(BATCH_COUNT);
private final TodoMapper todoMapper;
public TodoImportListener(TodoMapper todoMapper) {
this.todoMapper = todoMapper;
}
/**
* 每解析一行数据,就会调用一次这个方法
*/
@Override
public void invoke(ImportTodoDTO data, AnalysisContext context) {
log.info("解析到一条数据: {}", data);
cachedDataList.add(data);
// 达到阈值,执行批量入库
if (cachedDataList.size() >= BATCH_COUNT) {
saveData();
cachedDataList.clear(); // 清空缓存
}
}
/**
* 所有数据解析完成后,会调用这个方法
* 用于处理最后剩余不足 BATCH_COUNT 的数据
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
saveData(); // 确保最后一批数据也被保存
log.info("所有数据解析完成!");
}
/**
* 将缓存中的数据转换并插入数据库
*/
private void saveData() {
log.info("{}条数据,开始存储数据库!", cachedDataList.size());
List<Todo> todoList = new ArrayList<>();
for (ImportTodoDTO dto : cachedDataList) {
Todo todo = new Todo();
todo.setTitle(dto.getTitle());
todo.setCompleted(dto.getCompleted());
// 这里可以添加简单的日期解析逻辑
todoList.add(todo);
}
// 调用 Mapper 批量插入
todoMapper.insertBatch(todoList);
log.info("存储数据库成功!");
}
}
3. Service 层调用
java
@Service
public class TodoServiceImpl {
@Autowired
private TodoMapper todoMapper;
public void importTodos(MultipartFile file) throws IOException {
// 获取 Listener 实例,传入 Mapper 以便入库
TodoImportListener listener = new TodoImportListener(todoMapper);
// 开始读取
EasyExcel.read(file.getInputStream(), ImportTodoDTO.class, listener)
.sheet() // 默认读取第一个 sheet
.doRead();
}
}
4. Controller 层
java
@PostMapping("/import")
public Result importExcel(@RequestParam("file") MultipartFile file) {
try {
todoService.importTodos(file);
return Result.ok("导入成功");
} catch (Exception e) {
return Result.fail("导入失败: " + e.getMessage());
}
}
第三步:Excel 导出(写入)
导出非常简单,只需要定义好 DTO ,然后调用 write 方法即可。
1. 定义导出的 DTO (VO)
可以和导入共用一个 DTO,也可以单独建一个 ExportTodoVO。
java
@Data
public class ExportTodoVO {
@ExcelProperty(value = "ID", index = 0)
private Long id;
@ExcelProperty(value = "任务标题", index = 1)
private String title;
@ExcelProperty(value = "状态", index = 2)
private String statusStr; // 导出时通常希望显示 "已完成/未完成",而不是 true/false
@ExcelProperty(value = "创建时间", index = 3)
private LocalDateTime createTime;
}
2. Service 层准备数据
java
public List<ExportTodoVO> getExportData() {
List<Todo> todos = todoMapper.selectList(null); // 查询所有
return todos.stream().map(todo -> {
ExportTodoVO vo = new ExportTodoVO();
vo.setId(todo.getId());
vo.setTitle(todo.getTitle());
vo.setStatusStr(todo.getCompleted() ? "已完成" : "进行中");
vo.setCreateTime(todo.getCreateTime());
return vo;
}).collect(Collectors.toList());
}
3. Controller 层直接写出流
注意:导出接口不需要返回 Result 对象,而是直接操作 HttpServletResponse 输出文件流。
java
@GetMapping("/export")
public void exportExcel(HttpServletResponse response) throws IOException {
// 1. 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
// 文件名 URLEncode 防止中文乱码
String fileName = URLEncoder.encode("待办事项列表", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
// 2. 获取数据
List<ExportTodoVO> list = todoService.getExportData();
// 3. 写出 Excel
EasyExcel.write(response.getOutputStream(), ExportTodoVO.class)
.sheet("待办列表") // Sheet 名称
.doWrite(list);
}
💡 常见坑点与最佳实践
日期格式问题:
1.导入时:Excel 中的日期 可能是数字序列号 ,也可能是字符串 。建议在 DTO 中用 String 接收,然后在 Service 层用 DateTimeFormatter 手动解析,这样更灵活,不容易报错。
2.导出时:可以在 @ExcelProperty 中使用 converter 自定义转换器,或者直接在 VO 里准备好格式化好的字符串。
大数据量导出:
1.如果数据量超过 10 万条,不要一次性查出来再写。
2.使用 EasyExcel 的 分页查询 + 追加写入 模式:
java
ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream(), ExportTodoVO.class).build();
WriteSheet writeSheet = EasyExcel.writerSheet("Sheet1").build();
int pageNum = 1;
int pageSize = 1000;
while (true) {
Page<Todo> page = todoMapper.selectPage(new Page<>(pageNum, pageSize), null);
if (page.getRecords().isEmpty()) break;
// 转换数据
List<ExportTodoVO> voList = convert(page.getRecords());
// 追加写入
excelWriter.write(voList, writeSheet);
if (page.getRecords().size() < pageSize) break;
pageNum++;
}
excelWriter.finish();
模板下载:
通常导入功能会配合一个"下载模板"功能。你可以创建一个只有表头的空 Excel 文件,放在 resources/templates/todo_template.xlsx ,然后通过 ResourceUtils.getFile 读取并返回给前端。
事务控制:
导入操作涉及数据库写入,记得在 Service 方法上加 @Transactional,确保如果中间某一批数据出错,之前导入的数据能回滚(或者根据业务需求,记录错误行,继续导入其他行)。
总结
库选择 :无脑选 EasyExcel。
导入核心 :Listener 监听器 + 分批入库。
导出核心 :HttpServletResponse 输出流 + EasyExcel.write。
难点:日期格式转换、大数据量的内存优化(EasyExcel 已帮你解决大部分)。