使用新版本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一下項目,謝謝。

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