赞
踩
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)
解决方案:参考我另一博文(传送门)
解决方案:参考博文(传送门)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。