Easy Excel
EasyExcel是一个基于Java的、快速、简洁、解决大文件内存溢出的Excel处理工具。
他能让你在不用考虑性能、内存的等因素的情况下,快速完成Excel的读、写等功能。Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。
easyexcel重写了poi对07版Excel的解析,一个3M的excel用POI sax解析依然需要100M左右内存,改用easyexcel可以降低到几M,并且再大的excel也不会出现内存溢出;03版依赖POI的sax模式,在上层做了模型转换的封装,让使用者更加简单方便
EasyExcel底层对POI进行了封装,重写了poi的方法。在解析Excel时没有将文件数据一次性全部加载到内存中,而是从磁盘上一行行读取数据,逐个解析,遇到再大的excel都不会出现内存溢出的问题,能够将一个原本3M的excel文件,POI来操作将会占用内存100M,使用EasyExcel降低到几KB,并且使用起来更加简单。
数据导入导出执行原理和思路:
用户端逻辑:
1. 数据导入
用户先下载模板,根据模板填入数据,然后点击上传;
2. 数据导出
用户在界面选择需要导出的数据(导出条件),点击导出。
后台开发逻辑:
1. 模板下载
利用easyExcel生成文件,然后将文件放进响应流中,同时设置响应头为文件下载,浏览器收到响应之后,回去解析流中的内容,然后进行下载。
2. 文件上传
用在填写好Excel内容之后,会以文件上传的形式,将文件上传到服务端,此时,我们只需要利用EasyExcel将文件流中的数据读出来即可。
3. 数据导出
后台在接收到用户的数据导出请求之后,会根据请求中的筛选条件,查询对应数据,再将对应的数据填充进对应的导出模板中,以流的形式响应给浏览器。其实和模板下载的差不错,只是模板下载没有数据,数据导出有数据而已。
导入easyexcel依赖
<dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>3.1.0</version> </dependency>
导出功能
实体类
*/ @TableName(value ="score") @Data @AllArgsConstructor @NoArgsConstructor public class Score implements Serializable { public Score(String name, String xuehao, String score) { this.name = name; this.xuehao = xuehao; this.score = score; } /** * */ @ExcelProperty("ID") @TableId(type = IdType.AUTO) private Integer id; /** * */ @ExcelProperty("姓名") private String name; /** * */ @ExcelProperty("学号") private String xuehao; /** * */ @ExcelProperty("成绩") private String score; @TableField(exist = false) private static final long serialVersionUID = 1L; }
后端代码
@RequestMapping("/index/load") @ResponseBody public String load(HttpServletResponse response) throws Exception { List<Score> list = scoreMapper.selectList(null); if(list==null) return "error"; String filename=System.currentTimeMillis()+""; ServletOutputStream servletOutputStream = servletOutputStream(filename, response); EasyExcel.write(servletOutputStream,Score.class).sheet("成绩信息").doWrite(list); servletOutputStream.flush(); return "success"; } public static ServletOutputStream servletOutputStream(String filename,HttpServletResponse response) throws Exception { try { filename = URLEncoder.encode(filename, "UTF-8"); response.setContentType("application/vnd.ms-excel"); response.setCharacterEncoding("utf8"); response.setHeader("Content-Disposition", "attachment; filename=" + filename + ".xlsx"); response.setHeader("Pragma", "public"); response.setHeader("Cache-Control", "no-store"); response.addHeader("Cache-Control", "max-age=0"); return response.getOutputStream(); } catch (IOException e) { throw new Exception("导出excel表格失败!", e); } }
前端vue代码
<el-button type="primary" style="margin: 10px " @click="daochu"> <template #icon> <el-icon><Download /></el-icon> </template> 导出</el-button> const daochu=()=>{ window.open('http://localhost:8080/index/load') }
导入功能
监听器
package com.example.demo.listener; import com.alibaba.excel.context.AnalysisContext; import com.alibaba.excel.read.listener.ReadListener; import com.alibaba.excel.util.ListUtils; import com.alibaba.fastjson.JSON; import com.example.demo.mybatisx.entity.Score; import com.example.demo.mybatisx.mapper.ScoreMapper; import com.example.demo.mybatisx.service.ScoreService; import com.example.demo.utils.DemoDAO; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import java.util.List; // 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去 @Slf4j public class DemoDataListener implements ReadListener<Score> { /** * 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收 */ private static final int BATCH_COUNT = 100; /** * 缓存的数据 */ private List<Score> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT); /** * 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。 */ private DemoDAO demoDAO; private ScoreMapper scoreMapper; private ScoreService scoreService; public DemoDataListener(ScoreService scoreService) { // 这里是demo,所以随便new一个。实际使用如果到了spring,请使用下面的有参构造函数 demoDAO = new DemoDAO(); this.scoreService=scoreService; } /** * 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来 * * @param demoDAO */ public DemoDataListener(DemoDAO demoDAO) { this.demoDAO = demoDAO; } /** * 这个每一条数据解析都会来调用 * * @param score one row value. Is is same as {@link AnalysisContext#readRowHolder()} * @param context */ @Override public void invoke(Score score, AnalysisContext context) { score.setId(null); System.out.println(score); cachedDataList.add(score); // 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM if (cachedDataList.size() >= BATCH_COUNT) { saveData(); // 存储完成清理 list cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT); } } /** * 所有数据解析完成了 都会来调用 * * @param context */ @Override public void doAfterAllAnalysed(AnalysisContext context) { // 这里也要保存数据,确保最后遗留的数据也存储到数据库 saveData(); log.info("所有数据解析完成!"); } /** * 加上存储数据库 */ private void saveData() { log.info("{}条数据,开始存储数据库!", cachedDataList.size()); scoreService.saveBatch(cachedDataList); log.info("存储数据库成功!"); } }
后端代码 MultipartFile是spring里定义的接口,它封装了用户在上传文件时所包含的所有信息。
@RequestMapping("/index/import") @ResponseBody public String imp( @RequestParam("file") MultipartFile multipartFile) throws IOException { InputStream inputStream = multipartFile.getInputStream(); EasyExcel.read(inputStream,Score.class,new DemoDataListener(scoreService)).sheet().doRead(); return "success"; }
Vue 前端代码
<el-upload action="http://localhost:8080/index/import" style="display: inline-block" :show-file-list="false" :on-success="importsuccess" name="file" > <el-button type="primary" style="margin: 10px "> <template #icon> <el-icon><Upload /></el-icon> </template> 导入</el-button> </el-upload> const importsuccess=(response,file)=>{ console.log(response,file) alert("导入成功") load() }