EasyExcel是一个基于Java的简单、省内存的读写Excel的开源项目。在尽可能节约内存的情况下支持读写百M的Excel。
Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。easyexcel重写了poi对07版Excel的解析,能够原本一个3M的excel用POI sax依然需要100M左右内存降低到几M,并且再大的excel不会出现内存溢出,03版依赖POI的sax模式。在上层做了模型转换的封装,让使用者更加简单方便
--(摘自官方GitHub)
Easyexcel官网:https://alibaba-easyexcel.github.io/index.html
Easyexcel Github地址:https://github.com/alibaba/easyexcel
MyBatis-Plus官网:https://mp.baomidou.com/guide/
本文是基于官方示例编写的,使用Springboot(
2.2.2.RELEASE
)、Mybatis-Plus(3.1.2
)、EasyExcel(2.1.6
)和Thymeleaf前端框架,实现了Excel导入导出功能,包括日期、数字的格式化以及自定义格式转换
本文仅展示了主流程相关代码,若需要完整的Demo,可以自行下载(链接)
@Data
是Lombok的Jar包内的,Jar包安装成功仍然报错的话是因为IDE需要安装Lombok相关插件
@ExcelProperty(index=0)
注解用来指定哪些字段和excel列对应,其中index
表示在excel中第几列
用于Excel和Java之间数据暂存交互:
package com.lee.demo.easyexcel.entity;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import com.alibaba.excel.annotation.format.NumberFormat;
import com.lee.demo.easyexcel.utils.SexConverter;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDate;
import java.util.Date;
/**
* @ClassName: com.lee.demo.easyexcel.entity.User
* @Author: Jimmy
* @Date: 2020/1/11 23:32
* @Description: 用户实体类
* 1.@Data是Lombok的Jar包内的,报错的话是因为IDE需要安装Lombok相关插件
* 2.@ExcelProperty(index=0)注解用来指定哪些字段和excel列对应,其中index表示在excel中第几列
*/
@Data
public class User implements Serializable {
private String id;
@ExcelProperty(index=1)
private String username;
@ExcelProperty(index=2)
private String nickname;
@ExcelProperty(index=3)
private String password;
@ExcelProperty(index=4)
private String identitynum;
@ExcelProperty(index=5, converter = SexConverter.class)
private String sex;
@ExcelProperty(index=6)
private String age;
@ExcelProperty(index=7)
@DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
private Date birthday;
@ExcelProperty(index=8)
@NumberFormat("#.##厘米")
private Double height;
@ExcelProperty(index=9)
@NumberFormat("#.##斤")
private Double weight;
@ExcelProperty(index=10)
private String telephone;
@ExcelProperty(index=11)
private String email;
@ExcelProperty(index=12)
private String address;
}
用于提供web端接口,供前端调用:
package com.lee.demo.easyexcel.controller;
import com.lee.demo.easyexcel.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @ClassName: UserController
* @Author: Jimmy
* @Date: 2020/1/11 23:44
* @Description: TODO
*/
@RestController
@RequestMapping("/user")
public class UserController {
private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class);
@Autowired
private UserService userService;
/**
* 文件上传解析并导入数据库
* @param excel
* @throws IOException
*/
@PostMapping("import")
public void importExcel(MultipartFile excel) throws IOException {
try {
userService.importExcel(excel);
LOGGER.info("excel导入数据库成功!");
} catch (Exception e) {
e.printStackTrace();
LOGGER.error("excel导入数据库失败!");
}
}
/**
* 查询数据库数据并导出Excel
* @param response
* @throws IOException
*/
@GetMapping("export")
public void exportExcel(HttpServletResponse response) throws IOException {
try {
userService.exportExcel(response);
LOGGER.info("导出Excel成功!");
} catch (Exception e) {
e.printStackTrace();
LOGGER.error("导出Excel失败!");
}
}
}
用于调用Easyexcel,实现代码主要的逻辑业务:
package com.lee.demo.easyexcel.service.impl;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelReader;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.read.metadata.ReadSheet;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.lee.demo.easyexcel.dao.UserDao;
import com.lee.demo.easyexcel.entity.User;
import com.lee.demo.easyexcel.service.UserService;
import com.lee.demo.easyexcel.utils.ExcelListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @ClassName: UserServiceImpl
* @Author: Jimmy
* @Date: 2020/1/12 13:59
* @Description: TODO
*/
@Transactional
@Service
public class UserServiceImpl extends ServiceImpl<UserDao, User> implements UserService {
@Autowired
private UserDao userDao;
@Override
public void importExcel(MultipartFile excel) throws IOException {
long startTime=System.currentTimeMillis(); //获取开始时间
/**
* 写法一:
* 这里需要指定用哪个class去读,读取第一个sheet后文件流会自动关闭
*/
EasyExcel.read(excel.getInputStream(), User.class, new ExcelListener()).sheet().doRead();
/**
* 写法二:
* 该写法要记得手动关闭reader,读的时候会创建临时文件,到时磁盘会崩的
*/
/*
ExcelReader excelReader = EasyExcel.read(excel.getInputStream(), User.class, new ExcelListener()).build();
ReadSheet readSheet = EasyExcel.readSheet(0).build(); //readSheet(sheetNo)表示读取第几个sheet,0表示第一个sheet
excelReader.read(readSheet);
excelReader.finish();
*/
long endTime=System.currentTimeMillis(); //获取结束时间
System.out.println("程序运行时间: "+(endTime-startTime)/1000+"秒");
}
@Override
public void exportExcel(HttpServletResponse response) throws IOException {
// 这里注意 有同学反映使用swagger 会导致各种问题,请直接用浏览器或者用postman
try {
long startTime=System.currentTimeMillis(); //获取开始时间
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
String fileName = URLEncoder.encode("测试", "UTF-8");
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
/**
* 写法一:
* 这里需要指定用哪个class去写,写入第一个sheet后文件流会自动关闭
*/
//EasyExcel.write(response.getOutputStream(), User.class).sheet("模板").doWrite(data());
/**
* 写法二:
* 该写法要记得手动关闭writer,不然会报异常
*/
ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream(), User.class).build();
WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
excelWriter.write(data(), writeSheet);
// 千万别忘记finish 会帮忙关闭流
excelWriter.finish();
long endTime=System.currentTimeMillis(); //获取结束时间
System.out.println("程序运行时间: "+(endTime-startTime)/1000+"秒");
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
/**
* 使用MybatisPlus的条件查询方法查询所有数据
* @return
*/
private List<User> data() {
QueryWrapper<User> queryWrapper = Wrappers.query();
List<User> userList = userDao.selectList(queryWrapper);
return userList;
}
}
需要继承AnalysisEventListener
类,用于监听Excel的解析情况,它提供了一些父类方法,能够很方便的在解析后加入业务代码,比如新增至数据库中:
package com.lee.demo.easyexcel.utils;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.fastjson.JSON;
import com.lee.demo.easyexcel.entity.User;
import com.lee.demo.easyexcel.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* @ClassName: ExcelListener
* @Author: Jimmy
* @Date: 2020/1/12 14:08
* @Description: 模板读取类
*/
@Component
public class ExcelListener extends AnalysisEventListener {
private static final Logger LOGGER = LoggerFactory.getLogger(ExcelListener.class);
@Autowired
private static UserService userService;
public ExcelListener() {
}
/**
* 此处必须使用构造器来注入spring管理的类,不然使用@Autowired会注入失败,会报userService类NullPointerException
* @param userService
*/
@Autowired
public ExcelListener(UserService userService) {
this.userService = userService;
}
/**
* 每隔5条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收
*/
private static final int BATCH_COUNT = 3000;
List<User> list = new ArrayList<User>();
/**
* 这个每一条数据解析都会来调用
*/
@Override
public void invoke(Object object, AnalysisContext context) {
LOGGER.info("解析到一条数据:{}", JSON.toJSONString(object));
list.add((User)object);
// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM(Out Of Memory)
if (list.size() >= BATCH_COUNT) {
saveData();
// 存储完成清理 list
list.clear();
}
}
/**
* 所有数据解析完成了 都会来调用
*
* @param context
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
//这里也要保存数据,确保批量新增最后一批遗留的数据也存储到数据库
saveData();
LOGGER.info("所有数据解析完成并存储成功!");
}
/**
* 使用MybatisPlus的批量新增方法插入数据
*/
public void saveData() {
userService.saveBatch(this.list,BATCH_COUNT);
}
public List<User> getList() {
return list;
}
public void setList(List<User> list) {
this.list = list;
}
}
此处必须使用构造器来注入Spring管理的Service类,且ExcelListener类也必须加上@Component注解来让Spring管理,不然使用@Autowired会注入失败,会报userService类NullPointerException。
Easyexcel暂不支持JDK8的LocalDate
类型,所以在格式化日期时间
字段的时候还是得用Date类型:
@DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
private Date birthday;
字段类型不能为String,不然会转换失败:
@NumberFormat("#.##厘米")
private Double height;
@ExcelProperty(index=9)
@NumberFormat("#.##斤")
private Double weight;
自定义"性别"
转换器:
package com.lee.demo.easyexcel.utils;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.CellData;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
/**
* @ClassName: GenderConverter
* @Author: Jimmy
* @Date: 2020/1/14 10:59
* @Description: 性别 自定义转换器
*/
public class SexConverter implements Converter<String> {
public static final String MALE = "男";
public static final String FEMALE = "女";
@Override
public Class supportJavaTypeKey() {
return String.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
/**
* Excel转Java,用于Excel导入操作
* @param cellData
* @param excelContentProperty
* @param globalConfiguration
* @return
* @throws Exception
*/
@Override
public String convertToJavaData(CellData cellData, ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception {
String stringValue = cellData.getStringValue();
if (MALE.equals(stringValue)){
return "0";
}else {
return "1";
}
}
/**
* Java转Excel,用于Excel导出操作
* @param value
* @param excelContentProperty
* @param globalConfiguration
* @return
* @throws Exception
*/
@Override
public CellData convertToExcelData(String value, ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception {
CellData cellData = new CellData(value);
if (value.equals("0")){
cellData.setStringValue(MALE);
}else if (value.equals("1")){
cellData.setStringValue(FEMALE);
}
return cellData;
}
}
执行Junit测试实例中的initializeDatas()
方法初始化100万条数据:
启动服务,访问http://localhost:8080/index
打开测试页面:
点击"Excel导出"
,可发现当前数据量"导出时间"
大概在90秒左右:
通过sql脚本删除表数据:
DELETE FROM `user`
打开测试页面,"选择文件"
后点击"Excel导入"
,可以发现当前数据量"导入时间"
大概在736秒左右:
导入、导出时间和硬件计算速度、网络等因素有关,测试仅作参考。总而言之,相比EasyExcel的底层POI相比,EasyExcel的解析性能是大大超过原生POI的,而且内存占用率极低。
org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (48207514) exceeds the configured maximum (10485760)
解决方案:参考我另一博文(传送门)
解决方案:参考博文(传送门)