一、初識 EasyExcel
1.1、Apache POI
Apache POI
是Apache軟體基金會的開源函式庫,提供跨平臺的Java API
實現Microsoft Office
格式檔案讀寫。但是存在如下一些問題:學習使用成本較高、POI的記憶體消耗較大,程式碼書寫冗餘繁雜,讀寫大檔案耗費記憶體較大,容易OOM。當然,功能強大。
1.2、EasyExcel
- 在資料模型層面進行了封裝,使用簡單
- 重寫了07版本的Excel的解析程式碼,降低記憶體消耗,能有效避免OOM
- 只能操作Excel
- 不能讀取圖片
二、快速入門
引入座標依賴
<!-- EasyExcel -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.2.10</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
2.1、簡單的讀
將 excel 中的資料讀取出來,包含有姓名、性別、出生日期,excel 資料如下
2.1.1、編寫匯出資料的實體
基於 Lombok
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
private String name;
private Date birthday;
private String gender;
private String id;
}
2.1.2、讀取 Excel 檔案
其中,需要自定義監聽器,繼承官方提供的監聽器,實現兩個方法
public class ExcelEasyRead {
/**
* 工作薄: 一個 excel 檔案就是一個工作薄
* 工作表: 一個工作薄中可以有多個工作表(sheet)
*/
public static void main(String[] args) {
/**
* 構建一個工作簿
* pathName 要讀的檔案的路徑
* head 檔案中每一行資料要儲存到實體的型別的 class
* readListener 讀取監聽,每讀取一行內容,都會呼叫該物件的 invoke,在 invoke 可以操作使用讀取到的資料
* sheet方法引數: 工作表的順序號(從0開始)或者工作表的名字,不傳預設為0
*/
// 獲得一個工作簿物件
ExcelReaderBuilder readerBuilder = EasyExcel.read("read.xlsx", Student.class, new StudentListener());
// 獲得一個工作表物件
ExcelReaderSheetBuilder sheet = readerBuilder.sheet();
// 讀取工作表中的內容
sheet.doRead();
}
}
public class StudentListener extends AnalysisEventListener<Student> {
/**
* 每讀取一行內容,都會呼叫該物件的 invoke,在 invoke 可以操作使用讀取到的資料
* @param data 每次讀取到的資料封裝的物件
* @param context
*/
@Override
public void invoke(Student data, AnalysisContext context) {
System.out.println("student = " + data);
}
/**
* 全部讀完之後,會呼叫該方法
* @param context
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
}
}
執行效果如下
2.2、簡單的寫
將多個學生的資訊匯入到 Excel 表格中
2.2.1、直接寫入資料
public class ExcelEasyWrite {
public static void main(String[] args) {
/**
* pathName 要寫入的檔案路徑
* head 封裝寫入的實體的型別
* return 寫的工作簿物件
*/
// 工作簿物件
ExcelWriterBuilder writerBuilder = EasyExcel.write("write.xlsx", Student.class);
// 工作表物件
ExcelWriterSheetBuilder sheet = writerBuilder.sheet();
// 準備資料
List<Student> students = initData();
// 寫
sheet.doWrite(students);
}
private static List<Student> initData() {
ArrayList<Student> students = new ArrayList<>();
for (int i = 10; i < 20; i++) {
Student student = new Student();
student.setName("測試"+i);
student.setBirthday(new Date());
student.setGender("男");
students.add(student);
}
return students;
}
}
效果如下
自定義列名,列寬等屬性
@Data
@NoArgsConstructor
@AllArgsConstructor
// 全域性定義列寬
@ColumnWidth(10)
// 內容行高
//@ContentRowHeight(10)
// 表頭行高
@HeadRowHeight(20)
public class Student {
/**
* value 欄位名
* index 列順序
*/
@ExcelProperty(value = {"學生資訊表","姓名"},index = 0)
private String name;
@ExcelProperty(value = {"學生資訊表","出生日期"},index = 2)
@DateTimeFormat("YYYY-MM-dd")
@ColumnWidth(20)
private Date birthday;
@ExcelProperty(value = {"學生資訊表","性別"},index = 1)
private String gender;
/**
* 忽略欄位
*/
@ExcelIgnore
private String id;
效果如下
三、常用的 API 及 註解
1、常用類
EasyExcel 入口類,用於構建各種物件、開始各種操作;
ExcelReaderBuilder 構建出一個ReadWorkbook物件,即一個工作簿物件,對應的是一個Excel檔案;
ExcelWriterBuilder 構建出一個WriteWorkbook物件,即一個工作簿物件,對應的是一個Excel檔案;
ExcelReaderSheetBuilder 構建出一個ReadSheet物件,即一個工作表的物件,對應的Excel中的每個sheet,一個工作簿可以有多個工作表;
ExcelWriterSheetBuilder 構建出一WriteSheet物件,即一個工作表的物件,對應的Excel中的每個sheet,一個工作簿可以有多個工作表;
ReadListener 在每一行讀取完畢後都會呼叫ReadListener來處理資料,我們可以把呼叫service的程式碼可以寫在其invoke方法內部;
WriteHandler 在每一個操作包括建立單元格、建立表格等都會呼叫WriteHandler來處理資料,對使用者透明不可見;
所有配置都是繼承的 Workbook的配置會被Sheet繼承。所以在用EasyExcel設定引數的時候,在EasyExcel…sheet()方法之前作用域是整個workBook的所有sheet,之後針對單個sheet。
2、讀取時的註解
@ExcelProperty
使用位置:標準作用在成員變數上,把實體類中屬性和 excel 表的列關聯起來
可選屬性:
屬性名 | 含義 | 說明 |
---|---|---|
index | 對應Excel表中的列數 | 預設-1,建議指定時從0開始 |
value | 對應Excel表中的列頭 | |
converter | 成員變數轉換器 | 自定義轉換器需要實Converter介面 |
使用效果:index屬性可以指定當前欄位對應excel中的哪一列,可以根據列名value去匹配,也可以不寫。
如果不使用@ExcelProperty註解,成員變數從上到下的順序,對應表格中從左到右的順序;
使用建議:要麼全部不寫,要麼全部用index,要麼全部用value去匹配,儘量不要三個混著用。
@ExcelIgnore
標註在成員變數上,預設所有欄位都會和excel去匹配,加了這個註解會忽略該欄位
@DateTimeFormat
標註在成員變數上,日期轉換,程式碼中用String型別的成員變數
去接收excel中日期格式的資料
會呼叫這個註解。裡面的value
參照java.text.SimpleDateFormat
@NumberFormat
標註在成員變數上,數字轉換,程式碼中用String型別的成員變數
去接收excel數字格式的資料
會呼叫這個註解。裡面的value
參照java.text.DecimalFormat
@ExcelIgnoreUnannotated
標註在類上。
不標註該註解時,預設類中所有成員變數都會參與讀寫,無論是否在成員變數上加了@ExcelProperty
的註解。
標註該註解後,類中的成員變數如果沒有標註@ExcelProperty
註解將不會參與讀寫。
3、 讀取時通用引數
ReadWorkbook
,ReadSheet
都會有的引數,如果為空,預設使用上級。
converter
轉換器,預設載入了很多轉換器。也可以自定義。readListener
監聽器,在讀取資料的過程中會不斷的呼叫監聽器。headRowNumber
指定需要讀表格的 列頭行數。預設有一行頭,也就是認為第二行開始起為資料。head
與clazz
二選一。讀取檔案頭對應的列表,會根據列表匹配資料。建議使用class,就是檔案中每一行資料對應的程式碼中的實體型別。clazz
與head
二選一。讀取檔案的頭對應的class,也可以使用註解。如果兩個都不指定,則會讀取全部資料。autoTrim
字串、表頭等資料自動trimpassword
讀的時候是否需要使用密碼
4、ReadWorkbook(工作簿物件)引數
excelType
當前excel的型別,讀取時會自動判斷,無需設定。inputStream
與file
二選一。建議使用file。file
與inputStream
二選一。讀取檔案的檔案。autoCloseStream
自動關閉流。readCache
預設小於5M用 記憶體,超過5M會使用EhCache
,不建議使用這個引數。useDefaultListener
@since 2.1.4
預設會加入ModelBuildEventListener
來幫忙轉換成傳入class
的物件,設定成false
後將不會協助轉換物件,自定義的監聽器會接收到Map<Integer,CellData>
物件,如果還想繼續接聽到class
物件,請呼叫readListener
方法,加入自定義的beforeListener
、ModelBuildEventListener
、 自定義的afterListener
即可。
5、ReadSheet(工作表物件)引數
sheetNo
需要讀取Sheet的編號,建議使用這個來指定讀取哪個SheetsheetName
根據名字去匹配Sheet,excel 2003不支援根據名字去匹配
6、寫入時的註解註解
@ExcelProperty
使用位置:標準作用在成員變數上
可選屬性:
屬性名 | 含義 | 說明 |
---|---|---|
index | 對應Excel表中的列數 | 預設-1,指定時建議從0開始 |
value | 對應Excel表中的列頭 | |
converter | 成員變數轉換器 | 自定義轉換器需要實Converter介面 |
使用效果:index
指定寫到第幾列,如果不指定則根據成員變數位置排序;
value
指定寫入的列頭,如果不指定則使用成員變數的名字作為列頭;
如果要設定複雜的頭,可以為value指定多個值。
其他註解:
基本和讀取時一致
@ContentRowHeight() 標註在類上或屬性上,指定內容行高
@HeadRowHeight() 標註在類上或屬性上,指定列頭行高
@ColumnWidth() 標註在類上或屬性上,指定列寬
ExcelIgnore` 預設所有欄位都會寫入excel,這個註解會忽略這個欄位
DateTimeFormat
日期轉換,將Date
寫到excel會呼叫這個註解。裡面的value
參照java.text.SimpleDateFormat
NumberFormat
數字轉換,用Number
寫excel會呼叫這個註解。裡面的value
參照java.text.DecimalFormat
ExcelIgnoreUnannotated
預設不加ExcelProperty
的註解的都會參與讀寫,加了不會參與
7、寫入時通用引數
WriteWorkbook
、WriteSheet
都會有的引數,如果為空,預設使用上級。
converter
轉換器,預設載入了很多轉換器。也可以自定義。writeHandler
寫的處理器。可以實現WorkbookWriteHandler
,SheetWriteHandler
,RowWriteHandler
,CellWriteHandler
,在寫入excel的不同階段會呼叫,對使用者透明不可見。relativeHeadRowIndex
距離多少行後開始。也就是開頭空幾行needHead
是否匯出頭head
與clazz
二選一。寫入檔案的頭列表,建議使用class。clazz
與head
二選一。寫入檔案的頭對應的class,也可以使用註解。autoTrim
字串、表頭等資料自動trim
8、WriteWorkbook(工作簿物件)引數
excelType
當前excel的型別,預設為xlsx
outputStream
與file
二選一。寫入檔案的流file
與outputStream
二選一。寫入的檔案templateInputStream
模板的檔案流templateFile
模板檔案autoCloseStream
自動關閉流。password
寫的時候是否需要使用密碼useDefaultStyle
寫的時候是否是使用預設頭
9、WriteSheet(工作表物件)引數
sheetNo
需要寫入的編號。預設0sheetName
需要些的Sheet名稱,預設同sheetNo
四、資料填充
4.1、填充一組資料
4.1.1、準備模板
Excel表格中用{} 來表示包裹要填充的變數,如果單元格文字中本來就有{
、}
左右大括號,需要在括號前面使用斜槓轉義\{
、\}
。
程式碼中被填充資料的實體物件的成員變數名或被填充map集合的key需要和Excel中被{}包裹的變數名稱一致。
4.1.2、封裝資料
編寫封裝填充資料的類或選用 Map
/**
* @ClassName FillData
* @Description 實體類封裝填充資料,實體類中成員變數名稱需要和Excel表各中{包裹的變數名稱匹配}
* @Author hudu
* @Date 2021/9/15 2021/9/15
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class FillData {
private String name;
private Integer age;
}
4.1.2、填充資料
/**
* 單組資料填充
*/
public static void fillData1() {
// 準備模板
String template = "fill_data_template1.xlsx";
// 建立一個工作簿物件
ExcelWriterBuilder excelWriterBuilder = EasyExcel.write("export/fill_data1.xlsx", FillData.class).withTemplate(template);
// 建立一個工作表物件
ExcelWriterSheetBuilder sheet = excelWriterBuilder.sheet();
// 準備資料
FillData fillData = initData();
// 或者使用map
HashMap<String, Object> map = new HashMap<>();
map.put("name","Alex");
map.put("age",22);
// 填充資料
sheet.doFill(fillData);
}
public static FillData initData() {
return new FillData("Alex",22);
}
效果如下
4.2、填充多組資料
4.2.1、準備模板
Excel表格中用{.}
來表示包裹要填充的變數,如果單元格文字中本來就有{
、}
左右大括號,需要在括號前面使用斜槓轉義\{
、\}
。
程式碼中被填充資料的實體物件的成員變數名或被填充map集合的key需要和Excel中被{}包裹的變數名稱一致。
4.2.2、開始填充
/**
* 多組資料填充
*/
public static void fillData2() {
List<FillData> fillData = initDataList();
String template = "fill_data_template2.xlsx";
ExcelWriterBuilder excelWriterBuilder = EasyExcel.write("export/fill_data2.xlsx", FillData.class).withTemplate(template);
ExcelWriterSheetBuilder sheet = excelWriterBuilder.sheet();
sheet.doFill(fillData);
}
public static List<FillData> initDataList() {
ArrayList<FillData> arrayList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
FillData fillData = new FillData("test" + i, i + 20);
arrayList.add(fillData);
}
return arrayList;
}
效果如下
4.3、組合填充
4.3.1、準備模板
既有多組資料填充,又有單一資料填充,為了避免兩者資料出現衝突覆蓋的情況,在多組填充時需要透過FillConfig
物件設定換行。
4.3.2、資料填充
public static List<FillData> initDataList() {
ArrayList<FillData> arrayList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
FillData fillData = new FillData("test" + i, i + 20);
arrayList.add(fillData);
}
return arrayList;
}
/**
* 組合填充
*/
public static void fillData3() {
// 準備模板
String template = "fill_data_template3.xlsx";
// 目標檔案
String target = "fill_data3.xlsx";
// 準備資料
List<FillData> fillData = initDataList();
// 生成工作簿物件
ExcelWriter excelWriter = EasyExcel.write(target).withTemplate(template).build();
// 生成工作表物件
WriteSheet writeSheet = EasyExcel.writerSheet().build();
// 組合填充時,因為多組填充的資料量不確定,需要在多組填充完之後另起一行
FillConfig fillConfig = FillConfig.builder().forceNewRow(true).build();
// 填充並換行
excelWriter.fill(fillData,fillConfig,writeSheet);
HashMap<String, String> map = new HashMap<>();
map.put("date","2021");
map.put("total","10");
excelWriter.fill(map,writeSheet);
// 需要手動進行關閉
excelWriter.finish();
}
效果如下
4.4、水平填充
4.4.1、準備模板
水平填充和多組填充模板一樣,不一樣的地方在於,填充時需要透過FillConfig
物件設定水平填充。
4.4.2、填充資料
/**
* 水平填充
*/
public static void fillData4() {
// 準備模板
String template = "fill_data_template4.xlsx";
// 目標檔案
String target = "export/fill_data4.xlsx";
// 準備資料
List<FillData> fillData = initDataList();
// 生成工作簿物件
ExcelWriter excelWriter = EasyExcel.write(target).withTemplate(template).build();
// 生成工作表物件
WriteSheet writeSheet = EasyExcel.writerSheet().build();
// 組合填充時,因為多組填充的資料量不確定,需要在多組填充完之後另起一行
FillConfig fillConfig = FillConfig.builder().direction(WriteDirectionEnum.HORIZONTAL).build();
// 填充並換行
excelWriter.fill(fillData,fillConfig,writeSheet);
// 需要手動進行關閉
excelWriter.finish();
}
效果如下
4.5、注意事項
為了節省記憶體,所以沒有采用把整個文件在記憶體中組織好之後再整體寫入到檔案的做法,而是採用的是一行一行寫入的方式,不能實現刪除和移動行,也不支援備註寫入。多組資料寫入的時候,如果需要新增行,只能在最後一行增加,不能在中間位置新增。
5、實際應用
5.1 無實體類匯出
在我們實際專案開發中,有時候會出現以下應用場景,需要匯出資料庫中的資料,但是不知道匯出哪張表,並且這些表沒有具體的實體類對應,所以無法透過實體列註解來對映欄位匯出,需要自己設定列
模擬生成的資料
@Component
public class ExcelDataInit {
/**
* 模擬資料庫欄位
*/
public List<String> initExcelHeader() {
List<String> headerList = new ArrayList<>();
for (int i = 1; i <= 16; i++) {
headerList.add("第"+i+"列");
}
return headerList;
}
/**
* 模擬無實體類查詢出的 mysql 中的資料
*/
public List<LinkedHashMap<String, Object>> initMysqlData() {
List<LinkedHashMap<String, Object>> maps = new ArrayList<>();
for (int i = 1; i <= 100; i++) {
LinkedHashMap<String, Object> map = new LinkedHashMap<>();
for (int i1 = 1; i1 <= 16; i1++) {
map.put("file"+i1,"第"+i1+"列"+i+"行");
}
maps.add(map);
}
return maps;
}
/**
* 將資料庫中查詢出來的資料進行轉換,注意使用 LinkedHashMap,不然可能會導致匯出的資料欄位對應不上
*/
public List<List<Object>> initExcelData() {
final List<LinkedHashMap<String, Object>> linkedHashMaps = initMysqlData();
final List<List<Object>> collect = new ArrayList<>();
for (LinkedHashMap<String, Object> linkedHashMap : linkedHashMaps) {
// final List<Object> objectList = linkedHashMap.entrySet().stream().map(Map.Entry::getValue).collect(Collectors.toList());
final List<Object> objectList = new ArrayList<>(linkedHashMap.values());
collect.add(objectList);
}
return collect;
}
}
控制層程式碼
@RestController
@RequestMapping("/excel")
public class ExcelController {
@Autowired
private ExcelService excelService;
@GetMapping("/export-excel")
public void exportExcel(HttpServletResponse response) {
excelService.exportExcel(response);
}
}
服務層
@Service
public class ExcelServiceImpl implements ExcelService {
@Autowired
private ExcelDataInit excelDataInit;
@Override
public void exportExcel(HttpServletResponse response) {
ExcelWriter excelWriter = null;
OutputStream outputStream = null;
try {
outputStream = response.getOutputStream();
response.setContentType("application/octet-stream");
response.setCharacterEncoding("utf-8");
response.setHeader("content-Disposition", "attachment;filename=" + URLEncoder.encode("test.xlsx", "UTF-8"));
List<List<Object>> hashMaps = excelDataInit.initExcelData();
List<String> list = excelDataInit.initExcelHeader();
List<List<String>> header = new ArrayList<>();
list.forEach(h -> header.add(Collections.singletonList(h)));
excelWriter = EasyExcel.write(outputStream).build();
// 設定 sheet 名稱
// 設定自適應列寬,註冊一個handler: LongestMatchColumnWidthStyleStrategy
WriteSheet writeSheet = EasyExcel.writerSheet("資料匯出").registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).build();
// 設定表頭
writeSheet.setHead(header);
// 設定寫入的資料
excelWriter.write(hashMaps,writeSheet);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (excelWriter != null) {
excelWriter.finish();
}
if (outputStream != null) {
try {
outputStream.flush();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
最終效果
本作品採用《CC 協議》,轉載必須註明作者和本文連結