原文
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
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的數據讀取會稍微麻煩一點,直接通過工具類讀取到的數據不能直接處理,需要借助一個中間的類 監聽器 類,大致的流程如下圖
監聽器的代碼如下,有詳細的注釋
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);
}
}
復制代碼
注意:
- 監聽器這個類不能夠被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();
}
復制代碼
讀取的結果如下:
圖片的導出
在EasyExcel中支持多種圖片的導入導出,什么意思呢?一般情況下,數據庫表中的圖片字段存儲的是圖片路徑,讀取圖片文件導出是支持的,除此之外,還支持網絡路徑,流,字節數組等方式。我的案例以讀取本地圖片文件為主。
官方文檔地址:https://alibaba-easyexcel.github.io/quickstart/write.html#圖片導出
實體類改造
導出的代碼是【不需要做任何改變】
百萬數據導入導出
EasyExcel最大的特點就是能夠避免內存溢出,那么是怎么做到的呢?
首先我們說一下為什么Poi會有內存溢出的風險
Poi在讀取Excel文件的時候,會先將所有的數據都讀取的內存中,再進行處理。這個時候如果讀取的文件數據量比較大,就會發生java.lang.OutOfMemoryError: Java heap space錯誤。
那么EasyPoi是怎么做的呢?
-
EasyPoi 不會一次性把整個文件讀取到內存中,而是用過流,一邊讀取一邊處理
-
在處理的時候,EasyPoi采用讀取一條數據就處理一條的方法,保證了不在內存中存儲太多的數據,可以自己設置一個界限,例如設置每500條存儲一次數據庫。
接下來我們通過代碼測試一下百萬數據的導入和導出
官網有性能測試,需要的話可以查看官網的測試:https://alibaba-easyexcel.github.io
百萬數據導出
思路
- 分頁讀取數據
- 將每次讀取到的數據寫入Excel
- 拋棄樣式,字體等不重要的數據
准備一個百萬數據的用戶表
用戶實體類如下:
@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();
}
復制代碼
執行的總時間為:
百萬數據讀取
百萬數據的Excel文件是不能通過Poi直接讀取的,我測試了一下,發生一下異常
如果要使用Poi的話 需要做一些處理 接下來我們通過EasyExcel看一下效果
使用EasyExcel讀取文件分為兩步:
- 創建監聽器
- 讀取
監聽器代碼
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();
}
復制代碼
代碼是可以完美運行的