使用新版本EasyExcel优雅的导入和导出Excel文件

一、需求描述

最近系统后台有个关于使用Excel文件导出数据的需求,首先想到了POI和阿里的EasyExcel。经过技术选型和结合具体场景,最终决定使用EasyExcel。
使用过程中,遇到了一些问题,然后去查找资料,发现大多数资料都比较老旧,都是使用1.x或者一些beta版本,为了解决这个问题,我就自己去查阅了资料,为了方便后续使用,在此记录一下,也和大家分享一下,如何优雅的使用EasyExcel导入和导出文件。

二、性能比较

主流office文档操作组件性能比较。

组件 功能简介 使用场景 测试环境 内存消耗 读取时间 写入时间 文件大小
poi 1. 对Microsoft Office格式档案读和写的功能
2. HSSF提供读写Excel XLS
3. HPSF提供读写OLE2 Property Sets
4. POIFS提供读写OLE2 Filesystem
1. 操作Excel XLS
2. HSSFWorkbook只能解析2003之前版本xls格式
3. 使用HSSF时sheet最大行数65536,最大列数256
Win64 4核8g jdk1.8 5万行2列excel xls R:206.88MB W:138.34MB 1049ms 2005ms 4.15MB
poi-ooxml 1. poi升级扩展版本
2. XSSF提供读写XLSX
3. XSLF提供读写PPTX
4. XWPF提供读写DOCX
5. CommonSS读写XLS、XLSX
1. 操作pptx、docx、xlsx等
2. XSSF基于内存写入方式,一个sheet最大行数1048576,最大列数16384
3. SXSSF是在XSSF基础上基于内存+磁盘写入方式,用于大数据量的导出
Win64 4核8g jdk1.8 5万行2列excel xlsx/5万行word docx XSSF-R:185.04MB XSSF-W:405.58MB SXSSF-R:140.34MB SXSSF-W:41.83MB XWPF-R:23.14MB XWPF-W:158.21MB XSSF:2502ms SXSSF:1354ms XWPF:634ms XSSF:4644ms SXSSF:1417ms XWPF:21555ms XSSF:1.36MB SXSSF:1.33MB XWPF:999KB
poi-scratchpad 1. HWPF提供读写Word DOC
2. HSLF提供读写PPT
3. HDGF提供读Visio VSD
4. HPBF提供读Publisher PUB
5. HSMF提供读Outlook MSG
1. 操作PPT、DOC、VSD、PUB、MSG等格式
2. HWPFDocument写doc文件必须要先有doc文件
3. 不建议使用HWPF等低版本office
Win64 4核8g jdk1.8 5万行word doc R:81.80MB W:90.39MB 221ms 538ms 3.74MB
easyexcel 阿里开源,重写了poi对Excel2007版的解析,不会出现OOM,2003版依赖POI的sax模式 xls、xlsx操作 Win64 4核8g jdk1.8 5万行2列xlsx R:80.10MB W:60.56MB 1053ms 1149ms 1.33MB

三、项目代码

源码地址:https://github.com/wangming2674/easyexcel-basic-demo

1.项目依赖

demo项目基于springboot2.2.1版本。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/>
    </parent>

    <groupId>com.evan.easyexcel</groupId>
    <artifactId>easyexcel-basic-demo</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>easyexcel-basic-demo</name>
    <description>Easy excel 2.x import and export demo</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
            <version>4.1.0</version>
        </dependency>

        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>4.1.0</version>
        </dependency>

        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-scratchpad</artifactId>
            <version>4.1.0</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>2.1.6</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

2.ExcelApplication 主启动类

package com.evan.easyexcel;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ExcelApplication {

    public static void main(String[] args) {
        SpringApplication.run(ExcelApplication.class, args);
    }
}

3.定义导入和导出模型

package com.evan.easyexcel.model;

import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.annotation.write.style.ContentRowHeight;
import com.alibaba.excel.annotation.write.style.HeadRowHeight;
import lombok.Data;

/**
 * @ClassName ExportModel
 * @Description 导出模型
 * @Author Evan Wang
 * @Version 1.0.0
 * @Date 2020/4/1 20:55
 */
@ContentRowHeight(20)
@HeadRowHeight(25)
@ColumnWidth(25)
@Data
public class ExportModel  {

    @ExcelProperty(value = "姓名" ,index = 0)
    private String name;

    @ExcelProperty(value = "性别" ,index = 1)
    private String sex;

