EasyExcel
一、初識 EasyExcel
1. Apache POI
Apache POI
是Apache軟件基金會的開源小項目,它提供了 Java 的 API 來實現對Microsoft Office
(word、excel、ppt)格式檔案的讀寫。但是存在如下一些問題:
- 學習使用成本較高
1、對 POI 有過深入了解的才知道原來 POI 還有 SAX模式(相對於Dom解析模式)。
Dom解析模式:一次性把文檔加載到內存中,內存消耗巨大
SAX模式:一行行去讀文檔,算是Dom解析模式的優化
2、但SAX模式相對比較復雜,excel有03(xls)和07(xlsx)兩種版本,兩個版本數據存儲方式截然不同,sax解析方式也各不一樣。(版本不同解析的方法也不同,學習成本再一次提高)
3、想要了解清楚這兩種解析方式,才去寫代碼測試,估計兩天時間是需要的。再加上即使解析完,要轉換到自己業務模型還要很多繁瑣的代碼。總體下來感覺至少需要三天,由於代碼復雜,后續維護成本巨大。(學習和維護成本都很高)
4、POI的SAX模式的API可以一定程度的解決一些內存溢出的問題,但是POI還是有一些缺陷,比如07版Excel解壓縮以及解壓后存儲都是在內存中完成的,內存消耗依然很大,一個3M的Excel用POI的SAX解析,依然需要100M左右內存。(內存消耗07版的依舊很大)
- POI的內存消耗較大
大部分使用POI都是使用他的 userModel 模式。userModel 的好處是上手容易使用簡單,隨便拷貝個代碼跑一下,剩下就是寫業務轉換了,雖然轉換也要寫上百行代碼,相對比較好理解。然而 userModel 模式最大的問題是在於非常大的內存消耗,一個幾兆的文件解析要用掉上百兆的內存。現在很多應用采用這種模式,之所以還正常在跑一定是並發不大,並發上來后一定會OOM或者頻繁的full gc。(想要快速上手,內存消耗就非常大,高並發環境下內存很容易溢出)
總體上來說,簡單寫法重度依賴內存,復雜寫法學習成本高。但是POI的功能還是特別豐富強大的。
2. EasyExcel
1、EasyExcel 重寫了 POI 對07版 Excel 的解析,可以把內存消耗從100M 左右降低到 10M 以內,並且再大的 Excel 不會出現內存溢出。(內存消耗少了一個數量級)
2、但 EasyExcel 對 03 版的 Excel 仍使用 POI 的 SAX 模式。
3、EasyExcel 的效率也很高,在 64M 內存環境下,讀取 75M (46W行25列)的 Excel 能在 1 分鍾內跑完。(性能不錯)
4、在上層做了模型轉換的封裝,讓使用者更加簡單方便。(使用簡單)
總體上來說,使用簡單,內存消耗低(可以有效避免OOM),但只能操作 Excel,且不能讀取圖片。
二、快速入門
我們通常讀取 Excel 中的內容使用對應的實體類進行封裝,最終再存入數據庫。( Excel 的讀 )
或者將數據庫的數據使用對應的實體類進行封裝,再寫入 Excel。( Excel 的寫 )
1. Excel 的讀
1、引入依賴,easyexcel、lombok
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.2.10</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</dependency>
發現 easyexcel 依賴包含了 apache 的 poi
2、 編寫實體類,和 Excel 數據對應
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
private String id;
private String name;
private String gender;
private Date birthday;
}
3、編寫一個測試類,編寫簡單的讀 Excel 的 Demo
@Test
public void testReadExcel() {
/*
獲得一個工作簿讀對象
參數一:Excel文件路徑
參數二:每行數據對應的實體類型
參數三:讀監聽器,每讀一行就會調用一次該監聽器的invoke方法
*/
ExcelReaderBuilder readWorkBook = EasyExcel.read("E:/學生信息.xlsx", Student.class, new StudentReadListener());
// 獲得一個工作表對象
ExcelReaderSheetBuilder sheet = readWorkBook.sheet();
// 讀取工作表中的內容
sheet.doRead();
}
EasyExcel.read 方法需要一個讀監聽器去處理每次讀出的一行數據,每讀一行就觸發一次 invoke 方法,將讀到的數據封裝成實體傳入到參數中。
public class StudentReadListener extends AnalysisEventListener<Student> {
/**
* EasyExcel 每讀一行就會調用一次此方法,把讀到的數據存入 student 中
* @param student 每讀一行的數據
* @param analysisContext
*/
@Override
public void invoke(Student student, AnalysisContext analysisContext) {
System.out.println(student);
}
/**
* 讀取完整個文檔之后調用的方法
* @param analysisContext
*/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
System.out.println("Excel全部讀取完畢了!");
}
}
4、運行讀 Excel的 Demo,查看結果
2. Excel 的寫
1、編寫一個測試類,編寫簡單的寫 Excel 的 Demo
@Test
public void testWriteExcel() {
/*
獲得一個工作簿寫對象
參數一:導出的Excel文件路徑
參數二:每行數據對應的實體類型
*/
ExcelWriterBuilder writeWorkBook = EasyExcel.write("E:/學生信息2.xlsx", Student.class);
// 獲得一個工作表對象
ExcelWriterSheetBuilder sheet = writeWorkBook.sheet();
// 准備學生數據
List<Student> students = initData();
// 寫入工作表
sheet.doWrite(students);
}
private List<Student> initData(){
List<Student> students = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
Student student =new Student();
student.setName("學員"+i);
student.setBirthday(new Date());
student.setGender("男");
students.add(student);
}
return students;
}
2、運行寫 Excel 的 Demo,查看結果
問題:
- 是否能隱藏id列?
- 表頭的名稱是否能修改?
- 表頭的列寬能否改變?
- 每列的順序能否調換?
三、問題優化
1. 表頭的名稱修改
1、解決方法:實體類屬性的名字決定了表頭的名稱,在實體類屬性上添加 @ExcelProperty
注解,並指定對應名稱的 value。
public class Student {
@ExcelProperty(value = "ID") // 修改表頭的名稱
private String id;
@ExcelProperty(value = "學生姓名") // 修改表頭的名稱
private String name;
@ExcelProperty(value = "學生性別") // 修改表頭的名稱
private String gender;
@ExcelProperty(value = "學生生日") // 修改表頭的名稱
private Date birthday;
}
2、再次運行寫Excel的Demo,結果如下:
2. 表頭的列寬修改
1、解決方法:在實體類屬性上添加 @ColumnWidth
注解,值為 Excel 表中的列寬大小。
public class Student {
@ExcelProperty(value = "ID")
private String id;
@ExcelProperty(value = "學生姓名")
@ColumnWidth(20) // 修改表頭的列寬
private String name;
@ExcelProperty(value = "學生性別")
@ColumnWidth(20) // 修改表頭的列寬
private String gender;
@ExcelProperty(value = "學生生日")
@ColumnWidth(20) // 修改表頭的列寬
private Date birthday;
}
2、再次運行寫Excel的Demo,結果如下:
也可以在類上使用
@ColumnWidth
注解,整個表頭相同列寬。
3. 行高的修改
1、可以修改 Excel 表中內容的行高,在類上添加 @ContentRowHeight
注解,值為內容行高的大小。
2、可以修改表頭的行高,在類上添加 @HeadRowHeight
注解,值為表頭行高的大小。
@ContentRowHeight(10) // 修改 Excel 表中內容的行高
@HeadRowHeight(15) // 修改表頭的行高
public class Student {
@ExcelProperty(value = "ID")
private String id;
@ExcelProperty(value = "學生姓名")
@ColumnWidth(20)
private String name;
@ExcelProperty(value = "學生性別")
@ColumnWidth(20)
private String gender;
@ExcelProperty(value = "學生生日")
@ColumnWidth(20)
private Date birthday;
}
3、再次運行寫Excel的Demo,結果如下:
4. 修改列的順序
1、在實體類的屬性的 @ExcelProperty
注解里,添加一個屬性 index ,對應了列的位置。
注意:index對應的是列的位置,從0開始。
如果index從1開始寫,在excel表中的第一列會被空出來。
如果index寫為5,在excel表中就會放在第6列。
@ContentRowHeight(10)
@HeadRowHeight(15)
public class Student {
@ExcelProperty(value = "ID", index = 3)
private String id;
@ExcelProperty(value = "學生姓名", index = 0)
@ColumnWidth(20)
private String name;
@ExcelProperty(value = "學生性別", index = 1)
@ColumnWidth(20)
private String gender;
@ExcelProperty(value = "學生生日", index = 2)
@ColumnWidth(20)
private Date birthday;
}
2、再次運行寫Excel的Demo,結果如下:
5. 關於Excel讀
1、如果未指定 @ExcelProperty
注解,會將 Excel 表中從左到右列的值,分別讀取到實體類的從上到下的屬性中。
2、如果實體類的屬性上指定了@ExcelProperty(value = "學生姓名", index = 0)
注解,會根據 value 值去尋找表頭名和“學生姓名”相同的列的值存入實體類的屬性中;或者可以根據 index 去尋找第1(0+1)的列的值存入實體類的屬性中。
如果是 Excel 讀的實體類的話,建議要么所有的屬性都不加
@ExcelProperty
注解,要么全用@ExcelProperty(value="xxx")
通過 value 值匹配,要么全加@ExcelProperty(index=x)
通過列數去匹配,建議不要三種混合用。
6. 指定列隱藏
1、解決方法:在實體類需要需要隱藏的屬性上添加 @ExcelIgnore
注解,這一個數據就不會寫入 Excel 和讀取 Excel 了。
@ContentRowHeight(10)
@HeadRowHeight(15)
public class Student {
@ExcelIgnore // 忽略屬性,不參與讀寫
private String id;
@ExcelProperty(value = "學生姓名", index = 0)
@ColumnWidth(20)
private String name;
@ExcelProperty(value = "學生性別", index = 1)
@ColumnWidth(20)
private String gender;
@ExcelProperty(value = "學生生日", index = 2)
@ColumnWidth(20)
private Date birthday;
}
2、再次運行寫Excel的Demo,結果如下:
也可以在類上添加
@ExcelIgnoreUnannotated
注解,添加這個注解后,只有實體類屬性上有 @ExcelProperty 注解才會參與讀寫。
7. 日期格式的轉換
1、解決方法:在實體類的日期屬性上添加 @DateTimeFormat
注解,值為日期格式。
@ContentRowHeight(10)
@HeadRowHeight(15)
public class Student {
@ExcelProperty(value = "ID", index = 3)
@ExcelIgnore
private String id;
@ExcelProperty(value = "學生姓名", index = 0)
@ColumnWidth(20)
private String name;
@ExcelProperty(value = "學生性別", index = 1)
@ColumnWidth(20)
private String gender;
@ExcelProperty(value = "學生生日", index = 2)
@ColumnWidth(20)
@DateTimeFormat("yyyy-MM-dd") // 修改日期格式
private Date birthday;
}
2、再次運行寫Excel的Demo,結果如下:
數字類型的格式也可以指定,需要在實體類的數字屬性上添加
@NumberFormat
注解,數字格式可以參考java.text.DecimalFormat
。
8. 多表頭的寫
1、解決方法:在實體類的 @ExcelProperty
注解的value屬性發現是一個數據類型,說明可以添加多個value。
2、若只在一個屬性的注解上添加多個value,查看效果:
public class Student {
@ExcelIgnore
private String id;
@ExcelProperty(value = {"學生信息表","學生姓名"}, index = 0)
@ColumnWidth(20)
private String name;
@ExcelProperty(value = "學生性別", index = 1)
@ColumnWidth(20)
private String gender;
@ExcelProperty(value = "學生生日", index = 2)
@ColumnWidth(20)
@DateTimeFormat("yyyy-MM-dd")
private Date birthday;
}
3、若在所有的屬性的注解上添加多個value,查看效果,發現表頭自動合並了,可見相同value的表頭屬性會自動合並:
public class Student {
@ExcelIgnore
private String id;
@ExcelProperty(value = {"學生信息表","學生姓名"}, index = 0)
@ColumnWidth(20)
private String name;
@ExcelProperty(value = {"學生信息表","學生性別"}, index = 1)
@ColumnWidth(20)
private String gender;
@ExcelProperty(value = {"學生信息表","學生生日"}, index = 2)
@ColumnWidth(20)
@DateTimeFormat("yyyy-MM-dd")
private Date birthday;
}
讀取多表頭的 Excel 時,可以使用 ExcelReaderBuilder 中的 headRowNumber(Integer num) 方法,跳過前 num 行讀取數據。
如果設置 2,則跳過前兩行表頭從第三行開始讀取數據。
四、報表的填充
1、EasyExcel支持調整行高、列寬、背景色、字體大小等內容,但是控制方式與使用原生POI無異,比較繁瑣,不建議使用。
2、我們一般是先寫好幾套 excel 的模板,模板里面不存數據,只有報表的樣式。
3、這時我們就需要使用 easyexcel 對報表數據進行填充。
1. 單個數據填充
1、准備模板
Excel表格中用{} 來包裹要填充的變量,如果單元格文本中本來就有{
、}
左右大括號,需要在括號前面使用斜杠轉義\{
、\}
。
代碼中用來填充數據的實體對象的成員變量名或被填充map集合的key需要和Excel中被{}包裹的變量名稱一致。
模板的文件名這里名為 excel_template_01.xlsx
2、准備實體類
@Data
public class FillData {
private String name;
private int age;
}
3、編寫填充 Demo
@Test
public void testFullExcel() {
// 獲得工作簿對象
ExcelWriterBuilder writeWorkBook = EasyExcel.write("E:/填充一組數據.xlsx", FillData.class)
.withTemplate("E:/excel_template_01.xlsx"); // withTemplate 指定模板文件
// 獲得一個工作表對象
ExcelWriterSheetBuilder sheet = writeWorkBook.sheet();
// 准備填充數據
FillData fillData = new FillData();
fillData.setName("小明");
fillData.setAge(18);
// 寫入工作表
sheet.doFill(fillData); // doFill 開始填充
}
2. 多組數據填充
1、准備模板
和單個數據的模板類似,只是在大括號中的前面添加一個.
模板的文件名這里名為 excel_template_02.xlsx
2、編寫填充 Demo
@Test
public void testFullExcel() {
ExcelWriterBuilder writeWorkBook = EasyExcel.write("E:/填充多組數據.xlsx", FillData.class)
.withTemplate("E:/excel_template_02.xlsx");
// 獲得一個工作表對象
ExcelWriterSheetBuilder sheet = writeWorkBook.sheet();
// 准備填充數據
List<FillData> fillDataList = this.initFillData();
// 寫入工作表
sheet.doFill(fillDataList);
}
private List<FillData> initFillData() {
List<FillData> fillDataList = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
FillData fillData = new FillData();
fillData.setName("小明"+i);
fillData.setAge(18+i);
fillDataList.add(fillData);
}
return fillDataList;
}
3. 組合數據填充
又有單個數據,又有多組數據。
1、准備模板
即有多組數據填充,又有單一數據填充,為了避免兩者數據出現沖突覆蓋的情況,在多組填充時需要通過FillConfig
對象設置換行。
2、編寫填充 Demo
@Test
public void testFullExcel() {
ExcelWriter writeWorkBook = EasyExcel.write("E:/組合填充.xlsx", FillData.class)
.withTemplate("E:/excel_template_03.xlsx").build();
// 獲得一個工作表對象
WriteSheet writeSheet = EasyExcel.writerSheet().build();
// 准備填充數據
List<FillData> fillDataList = this.initFillData();
Map<String,String> dateAndTotal = new HashMap<>();
dateAndTotal.put("date","2021-9-15");
dateAndTotal.put("total","10086");
// 多組填充
writeWorkBook.fill(fillDataList,writeSheet);
// 單組填充
writeWorkBook.fill(dateAndTotal,writeSheet);
// 關閉流,切記!
writeWorkBook.finish();
}
測試發現,如果多組填充在前面,多組填充后沒有新增行,導致后續單組填充時,把之前多組填充的值覆蓋了!
3、可以將單個數據放在多個數據前面,或者需要設置多組填充時能夠添加一行,可以通過FillConfig
對象設置換行。
@Test
public void testFullExcel() {
ExcelWriter writeWorkBook = EasyExcel.write("E:/組合填充.xlsx", FillData.class)
.withTemplate("E:/excel_template_03.xlsx").build();
// 獲得一個工作表對象
WriteSheet writeSheet = EasyExcel.writerSheet().build();
// 准備填充數據
List<FillData> fillDataList = this.initFillData();
Map<String, String> dateAndTotal = new HashMap<>();
dateAndTotal.put("date", "2021-9-15");
dateAndTotal.put("total", "10086");
// 填充后換行
FillConfig fillConfig = FillConfig.builder().forceNewRow(true).build();
// 多組填充,填充后要換行
writeWorkBook.fill(fillDataList, fillConfig, writeSheet);
// 單組填充
writeWorkBook.fill(dateAndTotal, writeSheet);
// 關閉流,切記!
writeWorkBook.finish();
}
4. 水平數據填充
數據向右水平填充,而不是默認的向下。
1、准備模板
2、編寫填充 Demo,可以通過FillConfig
對象設置水平填充。
@Test
public void testHorizontalExcel() {
ExcelWriter writeWorkBook = EasyExcel.write("E:/水平填充.xlsx", FillData.class)
.withTemplate("E:/excel_template_04.xlsx").build();
// 獲得一個工作表對象
WriteSheet writeSheet = EasyExcel.writerSheet().build();
// 准備填充數據
List<FillData> fillDataList = this.initFillData();
Map<String, String> dateAndTotal = new HashMap<>();
dateAndTotal.put("date", "2021-9-15");
dateAndTotal.put("total", "10086");
// 水平填充
FillConfig fillConfig = FillConfig.builder().direction(WriteDirectionEnum.HORIZONTAL).build();
// 多組填充,需要水平填充
writeWorkBook.fill(fillDataList, fillConfig, writeSheet);
// 單組填充
writeWorkBook.fill(dateAndTotal, writeSheet);
// 關閉流,切記!
writeWorkBook.finish();
}