SpringBoot圖文教程14—阿里開源EasyExcel「為百萬數據讀寫設計」


原文
EasyExcel 是阿里巴巴開源的一個Java操作Excel的技術,和EasyPoi一樣是封裝Poi的工具類。但是不同的地方在於,在EasyExcel中解決了Poi技術讀取大批量數據耗費內存的問題。當然了,也封裝了很多常用的Excel操作

  • 最基本的導入導出
  • 圖片的導入導出
  • 百萬數據的導入導出

官方地址:https://alibaba-easyexcel.github.io/quickstart/write.html

最基本的導入導出

最基本的導入導出,要導出的數據的實體類如下:

public class Teacher {
    /**
     * 老師的主鍵
     */
    private Integer teacherId;
    /**
     * 名字
     */
    private String teacherName;
    /**
     * 頭像圖片地址
     */
    private String teacherImage;
    /**
     * 老師的狀態 0代表正常 1代表刪除
     */
    private Integer teacherStatus;
}
復制代碼

省略get set

1.導入依賴

<dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>2.0.5</version>
        </dependency>
復制代碼

2.給要導出數據實體類加注解

EasyExcel也是注解式開發,常用注解如下

  • ExcelProperty 指定當前字段對應excel中的那一列
  • ExcelIgnore 默認所有字段都會和excel去匹配,加了這個注解會忽略該字段
  • DateTimeFormat 日期轉換,用String去接收excel日期格式的數據會調用這個注解。里面的value參照java.text.SimpleDateFormat
  • NumberFormat 數字轉換,用String去接收excel數字格式的數據會調用這個注解。里面的value參照java.text.DecimalFormat

img

3.直接導入導出

導出代碼

   /**
     * 基本的導出
     */
    @Test
    public void test1(){
//        准備數據
        List<Teacher> teachers = new ArrayList<>();
        teachers.add(new Teacher(1,"hhh","hhh.jpg",1));
        teachers.add(new Teacher(1,"hhh","hhh.jpg",1));
        teachers.add(new Teacher(1,"hhh","hhh.jpg",1));
        teachers.add(new Teacher(1,"hhh","hhh.jpg",1));

        String fileName =  "/Users/lubingyang/Desktop/hhhh.xlsx";
        // 這里 需要指定寫用哪個class去寫,然后寫到第一個sheet,名字為模板 然后文件流會自動關閉
        // 如果這里想使用03 則 傳入excelType參數即可
        EasyExcel.write(fileName, Teacher.class).sheet("模板").doWrite(teachers);


    }
復制代碼

導入代碼

關於EasyExcel的數據讀取會稍微麻煩一點,直接通過工具類讀取到的數據不能直接處理,需要借助一個中間的類 監聽器 類,大致的流程如下圖

img

監聽器的代碼如下,有詳細的注釋

package com.lu.booteasyexcel;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.lu.booteasyexcel.dao.TeacherDao;
import com.lu.booteasyexcel.entity.Teacher;

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

// 有個很重要的點 DemoDataListener 不能被spring管理,要每次讀取excel都要new,然后里面用到spring可以構造方法傳進去
public class DemoDataListener extends AnalysisEventListener<Teacher> {
    /**
     * 每隔5條存儲數據庫,實際使用中可以3000條,然后清理list ,方便內存回收
     */
    private static final int BATCH_COUNT = 5;

    /**
     * 這個集合用於接收 讀取Excel文件得到的數據
     */
    List<Teacher> list = new ArrayList<Teacher>();

    /**
     * 假設這個是一個DAO,當然有業務邏輯這個也可以是一個service。當然如果不用存儲這個對象沒用。
     */
    private TeacherDao teacherDao;

    public DemoDataListener() {

    }

    /**
     *
     * 不要使用自動裝配
     * 在測試類中將dao當參數傳進來
     */
    public DemoDataListener(TeacherDao teacherDao) {
        this.teacherDao = teacherDao;
    }

    /**
     * 這個每一條數據解析都會來調用
     *
     */
    @Override
    public void invoke(Teacher teacher, AnalysisContext context) {

        list.add(teacher);
        // 達到BATCH_COUNT了,需要去存儲一次數據庫,防止數據幾萬條數據在內存,容易OOM
        if (list.size() >= BATCH_COUNT) {
            saveData();
            // 存儲完成清理 list
            list.clear();
        }
    }

    /**
     * 所有數據解析完成了 都會來調用
     *
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        // 這里也要保存數據,確保最后遺留的數據也存儲到數據庫
        saveData();

    }

    /**
     * 加上存儲數據庫
     */
    private void saveData() {
//        在這個地方可以調用dao  我們就直接打印數據了
        System.out.println(list);
    }
}
復制代碼

注意:

  1. 監聽器這個類不能夠被Spring管理,每次使用單獨的new出來

導入的代碼如下:

 /**
     * 添加數據庫用到的dao
     */
    @Autowired
    private TeacherDao teacherDao;
    /**
     * 最簡單的讀
     */
    @Test
    public void simpleRead() {
        String fileName =  "/Users/lubingyang/Desktop/hhhh.xlsx";

        // 這里 需要指定讀用哪個class去讀,然后讀取第一個sheet 文件流會自動關閉
        /**
         * 參數1 要讀取的文件
         * 參數2 要讀取的數據對應的實體類類對象
         * 參數3 監聽器對象 可以在創建的時候把dao當做參數傳進去
         */
        EasyExcel.read(fileName, Teacher.class, new DemoDataListener(teacherDao)).sheet().doRead();


    }
復制代碼

讀取的結果如下:

img

圖片的導出

在EasyExcel中支持多種圖片的導入導出,什么意思呢?一般情況下,數據庫表中的圖片字段存儲的是圖片路徑,讀取圖片文件導出是支持的,除此之外,還支持網絡路徑,流,字節數組等方式。我的案例以讀取本地圖片文件為主。