    @ExcelProperty(value = "年龄" ,index = 2)
    private Integer age;

}
package com.evan.easyexcel.model;

import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;

/**
 * @ClassName ImportModel
 * @Description 导入模型
 * @Author Evan Wang
 * @Version 1.0.0
 * @Date 2020/4/1 20:54
 */
@Data
public class ImportModel {

    @ExcelProperty(index = 0)
    private String date;

    @ExcelProperty(index = 1)
    private String author;

    @ExcelProperty(index = 2)
    private String book;

}

4.Object与实体类转换工具类

package com.evan.easyexcel.utils.common;

import org.springframework.beans.BeanUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * @ClassName ExportModel
 * @Description Object与实体类转换工具类
 * @Author Evan Wang
 * @Version 1.0.0
 * @Date 2020/4/1 20:59
 */
public class BeanConvert {
    private BeanConvert(){}
    /**
     * 将List<Object> 转换为List<Bean>
     * @param sources 源对象
     * @param targetClass 目标类
     * @param <T>
     * @return
     */
    public static <T> List<T> objectConvertBean(List<?> sources, Class<T> targetClass) {
        List<?> sourcesObj = sources;
        if (sourcesObj == null) {
            sourcesObj = Collections.emptyList();
        }
        List<T> targets = new ArrayList<>(sourcesObj.size());
        convert(sourcesObj, targets, targetClass);
        return targets;
    }

    /**
     * 复制源对象到目的对象
     * 注意:
     *   org.springframework.beans.BeanUtils.copyProperties 是一个Spring提供的名称相同的工具类
     *   但它不支持类型自动转换,如果某个类型属性不同,则不予转换那个属性
     *   org.apache.commons.beanutils.BeanUtils 是一个Apache提供的名称相同的工具类
     *   支持类型自动转换,如Date类型会自动转换为字符串
     * @param sources  源对象
     * @param targets 目的对象
     * @param targetClass 目标类
     * @param <T>
     */
    private static <T> void convert(List<?> sources, List<T> targets, Class<T> targetClass) {
        if (sources == null) {
            return;
        }
        if (targets == null) {
            return;
        }
        targets.clear();
        for (Object obj : sources) {
            try {
                T target = targetClass.newInstance();
                targets.add(target);
                BeanUtils.copyProperties(obj, target);
            } catch (Exception e) {
                return;
            }
        }
    }

}

5.ExcelListener 监听器

package com.evan.easyexcel.utils.excel;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.evan.easyexcel.model.ImportModel;

import java.util.ArrayList;
import java.util.List;

/**
 * @Author Evan Wang
 * @Description 监听类
 * @Date 2019-09-16
 */
public class ExcelListener extends AnalysisEventListener {
    //可以通过实例获取该值
    private List<Object> dataList = new ArrayList<>();


    @Override
    public void invoke(Object object, AnalysisContext context) {
        //数据存储到list,供批量处理,或后续自己业务逻辑处理。
        dataList.add(object);
        handleBusinessLogic();

          /*
        如数据过大,可以进行定量分批处理
        if(dataList.size()>=200){
            handleBusinessLogic();
            dataList.clear();
        }
         */
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        //非必要语句,查看导入的数据
        System.out.println("导入的数据 " + dataList.toString());
        //解析结束销毁不用的资源
        dataList.clear();
    }

    //根据业务自行实现该方法,例如将解析好的dataList存储到数据库中
    private void handleBusinessLogic() {

    }

    public List<Object> getDataList() {
        return dataList;
    }

    public void setDataList(List<Object> dataList) {
        this.dataList = dataList;
    }
}

6.ExcelUtil (核心类)

ExcelUtil对easyexcel2.X进行封装,实现一个方法完成简单的excel导入和导出。

package com.evan.easyexcel.utils.excel;

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.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import com.evan.easyexcel.utils.common.BeanConvert;
import org.apache.poi.ss.usermodel.FillPatternType;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.ArrayList;
import java.util.List;

/**
 *
 * @Author Evan Wang
 * @Description Excel读写工具类
 * @Date 2019-09-16
 */
public class ExcelUtil {

    private ExcelUtil(){}

