在日常开发中,Excel 文件的导入导出是非常常见的需求。无论是数据批量导入、报表生成还是数据备份,我们都离不开对 Excel 的操作。但传统的 POI 框架在处理大数据量 Excel 时,常常会遇到内存溢出的问题,让开发者头疼不已。
今天给大家介绍一款阿里开源的 Excel 处理工具 ——EasyExcel,它以低内存占用为核心优势,完美解决了大数据量 Excel 处理的痛点。
什么是 EasyExcel?
EasyExcel 是阿里巴巴开源的一个基于 Java 的 Excel 处理工具,它重写了 POI 对 Excel 的解析方式,通过事件驱动模式和增量解析的方式,在读取 Excel 时不会将整个文件加载到内存中,而是逐行解析,大大降低了内存占用。
项目地址:https://github.com/alibaba/easyexcel
EasyExcel 的核心优势
内存占用极低
- 传统 POI 解析 Excel 时,会将整个文档加载到内存,对于百万级数据的 Excel,很容易导致 OOM
- EasyExcel 采用逐行解析模式,内存占用可以控制在 KB 级别
API 简洁易用
- 封装了复杂的 Excel 解析逻辑,提供简单直观的 API
- 注解驱动,通过注解即可完成 Excel 与实体类的映射
功能完善
- 支持 Excel 的读写操作
- 支持 xls、xlsx 等多种格式
- 支持复杂表头、合并单元格等复杂场景
- 支持大数据量的导入导出
扩展性强
- 提供丰富的监听器接口,可以自定义处理逻辑
- 支持自定义转换器,处理特殊格式数据
快速入门:EasyExcel 基本使用
1. 引入依赖
首先在项目中引入 EasyExcel 的 Maven 依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.0</version>
</dependency>
2. 定义实体类
通过注解定义 Excel 与实体类的映射关系
@Data
public class UserData {
// index表示列的索引,value表示列名
@ExcelProperty(index = 0, value = "姓名")
private String name;
@ExcelProperty(index = 1, value = "年龄")
private Integer age;
@ExcelProperty(index = 2, value = "邮箱")
private String email;
// 日期格式化
@ExcelProperty(index = 3, value = "注册时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date registerTime;
}
3. 写入 Excel 文件
public class ExcelWriteDemo {
public static void main(String[] args) {
// 准备数据
List<UserData> dataList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
UserData data = new UserData();
data.setName("用户" + i);
data.setAge(20 + i);
data.setEmail("user" + i + "@example.com");
data.setRegisterTime(new Date());
dataList.add(data);
}
// 写入文件
String fileName = "D:/user_data.xlsx";
EasyExcel.write(fileName, UserData.class)
.sheet("用户列表") // 指定工作表名称
.doWrite(dataList); // 写入数据
}
}
4. 读取 Excel 文件
读取 Excel 需要定义一个监听器:
// 自定义监听器
public class UserDataListener extends AnalysisEventListener<UserData> {
// 每解析一行数据就会调用一次
@Override
public void invoke(UserData data, AnalysisContext context) {
System.out.println("解析到数据:" + data);
// 可以在这里处理数据,如存入数据库
}
// 所有数据解析完成后调用
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
System.out.println("所有数据解析完成");
}
}
// 读取Excel
public class ExcelReadDemo {
public static void main(String[] args) {
String fileName = "D:/user_data.xlsx";
EasyExcel.read(fileName, UserData.class, new UserDataListener())
.sheet() // 读取第一个工作表
.doRead(); // 开始读取
}
}
Web 场景下的 Excel 导出
在 Web 项目中,我们经常需要实现 Excel 导出功能,让用户可以直接下载文件:
@RequestMapping("/export")
public void exportExcel(HttpServletResponse response) throws IOException {
// 准备数据
List<UserData> dataList = getUserDataList();
// 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("UTF-8");
String fileName = URLEncoder.encode("用户数据", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
// 写入响应流
EasyExcel.write(response.getOutputStream(), UserData.class)
.sheet("用户列表")
.doWrite(dataList);
}
高级特性
- 大数据量处理
EasyExcel 专门为大数据量场景设计,即使处理百万级数据也不会出现内存问题:
// 读取大数据量Excel
EasyExcel.read(fileName, UserData.class, new UserDataListener())
.batchRead(1000) // 批量读取,每1000条处理一次
.sheet()
.doRead();
- 复杂表头处理
支持多级表头的导入导出:
// 定义复杂表头
List<List<String>> head = new ArrayList<>();
head.add(Arrays.asList("用户信息", "姓名"));
head.add(Arrays.asList("用户信息", "年龄"));
head.add(Arrays.asList("联系信息", "邮箱"));
// 写入复杂表头
EasyExcel.write(fileName)
.head(head)
.sheet("复杂表头示例")
.doWrite(dataList);
- 数据转换与格式化
通过自定义转换器处理特殊格式的数据:
// 自定义转换器
public class CustomConverter implements Converter<LocalDateTime> {
@Override
public Class<LocalDateTime> supportJavaTypeKey() {
return LocalDateTime.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
// 读取时转换
@Override
public LocalDateTime convertToJavaData(ReadConverterContext<?> context) {
return LocalDateTime.parse(context.getReadCellData().getStringValue(),
DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss"));
}
// 写入时转换
@Override
public WriteCellData<?> convertToExcelData(WriteConverterContext<LocalDateTime> context) {
return new WriteCellData<>(context.getValue().format(
DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss")));
}
}
一个完整的ExcelUtils类
public class ExcelUtils {
/**
* 读取前端上传的excel文件
*/
public static <T> void readAnalysis(MultipartFile file, Class<T> head, ExcelFinishCallBack<T> callBack) {
try {
EasyExcel.read(file.getInputStream(), head, new ExcelDataListener<>(callBack)).sheet().doRead();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 读取本地excel文件
*
* @param <T> 数据类型
* @param file excel文件
* @param head 列名
* @param callBack 回调 导入时传入定义好的回调接口,excel数据解析完毕之后监听器将数据传入回调函数
* 这样调用工具类时可以通过回调函数获取导入的数据,如果数据量过大可根据实际情况进行分配入库
*/
public static <T> void readAnalysis(File file, Class<T> head, ExcelFinishCallBack<T> callBack) {
try {
EasyExcel.read(new FileInputStream(file), head, new ExcelDataListener<>(callBack)).sheet().doRead();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 读取excel文件 同步
*
* @param <T> 数据类型
* @param file 文件
* @param clazz 模板类
* @return java.util.List
*/
public static <T> List<T> readSync(File file, Class<T> clazz) {
return readSync(file, clazz, 1, 0, ExcelTypeEnum.XLSX);
}
/**
* 读取excel文件 同步
*
* @param <T> 数据类型
* @param file 文件
* @param clazz 模板类
* @param rowNum 数据开始行 1
* @param sheetNo 第几张表
* @param excelType 数据表格式类型
* @return java.util.List list
*/
public static <T> List<T> readSync(File file, Class<T> clazz, Integer rowNum, Integer sheetNo, ExcelTypeEnum excelType) {
return EasyExcel.read(file).headRowNumber(rowNum).excelType(excelType).head(clazz).sheet(sheetNo).doReadSync();
}
/**
* 导出数据到文件
*
* @param <T> 数据类型
* @param head 类名
* @param file 导入到本地文件
* @param data 数据
*/
public static <T> void excelExport(Class<T> head, File file, List<T> data) {
excelExport(head, file, "sheet1", data);
}
/**
* 导出数据到文件
*
* @param <T> 写入格式
* @param head 类名
* @param file 写入到文件
* @param sheetName sheet名称
* @param data 数据列表
*/
public static <T> void excelExport(Class<T> head, File file, String sheetName, List<T> data) {
try {
EasyExcel.write(file, head).sheet(sheetName).doWrite(data);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 导出数据到web
* 文件下载(失败了会返回一个有部分数据的Excel)
*
* @param head 类名
* @param excelName excel名字
* @param sheetName sheet名称
* @param data 数据
* 数据导出到web响应
*/
public static <T> void excelExport(Class<T> head, String excelName, String sheetName, List<T> data) {
try {
HttpServletResponse response = getExportResponse(excelName);
EasyExcel.write(response.getOutputStream(), head).sheet(StringUtils.isBlank(sheetName) ? "sheet1" : sheetName).doWrite(data);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 导出数据到web
* 文件下载(失败了会返回一个有部分数据的Excel)
*
* @param head 类名
* @param excelName excel名字
* @param sheetName sheet名称
* @param data 数据
*/
public static <T> void excelExport(List<List<String>> head, String excelName, String sheetName, List<T> data) {
try {
HttpServletResponse response = getExportResponse(excelName);
EasyExcel.write(response.getOutputStream()).head(head).sheet(StringUtils.isBlank(sheetName) ? "sheet1" : sheetName).doWrite(data);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/*
设置编码格式,允许前端访问文件名进行跨域审核
*/
private static HttpServletResponse getExportResponse(String excelName) {
//获得当前HTTP响应对象
HttpServletResponse response = HttpContextUtils.getHttpServletResponse();
//告诉浏览器返回的是excel文件
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
//允许前端读取文件名 filename:指定下载时显示的文件名
response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
//设置编码格式
response.setCharacterEncoding("UTF-8");
//将文件名进行编码
String fileName = URLUtil.encode(excelName, StandardCharsets.UTF_8);
//允许前段JavaScript访问Content-Disposition头 获取文件名
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
return response;
}
/**
* 解析字典数据到字段上
* 比如 T中有 genderLabel字段 为男 需要给 gender 字段自动设置为0
*
* @param dataList 需要被反向解析的数据
*/
@SneakyThrows
public static <T extends TransPojo> void parseDict(List<T> dataList) {
//没有数据就不需要初始化
if (CollectionUtil.isEmpty(dataList)) {
return;
}
Class<? extends TransPojo> clazz = dataList.get(0).getClass();
//拿到所有需要反向翻译的字段 注解带Trans注解的字段
List<Field> fields = ReflectUtils.getAnnotationField(clazz, Trans.class);
//过滤出类型为字典TransType.DICTIONARY的字段
fields = fields.stream().filter(field -> TransType.DICTIONARY
.equals(field.getAnnotation(Trans.class).type())).collect(Collectors.toList());
//从spring容器中获取字典转换服务
DictionaryTransService dictionaryTransService = SpringUtil.getBean(DictionaryTransService.class);
//反射设置值
for (T data : dataList) {
//获取关联字段的值
for (Field field : fields) {
//从字典中服务中获取映射值
Trans trans = field.getAnnotation(Trans.class);
// key不能为空并且ref不为空的才自动处理
if (StrUtil.isAllNotBlank(trans.key(), trans.ref())) {
// 根据字段名获取对应的Field对象 类似于user类中的gender字段
Field ref = ReflectUtils.getDeclaredField(clazz, trans.ref());
//打开访问私有属性的开关
ref.setAccessible(true);
// 获取字典映射值
String value = dictionaryTransService.getDictionaryTransMap().get(trans.key() + "_" + ref.get(data));
if (StringUtils.isBlank(value)) {
continue;
}
// 一般目标字段是int或者string字段 后面有添加单独抽离方法
if (Integer.class.equals(field.getType())) {
field.setAccessible(true);
field.set(data, ConverterUtils.toInteger(value));
} else {
field.setAccessible(true);
field.set(data, ConverterUtils.toString(value));
}
}
}
}
}
}
总结
EasyExcel 作为一款优秀的 Excel 处理工具,凭借其低内存占用、简单易用的特点,已经成为 Java 开发中处理 Excel 的首选框架。无论是简单的 Excel 导入导出,还是复杂的大数据量处理场景,EasyExcel 都能轻松应对。
如果你还在为 POI 的内存问题烦恼,不妨试试 EasyExcel,相信它会给你带来惊喜!
欢迎在评论区分享你使用 EasyExcel 的经验和技巧~