EasyExcel


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();
}


免責聲明!

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



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