    /**
     * 读取Excel(一个sheet)
     * @param excel 文件
     * @param clazz 实体类
     * @param sheetNo sheet序号
     * @return 返回实体列表(需转换)
     */
    public static <T> List<T> readExcel(MultipartFile excel, Class<T> clazz,int sheetNo) {

        ExcelListener excelListener = new ExcelListener();

        ExcelReader excelReader = getReader(excel,clazz,excelListener);
        if (excelReader == null) {
            return new ArrayList<>();
        }

        ReadSheet readSheet = EasyExcel.readSheet(sheetNo).build();
        excelReader.read(readSheet);
        excelReader.finish();

        return BeanConvert.objectConvertBean(excelListener.getDataList(), clazz);
    }


    /**
     * 读取Excel(多个sheet可以用同一个实体类解析)
     * @param excel 文件
     * @param clazz 实体类
     * @return 返回实体列表(需转换)
     */
    public static <T> List<T> readExcel(MultipartFile excel, Class<T> clazz) {

        ExcelListener excelListener = new ExcelListener();
        ExcelReader excelReader = getReader(excel,clazz,excelListener);

        if (excelReader == null) {
            return new ArrayList<>();
        }

        List<ReadSheet> readSheetList = excelReader.excelExecutor().sheetList();

        for (ReadSheet readSheet:readSheetList){
            excelReader.read(readSheet);
        }
        excelReader.finish();

        return BeanConvert.objectConvertBean(excelListener.getDataList(), clazz);
    }


    /**
     * 导出Excel(一个sheet)
     *
     * @param response  HttpServletResponse
     * @param list      数据list
     * @param fileName  导出的文件名
     * @param sheetName 导入文件的sheet名
     * @param clazz 实体类
     */
    public static <T> void  writeExcel(HttpServletResponse response,List<T> list, String fileName, String sheetName, Class<T> clazz) {

        OutputStream outputStream = getOutputStream(response, fileName);

        ExcelWriter excelWriter = EasyExcel.write(outputStream, clazz).build();
        WriteSheet writeSheet = EasyExcel.writerSheet(sheetName).build();

        excelWriter.write(list, writeSheet);

        excelWriter.finish();
    }


    /**
     * 导出Excel(带样式)
     *
     * @return
     */
    public static  <T> void writeStyleExcel(HttpServletResponse response,List<T> list, String fileName, String sheetName, Class<T> clazz) {
        //表头策略
        WriteCellStyle headWriteCellStyle = new WriteCellStyle();
        //背景浅灰
        headWriteCellStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
        WriteFont headWriteFont = new WriteFont();
        headWriteFont.setFontHeightInPoints((short)20);
        headWriteCellStyle.setWriteFont(headWriteFont);

        //内容策略
        WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
        //这里需要指定 FillPatternType 为FillPatternType.SOLID_FOREGROUND 否则无法显示背景颜色;头默认了FillPatternType
        contentWriteCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND);
        //背景浅绿
        contentWriteCellStyle.setFillForegroundColor(IndexedColors.LIGHT_GREEN.getIndex());
        WriteFont contentWriteFont = new WriteFont();
        //字体大小
        contentWriteFont.setFontHeightInPoints((short)15);
        contentWriteCellStyle.setWriteFont(contentWriteFont);