官方文檔地址:https://alibaba-easyexcel.github.io/quickstart/write.html#圖片導出

實體類改造

img

導出的代碼是【不需要做任何改變】

img

百萬數據導入導出

EasyExcel最大的特點就是能夠避免內存溢出,那么是怎么做到的呢?

首先我們說一下為什么Poi會有內存溢出的風險

Poi在讀取Excel文件的時候,會先將所有的數據都讀取的內存中,再進行處理。這個時候如果讀取的文件數據量比較大,就會發生java.lang.OutOfMemoryError: Java heap space錯誤。

那么EasyPoi是怎么做的呢?

  1. EasyPoi 不會一次性把整個文件讀取到內存中,而是用過流,一邊讀取一邊處理

  2. 在處理的時候,EasyPoi采用讀取一條數據就處理一條的方法,保證了不在內存中存儲太多的數據,可以自己設置一個界限,例如設置每500條存儲一次數據庫。

    img

接下來我們通過代碼測試一下百萬數據的導入和導出

官網有性能測試,需要的話可以查看官網的測試:https://alibaba-easyexcel.github.io

百萬數據導出

思路

  1. 分頁讀取數據
  2. 將每次讀取到的數據寫入Excel
  3. 拋棄樣式,字體等不重要的數據

准備一個百萬數據的用戶表

img

用戶實體類如下:

@Data
public class CmfzUser implements Serializable {
   @ExcelIgnore
    private Integer userId;
    @ExcelProperty("手機號")
    private String userTelphone;
    private String userPassword;
    @ExcelProperty("頭像地址")
    private String userImage;
    @ExcelProperty("昵稱")
    private String userNickname;
    @ExcelProperty("名字")
    private String userName;
    @ExcelProperty("性別")
    private String userSex;
    @ExcelProperty("個性簽名")
    private String userAutograph;
    @ExcelProperty("省份")
    private String userProvince;
    @ExcelProperty("城市")
    private String userCity;
    @ExcelIgnore
    private Integer guruId;
    @ExcelIgnore
    private Integer userStatus;
    @ExcelIgnore
    private Date userCreateDate;

}
復制代碼

代碼:

    @Test
    public void test10() throws IOException {
        Date start = new Date();
//        一百萬數據 分頁查詢數據庫
        Integer userCount = 1000000;
        Integer size = 20000;
        Integer pageCount = userCount / size + 1;


        List<CmfzUser> users = null;
        String fileName =  "/Users/lubingyang/Desktop/大數據.xlsx";

        // 這里 需要指定寫用哪個class去寫
        ExcelWriter excelWriter = EasyExcel.write(fileName, CmfzUser.class).build();

//        這里注意 如果同一個sheet只要創建一次
        WriteSheet writeSheet = EasyExcel.writerSheet("大數據").build();

//        查詢測試 頁數  每次查詢20w條數據
        for (int i = 1; i <= pageCount; i++) {
            System.out.println(i);
            users = userDao.selectPage(new Page<>(i, size), null).getRecords();
//            數據寫出
            excelWriter.write(users, writeSheet);

            users.clear();
        }


        Date end = new Date();
        System.out.println(new Date().getTime() - start.getTime()+"ms");

        // 千萬別忘記finish 會幫忙關閉流
        excelWriter.finish();

    }
復制代碼

執行的總時間為:

img

百萬數據讀取

百萬數據的Excel文件是不能通過Poi直接讀取的,我測試了一下,發生一下異常

img

如果要使用Poi的話 需要做一些處理 接下來我們通過EasyExcel看一下效果

使用EasyExcel讀取文件分為兩步:

  1. 創建監聽器
  2. 讀取

監聽器代碼

public class UserDataListener extends AnalysisEventListener<CmfzUser> {

    List<CmfzUser> list = new ArrayList<>();

    /**
     * 假設這個是一個DAO,當然有業務邏輯這個也可以是一個service。當然如果不用存儲這個對象沒用。
     */
    private CmfzUserDao userDao;

    public UserDataListener() {

    }

    /**
     *
     * 不要使用自動裝配
     * 在測試類中將dao當參數傳進來
     */
    public UserDataListener(CmfzUserDao userDao) {
        this.userDao = userDao;
    }

    /**
     * 這個每一條數據解析都會來調用
     *
     */
    @Override
    public void invoke(CmfzUser user, AnalysisContext context) {

        list.add(user);
        // 可以設置超過500條了,需要去存儲一次數據庫,防止數據幾萬條數據在內存,容易OOM
        if (list.size() >= 3000) {
            saveData();
            // 存儲完成清理 list
            list.clear();
        }
    }

    /**
     * 所有數據解析完成了 都會來調用
     *
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        // 這里也要保存數據,確保最后遺留的數據也存儲到數據庫
        saveData();

    }

    /**
     * 加上存儲數據庫
     */
    private void saveData() {
//        在這個地方可以調用dao  我們就直接打印數據了
teacherDao.addList(list);
        System.out.println("存儲數據:"+list.size()+"條");
    }
}
復制代碼

讀取代碼

 @Test
    public void test4(){
        String fileName =  "/Users/lubingyang/Desktop/大數據.xlsx";

        // 這里 需要指定讀用哪個class去讀,然后讀取第一個sheet 文件流會自動關閉
        /**
         * 參數1 要讀取的文件
         * 參數2 要讀取的數據對應的實體類類對象
         * 參數3 監聽器對象 可以在創建的時候把dao當做參數傳進去
         */
        EasyExcel.read(fileName, CmfzUser.class, new UserDataListener(userDao)).sheet().doRead();


    }
復制代碼

代碼是可以完美運行的

img


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM