外观
EasyExcel 导入导出
2026-04-01
版本: 4.0.3 官网: easyexcel.opensource.alibaba.com
EasyExcel 是阿里巴巴开源的 Excel 处理库,基于 Apache POI 二次封装,核心优势是内存友好:读取时逐行解析不把整个文件加载进内存,适合万级乃至十万级数据量的导入/导出场景。
引入依赖
// build.gradle
dependencies {
implementation 'com.alibaba:easyexcel'
}定义 Excel 行模型
用 @ExcelProperty 注解字段与 Excel 列的映射关系:
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import com.alibaba.excel.annotation.format.NumberFormat;
import lombok.Data;
@Data
public class UserExcelVO {
@ExcelProperty("用户名")
private String username;
@ExcelProperty("姓名")
private String realName;
@ExcelProperty("手机号")
private String phone;
@ExcelProperty("状态")
private String statusText; // 导出时转成文字,不直接导出状态码
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@ExcelIgnore // 不导出此字段
private String password;
}导出 Excel
Controller 层(HTTP 响应流导出)
@Operation(summary = "导出用户列表")
@GetMapping("/export")
public void exportUser(HttpServletResponse response,
@RequestParam(required = false) String keyword) throws IOException {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
// 文件名需 URL 编码,防止中文乱码
String fileName = URLEncoder.encode("用户列表_" + LocalDate.now(), StandardCharsets.UTF_8);
response.setHeader("Content-Disposition", "attachment;filename*=UTF-8''" + fileName + ".xlsx");
// 查询数据
List<UserExcelVO> data = sysUserService.listForExport(keyword);
// 写入响应流
EasyExcel.write(response.getOutputStream(), UserExcelVO.class)
.sheet("用户列表")
.doWrite(data);
}导出带样式
// 自定义列宽
EasyExcel.write(response.getOutputStream(), UserExcelVO.class)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 自动列宽
.sheet("用户列表")
.doWrite(data);导入 Excel
定义导入监听器
监听器负责逐行处理解析到的数据,是 EasyExcel 导入的核心扩展点:
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class UserImportListener extends AnalysisEventListener<UserExcelVO> {
/** 每批缓存的行数,达到此数量后批量入库 */
private static final int BATCH_SIZE = 200;
private final ISysUserService sysUserService;
private final List<UserExcelVO> cachedList = new ArrayList<>();
private final List<String> errorMessages = new ArrayList<>();
public UserImportListener(ISysUserService sysUserService) {
this.sysUserService = sysUserService;
}
/** 每解析一行数据回调一次 */
@Override
public void invoke(UserExcelVO data, AnalysisContext context) {
// 单行校验
if (StrUtil.isBlank(data.getUsername())) {
int rowIndex = context.readRowHolder().getRowIndex() + 1;
errorMessages.add("第 " + rowIndex + " 行:用户名不能为空");
return;
}
cachedList.add(data);
if (cachedList.size() >= BATCH_SIZE) {
saveData();
}
}
/** 所有行解析完毕后回调(处理剩余数据)*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
saveData();
log.info("[用户导入] 完成,共处理 {} 条", cachedList.size());
}
private void saveData() {
if (CollUtil.isEmpty(cachedList)) return;
sysUserService.batchImport(cachedList);
cachedList.clear();
}
public List<String> getErrorMessages() {
return errorMessages;
}
}Controller 层(接收上传文件)
@Operation(summary = "导入用户")
@PostMapping("/import")
public R<Map<String, Object>> importUser(@RequestParam("file") MultipartFile file) throws IOException {
UserImportListener listener = new UserImportListener(sysUserService);
EasyExcel.read(file.getInputStream(), UserExcelVO.class, listener)
.sheet()
.doRead();
Map<String, Object> result = new HashMap<>();
result.put("errorMessages", listener.getErrorMessages());
return R.ok(result);
}下载导入模板
@Operation(summary = "下载导入模板")
@GetMapping("/import-template")
public void downloadTemplate(HttpServletResponse response) throws IOException {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
String fileName = URLEncoder.encode("用户导入模板", StandardCharsets.UTF_8);
response.setHeader("Content-Disposition", "attachment;filename*=UTF-8''" + fileName + ".xlsx");
// 写空数据(只写表头)
EasyExcel.write(response.getOutputStream(), UserExcelVO.class)
.sheet("导入模板")
.doWrite(Collections.emptyList());
}常见配置
指定读取的 Sheet
// 读第 1 个 sheet(默认)
EasyExcel.read(inputStream, UserExcelVO.class, listener).sheet(0).doRead();
// 读指定名称的 sheet
EasyExcel.read(inputStream, UserExcelVO.class, listener).sheet("用户列表").doRead();跳过表头行
// headRowNumber 默认为 1(第 1 行是表头,从第 2 行开始读数据)
EasyExcel.read(inputStream, UserExcelVO.class, listener)
.headRowNumber(2) // 前 2 行是表头,从第 3 行开始读
.sheet()
.doRead();常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 导出文件名乱码 | 响应头未做 URL 编码 | 使用 URLEncoder.encode() 编码文件名 |
导入数字列解析为 null | Excel 单元格格式为数值,Java 字段为 String | 字段改为 Double 或用 @ExcelProperty 配置 converter |
| 大文件导入 OOM | 一次性加载太多行 | 监听器中设置 BATCH_SIZE,分批入库 |
| 日期字段解析错误 | 格式不匹配 | @DateTimeFormat("yyyy-MM-dd") 与 Excel 格式一致 |
| 导出后 Excel 提示文件损坏 | 响应流提前关闭或有额外内容写入 | 确保 Controller 方法返回 void,不要有额外 return 输出 |