        HorizontalCellStyleStrategy horizontalCellStyleStrategy = new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);

        OutputStream outputStream = getOutputStream(response, fileName);
        EasyExcel.write(outputStream, clazz).registerWriteHandler(horizontalCellStyleStrategy).sheet(sheetName).doWrite(list);

    }


    /**
     * 导出Excel(动态表头)
     * write时不传入class,table时传入并设置needHead为false
     * @return
     */
    public static  <T> void writeDynamicHeadExcel(HttpServletResponse response,List<T> list, String fileName, String sheetName, Class<T> clazz,List<List<String>> headList) {

        OutputStream outputStream = getOutputStream(response, fileName);

        EasyExcel.write(outputStream)
                .head(headList)
                .sheet(sheetName)
                .table().head(clazz).needHead(Boolean.FALSE)
                .doWrite(list);
    }


    /**
     * 导出时生成OutputStream
     */
    private static OutputStream getOutputStream(HttpServletResponse response,String fileName) {
        //创建本地文件
        String filePath = fileName + ".xlsx";
        File file = new File(filePath);
        try {
            if (!file.exists() || file.isDirectory()) {
                file.createNewFile();
            }
            fileName = new String(filePath.getBytes(), "ISO-8859-1");
            response.addHeader("Content-Disposition", "filename=" + fileName);
            return response.getOutputStream();
        } catch (IOException  e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 返回ExcelReader
     * @param excel         文件
     * @param clazz         实体类
     * @param excelListener
     */
    private static <T> ExcelReader getReader(MultipartFile excel, Class<T> clazz, ExcelListener excelListener) {
        String filename = excel.getOriginalFilename();

        try {
            if (filename == null || (!filename.toLowerCase().endsWith(".xls") && !filename.toLowerCase().endsWith(".xlsx"))) {
                return null;
            }

            InputStream inputStream = new BufferedInputStream(excel.getInputStream());

            ExcelReader excelReader = EasyExcel.read(inputStream, clazz, excelListener).build();

            inputStream.close();

            return excelReader;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

7.ExcelTestController

package com.evan.easyexcel.controller;

import com.evan.easyexcel.model.ExportModel;
import com.evan.easyexcel.model.ImportModel;
import com.evan.easyexcel.utils.excel.ExcelUtil;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;

/**
 * @ClassName ExcelTestController
 * @Description
 * @Author Evan Wang
 * @Version 1.0.0
 * @Date 2020/4/1 21:25
 */
@RestController
@RequestMapping(value = "/easyExcel")
public class ExcelTestController {

    @PostMapping(value = "/import")
    public List<ImportModel> read(MultipartFile excel) {
        return ExcelUtil.readExcel(excel, ImportModel.class, 0);
    }

    @GetMapping(value = "/export")
    public void writeExcel(HttpServletResponse response) {
        List<ExportModel> list = getList();
        String fileName = "Excel导出测试";
        String sheetName = "sheet1";
        ExcelUtil.writeDynamicHeadExcel(response, list, fileName, sheetName, ExportModel.class, head());
    }

    private List<ExportModel> getList() {
        List<ExportModel> modelList = new ArrayList<>();

        ExportModel firstModel = new ExportModel();
        firstModel.setName("李明");
        firstModel.setSex("男");
        firstModel.setAge(20);
        modelList.add(firstModel);

        ExportModel secondModel = new ExportModel();
        secondModel.setName("珍妮");
        secondModel.setSex("女");
        secondModel.setAge(19);
        modelList.add(secondModel);

        return modelList;
    }

    private List<List<String>> head() {
        List<List<String>> headList = new ArrayList<>();
        List<String> nameHead = new ArrayList<>();
        nameHead.add("姓名");
        List<String> genderHead = new ArrayList<>();
        genderHead.add("性别");
        List<String> ageHead = new ArrayList<>();
        ageHead.add("年龄");
        headList.add(nameHead);
        headList.add(genderHead);
        headList.add(ageHead);

        return headList;
    }
}

8.application.yml 端口号配置

server:
  port: 8090

四、项目测试

1.导出测试

先启动项目,然后在浏览器中访问如下地址。

http://localhost:8090/easyExcel/export

在这里插入图片描述
然后可以看到,浏览器弹出了下载框,自动开始下载。
在这里插入图片描述
打开下载好的Excel,如果看到如下内容,证明导出成功。
在这里插入图片描述

2.导入测试

注:这里我把提前准备好的Excel文件放在如下路径供大家测试使用。
在这里插入图片描述

要导入的Excel文件内容:
在这里插入图片描述

导入测试这里我使用的测试工具是postman,大家也可以根据需要选择自己喜欢的测试工具,注意箭头位置一定要跟我选择的一致,否则不能导入。

http://localhost:8090/easyExcel/import

在这里插入图片描述
选完后,点击send,成功请求后查看控制台,出现以下内容,代表导入成功。

导入的数据 [ImportModel(date=20200329, author=李明, book=测试书籍一), ImportModel(date=20200330, author=珍妮, book=测试书籍二), ImportModel(date=20200331, author=李雷, book=测试书籍三), ImportModel(date=20200401, author=韩梅梅, book=测试书籍四)]

五、总结

到此为止,EasyExcel 2.x版本的代码示范就结束了,需要源码的小伙伴可以访问我的git获取。
虽然上面已经写过了,但是避免粗心的小伙伴看不到,再重复一下。

源码地址:https://github.com/wangming2674/easyexcel-basic-demo

另外,我在应用时,是前后端分离架构,前端用的vue.js。

在请求时,也可能遇见一些小坑,如果有问题,可以加入技术交流qq群:805069260。

在群里能直接联系到我,或者在我的博客留言,看到的话我都会第一时间为你解决问题的。

如果帮到你的话,麻烦帮我点个赞或者star一下项目,谢谢。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章