前言
-
概述
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模式。在上层做了模型转换的封装,让使用者更加简单方便
它的官方建议对于1000行以内的采用原来poi的写法一次读写,但于1000行以上的数据,又用了一行行进行解析的方案,这样避免了内存的溢出。
-
官方参考文档
https://alibaba-easyexcel.github.io
https://www.yuque.com/easyexcel/doc/easyexcel
实现流程
导入依赖
-
依赖版本要求
<!-- https://mvnrepository.com/artifact/com.alibaba/easyexcel --> <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>2.1.7</version> </dependency>
实现代码
-
读取对象实体
@Data public class Demo implements Serializable { @ExcelProperty("序号") private Integer demoId; // ExcelProperty 指定当前字段对应excel中的那一列。可以根据名字或者Index去匹配。当然也可以不写,默认第一个字段就是index=0,以此类推。千万注意,要么全部不写,要么全部用index,要么全部用名字去匹配。千万别三个混着用,除非你非常了解源代码中三个混着用怎么去排序的。 @ExcelProperty("标题") private String title; // DateTimeFormat 日期转换,用String去接收excel日期格式的数据会调用这个注解。里面的value参照java.text.SimpleDateFormat @ExcelProperty("日期") @DateTimeFormat("yyyy/MM/dd") private Date demoData; @ExcelProperty("数字") private Integer dataNum; private static final long serialVersionUID = 1L; public Integer getDemoId() { return demoId; } public void setDemoId(Integer demoId) { this.demoId = demoId; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public Date getDemoData() { return demoData; } public void setDemoData(Date demoData) { this.demoData = demoData; } public Integer getDataNum() { return dataNum; } public void setDataNum(Integer dataNum) { this.dataNum = dataNum; } @Override public boolean equals(Object that) { if (this == that) { return true; } if (that == null) { return false; } if (getClass() != that.getClass()) { return false; } Demo other = (Demo) that; return (this.getDemoId() == null ? other.getDemoId() == null : this.getDemoId().equals(other.getDemoId())) && (this.getTitle() == null ? other.getTitle() == null : this.getTitle().equals(other.getTitle())) && (this.getDemoData() == null ? other.getDemoData() == null : this.getDemoData().equals(other.getDemoData())) && (this.getDataNum() == null ? other.getDataNum() == null : this.getDataNum().equals(other.getDataNum())); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((getDemoId() == null) ? 0 : getDemoId().hashCode()); result = prime * result + ((getTitle() == null) ? 0 : getTitle().hashCode()); result = prime * result + ((getDemoData() == null) ? 0 : getDemoData().hashCode()); result = prime * result + ((getDataNum() == null) ? 0 : getDataNum().hashCode()); return result; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(getClass().getSimpleName()); sb.append(" ["); sb.append("Hash = ").append(hashCode()); sb.append(", demoId=").append(demoId); sb.append(", title=").append(title); sb.append(", demoData=").append(demoData); sb.append(", dataNum=").append(dataNum); sb.append(", serialVersionUID=").append(serialVersionUID); sb.append("]"); return sb.toString(); } }
-
文件入参实体类
package link.lycreate.springbooteasyexceldemo.domain; import com.fasterxml.jackson.annotation.JsonProperty; import java.io.Serializable; /** * @ClassName LocalFile * @Description 接收附件入参实体类 * @Author charlesYan * @Date 2020/9/27 18:43 * @Version 1.0 **/ public class LocalFile implements Serializable { @JsonProperty(value = "filePath") private String filePathStr; public String getFilePathStr() { return filePathStr; } public void setFilePathStr(String filePathStr) { this.filePathStr = filePathStr; } }
-
自定义监听器
package link.lycreate.springbooteasyexceldemo.listener; import com.alibaba.excel.context.AnalysisContext; import com.alibaba.excel.event.AnalysisEventListener; import com.alibaba.fastjson.JSON; import link.lycreate.springbooteasyexceldemo.dao.DemoDao; import link.lycreate.springbooteasyexceldemo.domain.Demo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; /** * @ClassName UploadDataListener * @Description 有个很重要的点 UploadDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去 * @Author charlesYan * @Date 2020/5/1 17:49 */ public class UploadDataListener extends AnalysisEventListener<Demo> { private Logger LOGGER= LoggerFactory.getLogger(this.getClass()); /** * 每隔5条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收 */ private static final int BATCH_COUNT = 5; List<Demo> list=new ArrayList<Demo>(); /** * 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。 */ private DemoDao demoDao; public UploadDataListener(DemoDao demoDao){ this.demoDao=demoDao; } /** * 这个每一条数据解析都会来调用 */ @Override public void invoke(Demo demo, AnalysisContext analysisContext) { LOGGER.info("解析到一条数据:{}", JSON.toJSONString(demo)); list.add(demo); // 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM if (list.size() >= BATCH_COUNT) { saveData(); // 存储完成清理 list list.clear(); } } /** * 所有数据解析完成了 都会来调用 */ @Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { // 这里也要保存数据,确保最后遗留的数据也存到数据库 saveData(); LOGGER.info("所有数据解析完成!"); } /** * 加上存储数据库 */ private void saveData() { LOGGER.info("{}条数据,开始存储数据库!", list.size()); for (Demo demo:list){ demoDao.insert(demo); } LOGGER.info("存储数据库成功!"); } }
-
控制层代码
package link.lycreate.springbooteasyexceldemo.controller; import com.alibaba.excel.EasyExcel; import link.lycreate.springbooteasyexceldemo.dao.DemoDao; import link.lycreate.springbooteasyexceldemo.domain.Demo; import link.lycreate.springbooteasyexceldemo.domain.LocalFile; import link.lycreate.springbooteasyexceldemo.listener.UploadDataListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; import java.io.IOException; /** * @ClassName ExcelDemoController * @Description TODO * @Author charlesYan * @Date 2020/5/1 17:40 */ @Controller public class ExcelDemoController { private final ThreadLocal<Logger> LOGGER = ThreadLocal.withInitial(() -> LoggerFactory.getLogger(ExcelDemoController.class)); @Resource private DemoDao demoDao; /** * @Author charlesYan * @Description //接收前台传过来的文件 * @Date 15:03 2020/10/13 * @Param [file, model] * @return java.lang.String **/ @RequestMapping(path="/upload",method = RequestMethod.POST) public String uploadExcel(@RequestParam("file") MultipartFile file, Model model) throws IOException { EasyExcel.read(file.getInputStream(), Demo.class,new UploadDataListener(demoDao)).sheet().doRead(); return "/uploadStatus"; } /** * @Author charlesYan * @Description //接收前台传过来的文件路径 * @Date 14:57 2020/10/13 * @Param [localFile] * @return java.lang.String **/ @RequestMapping(path = "/read",method = RequestMethod.POST) public String readLocalExcel(@RequestBody LocalFile localFile){ LOGGER.get().info("读取文件路径:{}",localFile.getFilePathStr()); EasyExcel.read(localFile.getFilePathStr(),Demo.class,new UploadDataListener(demoDao)).sheet().doRead(); LOGGER.get().info("读取文件成功!"); return "/uploadStatus"; } /** * @Author charlesYan * @Description //将数据导出到excel以文件路径形式返回 * @Date 16:19 2020/10/13 * @Param [targetFilePath] * @return java.lang.String **/ @RequestMapping(path = "/write",method = RequestMethod.POST) public String writeExcel(@RequestParam("targetFilePath")String targetFilePath){ LOGGER.get().info("写入文件路径:{}",targetFilePath); // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭 // 如果这里想使用03 则 传入excelType参数即可 Demo demo = demoDao.selectByPrimaryKey(63); List<Demo> list = new ArrayList<Demo>(); list.add(demo); EasyExcel.write(targetFilePath, Demo.class).sheet("模板").doWrite(list); return "/uploadStatus"; } /** * @Author charlesYan * @Description //将数据导出到excel直接从web端返回 * @Date 10:31 2020/10/14 * @Param [response] * @return java.lang.String **/ @GetMapping("/download") public void downloadExcel(HttpServletResponse response) throws IOException { LOGGER.get().info("web端下载文件"); // 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman response.setContentType("application/vnd.ms-excel"); response.setCharacterEncoding("utf-8"); // 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系 String fileName = URLEncoder.encode("202010141020测试", "UTF-8").replaceAll("\\+", "%20"); response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx"); // 读取数据列表 Demo demo = demoDao.selectByPrimaryKey(63); List<Demo> list = new ArrayList<Demo>(); list.add(demo); EasyExcel.write(response.getOutputStream(), Demo.class).sheet("模板").doWrite(list); } }
总结
Web端导出文件时getOutputStream()重复被调用
-
参考链接
https://www.cnblogs.com/zs-notes/p/9456234.html
-
报错信息
java.lang.IllegalStateException: getOutputStream() has already been called for this response
-
原因分析
在tomcat下jsp出现该错误一般都是在使用了输出流(如输出图片验证码,文件下载等)。
产生这样的异常原因:是web容器生成的servlet代码中有out.write(""),这个和JSP中调用的response.getOutputStream()产生冲突。即Servlet规范说明,不能既调用response.getOutputStream(),又调用response.getWriter(),无论先调用哪一个,在调用第二个时候应会抛出IllegalStateException,因为在jsp中,out变量实际上是通过response.getWriter得到的,你的程序中既用了response.getOutputStream,又用了out变量,故出现以上错误。
-
解决方案
-
Controller层downloadExcel方法返回值类型改为void,不再重定向到其他页面。
-
在调用方法上添加@ResponseBody注解,使响应结果变为json字符串返回
-
参考链接
-
SpringBoot2.x 整合 easyexcel 进行报表导入导出
https://blog.csdn.net/qidasheng2012/article/details/102707394
-
阿里easyExcel使用—下(easyExcel2.0.0 版本)
https://blog.csdn.net/weixin_42083036/article/details/102802644
-
springboot结合Easyexcel的使用(详细介绍Easyexcel)小白入门到精通
https://blog.csdn.net/weixin_37407422/article/details/105742211