在企业级 Java Web 项目中,Excel 导入导出是高频核心需求 ------ 比如批量导入用户数据、导出业务报表、批量更新商品信息等。传统 POI 操作 Excel 易引发内存溢出(OOM)、API 繁琐且性能低下,而阿里开源的 EasyExcel 框架完美解决这些问题。本文基于 EasyExcel 3.3.2 版本,从基础配置到高级场景,详解 Excel 导入导出的全流程实现,适配单表导出、条件导出、批量导入、导入校验等所有常见场景。
一、技术选型:为什么选择 EasyExcel?
对比传统 POI/JXL,EasyExcel 的核心优势:
| 特性 | EasyExcel | 传统 POI |
|---|---|---|
| 内存占用 | 逐行读写,低内存(MB 级) | 加载整表到内存,易 OOM |
| API 易用性 | 注解式映射,代码量少 | 手动解析单元格,代码繁琐 |
| 性能 | 处理 10 万行数据仅需数秒 | 处理 10 万行数据耗时久 |
| 功能支持 | 样式定制、导入校验、合并单元格 | 需手动实现复杂功能 |
| 兼容性 | 支持 Excel 2003-2019 | 需区分 HSSF/XSSF |
二、环境准备与基础配置
1. 引入 Maven 依赖
在项目pom.xml中添加 EasyExcel 核心依赖及辅助依赖:
XML
<!-- EasyExcel核心依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.2</version>
</dependency>
<!-- POI核心依赖(EasyExcel底层依赖) -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
<!-- 日期格式化工具 -->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.12.5</version>
</dependency>
<!-- JSON依赖(导入校验返回错误信息) -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.32</version>
</dependency>
2. 通用工具类封装
编写ExcelUtil工具类,封装导出响应头设置、文件名编码等通用逻辑,减少重复代码:
java
import javax.servlet.http.HttpServletResponse;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
/**
* Excel通用工具类
*/
public class ExcelUtil {
/**
* 设置Excel导出响应头(解决文件名乱码、指定文件类型)
*/
public static void setExcelResponseHeader(HttpServletResponse response, String fileName) {
try {
// 文件名编码,兼容浏览器
String encodeFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name())
.replaceAll("\\+", "%20");
// 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
response.setHeader("Content-Disposition",
"attachment;filename*=UTF-8''" + encodeFileName + ".xlsx");
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache");
} catch (Exception e) {
throw new RuntimeException("设置Excel响应头失败:" + e.getMessage());
}
}
}
三、Excel 导出实现(核心场景)
1. 基础场景:单列表数据导出
适用于简单数据导出(如用户列表、商品列表),核心是通过注解映射 Java 对象与 Excel 列。
(1)定义导出数据实体类
使用@ExcelProperty注解映射 Excel 表头与字段,@ColumnWidth设置列宽:
java
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import lombok.Data;
import java.util.Date;
/**
* 用户Excel导出实体
*/
@Data
public class UserExcelVO {
@ExcelProperty("用户ID")
@ColumnWidth(10)
private Long id;
@ExcelProperty("用户名")
@ColumnWidth(20)
private String username;
@ExcelProperty("手机号")
@ColumnWidth(15)
private String phone;
@ExcelProperty("创建时间")
@ColumnWidth(25)
private Date createTime;
@ExcelProperty(value = "状态", converter = StatusConverter.class)
@ColumnWidth(10)
private Integer status; // 0-禁用 1-启用
}
(2)自定义转换器(可选)
将枚举 / 数字类型转换为中文展示(如状态 0→禁用,1→启用):
java
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
/**
* 状态转换器
*/
public class StatusConverter implements Converter<Integer> {
@Override
public Class<Integer> supportJavaTypeKey() {
return Integer.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
@Override
public WriteCellData<?> convert(Integer value, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
String statusDesc = value == 1 ? "启用" : "禁用";
return new WriteCellData<>(statusDesc);
}
}
(3)编写导出接口
java
import com.alibaba.excel.EasyExcel;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
/**
* Excel导出控制器
*/
@RestController
@RequestMapping("/excel/export")
public class ExcelExportController {
// 模拟业务服务
private final UserService userService;
public ExcelExportController(UserService userService) {
this.userService = userService;
}
/**
* 导出所有用户列表
*/
@GetMapping("/user/list")
public void exportUserList(HttpServletResponse response) {
// 1. 设置响应头
ExcelUtil.setExcelResponseHeader(response, "用户列表_" + System.currentTimeMillis());
// 2. 查询业务数据
List<UserExcelVO> userList = userService.listAllUserForExcel();
// 3. 写入Excel并响应到前端
EasyExcel.write(response.getOutputStream(), UserExcelVO.class)
.sheet("用户列表") // Sheet名称
.doWrite(userList);
}
}
2. 进阶场景:条件导出 + 自定义样式
适用于按条件导出数据(如导出指定时间段的订单),并自定义表头样式、字体等。
(1)自定义样式配置
java
import com.alibaba.excel.write.handler.SheetWriteHandler;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.IndexedColors;
/**
* Excel样式工具类
*/
public class ExcelStyleUtil {
/**
* 获取自定义表头样式
*/
public static HorizontalCellStyleStrategy getCustomStyle() {
// 表头样式
WriteCellStyle headStyle = new WriteCellStyle();
// 表头背景色
headStyle.setFillForegroundColor(IndexedColors.LIGHT_BLUE.getIndex());
// 表头字体
WriteFont headFont = new WriteFont();
headFont.setFontName("微软雅黑");
headFont.setFontHeightInPoints((short) 12);
headFont.setBold(true);
headStyle.setWriteFont(headFont);
headStyle.setHorizontalAlignment(HorizontalAlignment.CENTER); // 居中
// 内容样式
WriteCellStyle contentStyle = new WriteCellStyle();
WriteFont contentFont = new WriteFont();
contentFont.setFontName("微软雅黑");
contentFont.setFontHeightInPoints((short) 10);
contentStyle.setWriteFont(contentFont);
contentStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
// 样式策略
return new HorizontalCellStyleStrategy(headStyle, contentStyle);
}
}
(2)条件导出接口
java
/**
* 按条件导出用户(如导出指定状态的用户)
*/
@GetMapping("/user/condition")
public void exportUserByCondition(HttpServletResponse response,
@RequestParam Integer status) {
// 1. 设置响应头
ExcelUtil.setExcelResponseHeader(response, "启用用户列表_" + System.currentTimeMillis());
// 2. 按条件查询数据
List<UserExcelVO> userList = userService.listUserByStatusForExcel(status);
// 3. 自定义样式+写入Excel
EasyExcel.write(response.getOutputStream(), UserExcelVO.class)
.sheet("指定状态用户列表")
.registerWriteHandler(ExcelStyleUtil.getCustomStyle()) // 注册样式
.doWrite(userList);
}
3. 高级场景:多 Sheet 导出
适用于一次导出多个维度的数据(如同时导出用户、订单、商品数据):
java
/**
* 多Sheet导出
*/
@GetMapping("/multi/sheet")
public void exportMultiSheet(HttpServletResponse response) {
ExcelUtil.setExcelResponseHeader(response, "多维度数据_" + System.currentTimeMillis());
// 构建Excel写入对象
ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).build();
// Sheet1:用户数据
WriteSheet userSheet = EasyExcel.writerSheet(0, "用户数据")
.head(UserExcelVO.class)
.registerWriteHandler(ExcelStyleUtil.getCustomStyle())
.build();
excelWriter.write(userService.listAllUserForExcel(), userSheet);
// Sheet2:订单数据(假设存在OrderExcelVO)
WriteSheet orderSheet = EasyExcel.writerSheet(1, "订单数据")
.head(OrderExcelVO.class)
.registerWriteHandler(ExcelStyleUtil.getCustomStyle())
.build();
excelWriter.write(orderService.listAllOrderForExcel(), orderSheet);
// 关闭writer
excelWriter.finish();
}
四、Excel 导入实现(核心场景)
1. 基础场景:批量导入数据
适用于批量新增 / 更新数据(如批量导入用户、商品),核心是通过监听器逐行读取数据并处理。
(1)定义导入数据实体类
java
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
/**
* 用户Excel导入实体
*/
@Data
public class UserExcelImportVO {
@ExcelProperty(value = "用户名", index = 0)
private String username;
@ExcelProperty(value = "手机号", index = 1)
private String phone;
@ExcelProperty(value = "密码", index = 2)
private String password;
@ExcelProperty(value = "状态", index = 3)
private String status; // 启用/禁用
}
(2)编写导入监听器
继承AnalysisEventListener,逐行处理导入数据,支持数据校验、批量入库:
java
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
/**
* 用户Excel导入监听器
*/
@Slf4j
public class UserExcelImportListener extends AnalysisEventListener<UserExcelImportVO> {
// 批量入库阈值
private static final int BATCH_SIZE = 100;
// 临时存储导入数据
private List<UserExcelImportVO> dataList = new ArrayList<>(BATCH_SIZE);
// 业务服务
private final UserService userService;
// 导入错误信息
private final List<String> errorMsgList = new ArrayList<>();
// 构造方法注入业务服务
public UserExcelImportListener(UserService userService) {
this.userService = userService;
}
/**
* 逐行处理数据
*/
@Override
public void invoke(UserExcelImportVO data, AnalysisContext context) {
log.info("读取到Excel数据:{}", JSON.toJSONString(data));
// 1. 数据校验
String validateMsg = validateData(data, context.readRowHolder().getRowIndex() + 1);
if (validateMsg != null) {
errorMsgList.add(validateMsg);
return;
}
// 2. 加入临时列表
dataList.add(data);
// 3. 批量入库
if (dataList.size() >= BATCH_SIZE) {
saveData();
// 清空临时列表
dataList.clear();
}
}
/**
* 数据校验
* @param rowNum 行号(Excel行号,从1开始)
*/
private String validateData(UserExcelImportVO data, int rowNum) {
// 用户名非空校验
if (data.getUsername() == null || data.getUsername().trim().isEmpty()) {
return "第" + rowNum + "行:用户名不能为空";
}
// 手机号格式校验
if (data.getPhone() == null || !data.getPhone().matches("^1[3-9]\\d{9}$")) {
return "第" + rowNum + "行:手机号格式错误";
}
// 状态校验
if (!"启用".equals(data.getStatus()) && !"禁用".equals(data.getStatus())) {
return "第" + rowNum + "行:状态只能是"启用"或"禁用"";
}
// 手机号重复校验
if (userService.existsByPhone(data.getPhone())) {
return "第" + rowNum + "行:手机号" + data.getPhone() + "已存在";
}
return null;
}
/**
* 批量保存数据
*/
private void saveData() {
log.info("批量入库{}条数据", dataList.size());
userService.batchSave(dataList);
}
/**
* 所有数据读取完成后执行
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 保存剩余数据
if (!dataList.isEmpty()) {
saveData();
}
log.info("Excel导入完成,共处理{}条数据,错误{}条",
context.readRowHolder().getRowIndex(), errorMsgList.size());
}
// 获取错误信息
public List<String> getErrorMsgList() {
return errorMsgList;
}
}
(3)编写导入接口
java
import com.alibaba.excel.EasyExcel;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* Excel导入控制器
*/
@RestController
@RequestMapping("/excel/import")
public class ExcelImportController {
private final UserService userService;
public ExcelImportController(UserService userService) {
this.userService = userService;
}
/**
* 批量导入用户
*/
@PostMapping("/user/batch")
public Map<String, Object> importUser(@RequestParam("file") MultipartFile file) {
Map<String, Object> result = new HashMap<>();
try {
// 1. 校验文件
if (file.isEmpty()) {
result.put("code", 500);
result.put("msg", "上传文件不能为空");
return result;
}
// 2. 创建导入监听器
UserExcelImportListener listener = new UserExcelImportListener(userService);
// 3. 读取Excel文件
EasyExcel.read(file.getInputStream(), UserExcelImportVO.class, listener)
.sheet() // 读取第一个Sheet
.doRead();
// 4. 处理导入结果
if (!listener.getErrorMsgList().isEmpty()) {
result.put("code", 500);
result.put("msg", "部分数据导入失败");
result.put("errorList", listener.getErrorMsgList());
} else {
result.put("code", 200);
result.put("msg", "数据导入成功");
}
} catch (IOException e) {
log.error("Excel导入失败", e);
result.put("code", 500);
result.put("msg", "导入失败:" + e.getMessage());
}
return result;
}
}
2. 进阶场景:导入数据回写错误信息
适用于导入失败时,生成包含错误提示的 Excel 文件返回给用户,便于修正后重新导入:
java
/**
* 导入失败回写错误信息
*/
@PostMapping("/user/batch/withError")
public void importUserWithError(HttpServletResponse response,
@RequestParam("file") MultipartFile file) {
try {
UserExcelImportListener listener = new UserExcelImportListener(userService);
// 读取Excel
EasyExcel.read(file.getInputStream(), UserExcelImportVO.class, listener)
.sheet().doRead();
// 有错误则生成错误Excel
if (!listener.getErrorMsgList().isEmpty()) {
// 设置响应头
ExcelUtil.setExcelResponseHeader(response, "导入错误数据_" + System.currentTimeMillis());
// 构建错误数据实体(假设存在UserErrorExcelVO)
List<UserErrorExcelVO> errorList = buildErrorData(listener.getErrorMsgList());
// 写入错误Excel
EasyExcel.write(response.getOutputStream(), UserErrorExcelVO.class)
.sheet("导入错误信息")
.doWrite(errorList);
} else {
response.getWriter().write("{\"code\":200,\"msg\":\"导入成功\"}");
}
} catch (Exception e) {
throw new RuntimeException("导入失败:" + e.getMessage());
}
}
// 构建错误数据(示例)
private List<UserErrorExcelVO> buildErrorData(List<String> errorMsgList) {
List<UserErrorExcelVO> list = new ArrayList<>();
for (String msg : errorMsgList) {
UserErrorExcelVO vo = new UserErrorExcelVO();
vo.setErrorMsg(msg);
list.add(vo);
}
return list;
}
五、核心注意事项与性能优化
1. 关键注意事项
- 内存控制:导入超大文件(10 万 + 行)时,务必设置批量入库阈值(如 100 条 / 批),避免一次性加载过多数据;
- 数据校验:导入前必须校验数据格式、唯一性、非空等,避免脏数据入库;
- 异常处理:导出时捕获 IO 异常,导入时捕获解析异常,保证接口稳定性;
- 文件名编码:导出文件名需 URLEncoder 编码,兼容不同浏览器(如 Chrome、Firefox);
- 资源释放 :多 Sheet 导出时,必须调用
excelWriter.finish()关闭流,避免资源泄漏。
2. 性能优化
- 批量读写:导入时设置合理的批量入库阈值(50-200 条),减少数据库交互次数;
- 禁用自动头 :如果 Excel 模板固定,可禁用自动生成表头(
head(false)),提升导出速度; - 异步导出:超大文件导出(100 万 + 行)时,采用异步方式(如 MQ + 定时任务),避免接口超时;
- 数据库优化 :批量入库时使用 MyBatis 批量插入(
foreach),开启数据库批处理模式。
六、总结
EasyExcel 凭借低内存、高性能、易扩展的特性,成为 Java 项目处理 Excel 的首选框架。本文覆盖了 Excel 导入导出的核心场景:
- 基础导出:注解式映射实体与 Excel 列,实现简单列表导出;
- 高级导出:自定义样式、多 Sheet 导出、条件导出,适配复杂业务场景;
- 基础导入:监听器逐行处理数据,支持数据校验、批量入库;
- 进阶导入:错误信息回写,提升用户体验。
该方案可直接复用至各类 Java Web 项目(如电商、OA、后台管理系统),解决 Excel 处理的核心痛点,兼顾性能与易用性。