SpringBoot学习之整合EasyExcel进行数据导入导出

前言

  • 概述

    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

实现流程

导入依赖

  • 依赖版本要求
    EasyExcel读取excel版本要求

    
        <!-- 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变量,故出现以上错误。

  • 解决方案

    1. Controller层downloadExcel方法返回值类型改为void,不再重定向到其他页面。

    2. 在调用方法上添加@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

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值