SpringBoot?整合?EasyExcel?實(shí)現(xiàn)自由導(dǎo)入導(dǎo)出功能
在實(shí)際的業(yè)務(wù)系統(tǒng)開(kāi)發(fā)過(guò)程中,操作 Excel 實(shí)現(xiàn)數(shù)據(jù)的導(dǎo)入導(dǎo)出基本上是個(gè)非常常見(jiàn)的需求。
之前,我們有介紹一款非常好用的工具:EasyPoi,有讀者提出在數(shù)據(jù)量大的情況下,EasyPoi 會(huì)占用內(nèi)存大,性能不夠好,嚴(yán)重的時(shí)候,還會(huì)出現(xiàn)內(nèi)存異常的現(xiàn)象。
今天我給大家推薦一款性能更好的 Excel 導(dǎo)入導(dǎo)出工具:EasyExcel,希望對(duì)大家有所幫助!
一、簡(jiǎn)介
easyexcel 是阿里開(kāi)源的一款 Excel導(dǎo)入導(dǎo)出工具,具有處理速度快、占用內(nèi)存小、使用方便的特點(diǎn),底層邏輯也是基于 apache poi 進(jìn)行二次開(kāi)發(fā)的,目前的應(yīng)用也是非常廣!

相比 EasyPoi,EasyExcel 的處理數(shù)據(jù)性能非常高,讀取 75M (46W行25列) 的Excel,僅需使用 64M 內(nèi)存,耗時(shí) 20s,極速模式還可以更快!

廢話也不多說(shuō)了,下面直奔主題!
二、實(shí)踐
在 SpringBoot 項(xiàng)目中集成 EasyExcel 其實(shí)非常簡(jiǎn)單,僅需一個(gè)依賴(lài)即可。
<!--EasyExcel相關(guān)依賴(lài)-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.0.5</version>
</dependency>EasyExcel 的導(dǎo)出導(dǎo)入支持兩種方式進(jìn)行處理
- 第一種是通過(guò)實(shí)體類(lèi)注解方式來(lái)生成文件和反解析文件數(shù)據(jù)映射成對(duì)象
- 第二種是通過(guò)動(dòng)態(tài)參數(shù)化生成文件和反解析文件數(shù)據(jù)
下面我們以用戶信息的導(dǎo)出導(dǎo)入為例,分別介紹兩種處理方式。
簡(jiǎn)單導(dǎo)出
首先,我們只需要?jiǎng)?chuàng)建一個(gè)UserEntity用戶實(shí)體類(lèi),然后添加對(duì)應(yīng)的注解字段即可,示例代碼如下:
public class UserWriteEntity {
@ExcelProperty(value = "姓名")
private String name;
@ExcelProperty(value = "年齡")
private int age;
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
@ExcelProperty(value = "操作時(shí)間")
private Date time;
//set、get...
}然后,使用 EasyExcel 提供的EasyExcel工具類(lèi),即可實(shí)現(xiàn)文件的導(dǎo)出。
public static void main(String[] args) throws FileNotFoundException {
List<UserWriteEntity> dataList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
UserWriteEntity userEntity = new UserWriteEntity();
userEntity.setName("張三" + i);
userEntity.setAge(20 + i);
userEntity.setTime(new Date(System.currentTimeMillis() + i));
dataList.add(userEntity);
}
//定義文件輸出位置
FileOutputStream outputStream = new FileOutputStream(new File("/Users/panzhi/Documents/easyexcel-export-user1.xlsx"));
EasyExcel.write(outputStream, UserWriteEntity.class).sheet("用戶信息").doWrite(dataList);
}運(yùn)行程序,打開(kāi)文件內(nèi)容結(jié)果!

簡(jiǎn)單導(dǎo)入
這種簡(jiǎn)單固定表頭的 Excel 文件,如果想要讀取文件數(shù)據(jù),操作也很簡(jiǎn)單。
以上面的導(dǎo)出文件為例,使用 EasyExcel 提供的EasyExcel工具類(lèi),即可來(lái)實(shí)現(xiàn)文件內(nèi)容數(shù)據(jù)的快速讀取,示例代碼如下:
首先創(chuàng)建讀取實(shí)體類(lèi)
/**
* 讀取實(shí)體類(lèi)
*/
public class UserReadEntity {
@ExcelProperty(value = "姓名")
private String name;
/**
* 強(qiáng)制讀取第三個(gè) 這里不建議 index 和 name 同時(shí)用,要么一個(gè)對(duì)象只用index,要么一個(gè)對(duì)象只用name去匹配
*/
@ExcelProperty(index = 1)
private int age;
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
@ExcelProperty(value = "操作時(shí)間")
private Date time;
//set、get...
}然后讀取文件數(shù)據(jù),并封裝到對(duì)象里面
public static void main(String[] args) throws FileNotFoundException {
//同步讀取文件內(nèi)容
FileInputStream inputStream = new FileInputStream(new File("/Users/panzhi/Documents/easyexcel-user1.xls"));
List<UserReadEntity> list = EasyExcel.read(inputStream).head(UserReadEntity.class).sheet().doReadSync();
System.out.println(JSONArray.toJSONString(list));
}運(yùn)行程序,輸出結(jié)果如下:
[{"age":20,"name":"張三0","time":1616920360000},{"age":21,"name":"張三1","time":1616920360000},{"age":22,"name":"張三2","time":1616920360000},{"age":23,"name":"張三3","time":1616920360000},{"age":24,"name":"張三4","time":1616920360000},{"age":25,"name":"張三5","time":1616920360000},{"age":26,"name":"張三6","time":1616920360000},{"age":27,"name":"張三7","time":1616920360000},{"age":28,"name":"張三8","time":1616920360000},{"age":29,"name":"張三9","time":1616920360000}]
在實(shí)際使用開(kāi)發(fā)中,我們不可能每來(lái)一個(gè) excel 導(dǎo)入導(dǎo)出需求,就編寫(xiě)一個(gè)實(shí)體類(lèi),很多業(yè)務(wù)需求需要根據(jù)不同的字段來(lái)動(dòng)態(tài)導(dǎo)入導(dǎo)出,沒(méi)辦法基于實(shí)體類(lèi)注解的方式來(lái)讀取文件或者寫(xiě)入文件。
因此,基于EasyExcel提供的動(dòng)態(tài)參數(shù)化生成文件和動(dòng)態(tài)監(jiān)聽(tīng)器讀取文件方法,我們可以單獨(dú)封裝一套動(dòng)態(tài)導(dǎo)出導(dǎo)出工具類(lèi),省的我們每次都需要重新編寫(xiě)大量重復(fù)工作,以下就是小編我在實(shí)際使用過(guò)程,封裝出來(lái)的工具類(lèi),在此分享給大家!
首先,我們可以編寫(xiě)一個(gè)動(dòng)態(tài)導(dǎo)出工具類(lèi)
public class DynamicEasyExcelExportUtils {
private static final Logger log = LoggerFactory.getLogger(DynamicEasyExcelExportUtils.class);
private static final String DEFAULT_SHEET_NAME = "sheet1";
/**
* 動(dòng)態(tài)生成導(dǎo)出模版(單表頭)
* @param headColumns 列名稱(chēng)
* @return excel文件流
*/
public static byte[] exportTemplateExcelFile(List<String> headColumns){
List<List<String>> excelHead = Lists.newArrayList();
headColumns.forEach(columnName -> { excelHead.add(Lists.newArrayList(columnName)); });
byte[] stream = createExcelFile(excelHead, new ArrayList<>());
return stream;
}
/**
* 動(dòng)態(tài)生成模版(復(fù)雜表頭)
* @param excelHead 列名稱(chēng)
* @return
*/
public static byte[] exportTemplateExcelFileCustomHead(List<List<String>> excelHead){
byte[] stream = createExcelFile(excelHead, new ArrayList<>());
return stream;
}
/**
* 動(dòng)態(tài)導(dǎo)出文件(通過(guò)map方式計(jì)算)
* @param headColumnMap 有序列頭部
* @param dataList 數(shù)據(jù)體
* @return
*/
public static byte[] exportExcelFile(LinkedHashMap<String, String> headColumnMap, List<Map<String, Object>> dataList){
//獲取列名稱(chēng)
List<List<String>> excelHead = new ArrayList<>();
if(MapUtils.isNotEmpty(headColumnMap)){
//key為匹配符,value為列名,如果多級(jí)列名用逗號(hào)隔開(kāi)
headColumnMap.entrySet().forEach(entry -> {
excelHead.add(Lists.newArrayList(entry.getValue().split(",")));
});
}
List<List<Object>> excelRows = new ArrayList<>();
if(MapUtils.isNotEmpty(headColumnMap) && CollectionUtils.isNotEmpty(dataList)){
for (Map<String, Object> dataMap : dataList) {
List<Object> rows = new ArrayList<>();
headColumnMap.entrySet().forEach(headColumnEntry -> {
if(dataMap.containsKey(headColumnEntry.getKey())){
Object data = dataMap.get(headColumnEntry.getKey());
rows.add(data);
}
});
excelRows.add(rows);
}
}
byte[] stream = createExcelFile(excelHead, excelRows);
return stream;
}
/**
* 生成文件(自定義頭部排列)
* @param rowHeads
* @param excelRows
* @return
*/
public static byte[] customerExportExcelFile(List<List<String>> rowHeads, List<List<Object>> excelRows){
//將行頭部轉(zhuǎn)成easyexcel能識(shí)別的部分
List<List<String>> excelHead = transferHead(rowHeads);
return createExcelFile(excelHead, excelRows);
}
/**
* 生成文件
* @param excelHead
* @param excelRows
* @return
*/
private static byte[] createExcelFile(List<List<String>> excelHead, List<List<Object>> excelRows){
try {
if(CollectionUtils.isNotEmpty(excelHead)){
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
EasyExcel.write(outputStream).registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
.head(excelHead)
.sheet(DEFAULT_SHEET_NAME)
.doWrite(excelRows);
return outputStream.toByteArray();
}
} catch (Exception e) {
log.error("動(dòng)態(tài)生成excel文件失敗,headColumns:" + JSONArray.toJSONString(excelHead) + ",excelRows:" + JSONArray.toJSONString(excelRows), e);
}
return null;
}
/**
* 將行頭部轉(zhuǎn)成easyexcel能識(shí)別的部分
* @param rowHeads
* @return
*/
public static List<List<String>> transferHead(List<List<String>> rowHeads){
//將頭部列進(jìn)行反轉(zhuǎn)
List<List<String>> realHead = new ArrayList<>();
if(CollectionUtils.isNotEmpty(rowHeads)){
Map<Integer, List<String>> cellMap = new LinkedHashMap<>();
//遍歷行
for (List<String> cells : rowHeads) {
//遍歷列
for (int i = 0; i < cells.size(); i++) {
if(cellMap.containsKey(i)){
cellMap.get(i).add(cells.get(i));
} else {
cellMap.put(i, Lists.newArrayList(cells.get(i)));
}
}
}
//將列一行一行加入realHead
cellMap.entrySet().forEach(item -> realHead.add(item.getValue()));
}
return realHead;
}
/**
* 導(dǎo)出文件測(cè)試
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
//導(dǎo)出包含數(shù)據(jù)內(nèi)容的文件(方式一)
LinkedHashMap<String, String> headColumnMap = Maps.newLinkedHashMap();
headColumnMap.put("className","班級(jí)");
headColumnMap.put("name","學(xué)生信息,姓名");
headColumnMap.put("sex","學(xué)生信息,性別");
List<Map<String, Object>> dataList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Map<String, Object> dataMap = Maps.newHashMap();
dataMap.put("className", "一年級(jí)");
dataMap.put("name", "張三" + i);
dataMap.put("sex", "男");
dataList.add(dataMap);
}
byte[] stream1 = exportExcelFile(headColumnMap, dataList);
FileOutputStream outputStream1 = new FileOutputStream(new File("/Users/panzhi/Documents/easyexcel-export-user5.xlsx"));
outputStream1.write(stream1);
outputStream1.close();
//導(dǎo)出包含數(shù)據(jù)內(nèi)容的文件(方式二)
//頭部,第一層
List<String> head1 = new ArrayList<>();
head1.add("第一行頭部列1");
head1.add("第一行頭部列1");
head1.add("第一行頭部列1");
head1.add("第一行頭部列1");
//頭部,第二層
List<String> head2 = new ArrayList<>();
head2.add("第二行頭部列1");
head2.add("第二行頭部列1");
head2.add("第二行頭部列2");
head2.add("第二行頭部列2");
//頭部,第三層
List<String> head3 = new ArrayList<>();
head3.add("第三行頭部列1");
head3.add("第三行頭部列2");
head3.add("第三行頭部列3");
head3.add("第三行頭部列4");
//封裝頭部
List<List<String>> allHead = new ArrayList<>();
allHead.add(head1);
allHead.add(head2);
allHead.add(head3);
//封裝數(shù)據(jù)體
//第一行數(shù)據(jù)
List<Object> data1 = Lists.newArrayList(1,1,1,1);
//第二行數(shù)據(jù)
List<Object> data2 = Lists.newArrayList(2,2,2,2);
List<List<Object>> allData = Lists.newArrayList(data1, data2);
byte[] stream2 = customerExportExcelFile(allHead, allData);
FileOutputStream outputStream2 = new FileOutputStream(new File("/Users/panzhi/Documents/easyexcel-export-user6.xlsx"));
outputStream2.write(stream2);
outputStream2.close();
}
}- 然后,編寫(xiě)一個(gè)動(dòng)態(tài)導(dǎo)入工具類(lèi)
???????
/**
* 創(chuàng)建一個(gè)文件讀取監(jiān)聽(tīng)器
*/
public class DynamicEasyExcelListener extends AnalysisEventListener<Map<Integer, String>> {
private static final Logger LOGGER = LoggerFactory.getLogger(UserDataListener.class);
/**
* 表頭數(shù)據(jù)(存儲(chǔ)所有的表頭數(shù)據(jù))
*/
private List<Map<Integer, String>> headList = new ArrayList<>();
/**
* 數(shù)據(jù)體
*/
private List<Map<Integer, String>> dataList = new ArrayList<>();
/**
* 這里會(huì)一行行的返回頭
*
* @param headMap
* @param context
*/
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
LOGGER.info("解析到一條頭數(shù)據(jù):{}", JSON.toJSONString(headMap));
//存儲(chǔ)全部表頭數(shù)據(jù)
headList.add(headMap);
}
/**
* 這個(gè)每一條數(shù)據(jù)解析都會(huì)來(lái)調(diào)用
*
* @param data
* one row value. Is is same as {@link AnalysisContext#readRowHolder()}
* @param context
*/
@Override
public void invoke(Map<Integer, String> data, AnalysisContext context) {
LOGGER.info("解析到一條數(shù)據(jù):{}", JSON.toJSONString(data));
dataList.add(data);
}
/**
* 所有數(shù)據(jù)解析完成了 都會(huì)來(lái)調(diào)用
*
* @param context
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 這里也要保存數(shù)據(jù),確保最后遺留的數(shù)據(jù)也存儲(chǔ)到數(shù)據(jù)庫(kù)
LOGGER.info("所有數(shù)據(jù)解析完成!");
}
public List<Map<Integer, String>> getHeadList() {
return headList;
}
public List<Map<Integer, String>> getDataList() {
return dataList;
}
}???????
???????
- ???????動(dòng)態(tài)導(dǎo)入工具類(lèi)
/**
* 編寫(xiě)導(dǎo)入工具類(lèi)
*/
public class DynamicEasyExcelImportUtils {
/**
* 動(dòng)態(tài)獲取全部列和數(shù)據(jù)體,默認(rèn)從第一行開(kāi)始解析數(shù)據(jù)
* @param stream
* @return
*/
public static List<Map<String,String>> parseExcelToView(byte[] stream) {
return parseExcelToView(stream, 1);
}
/**
* 動(dòng)態(tài)獲取全部列和數(shù)據(jù)體
* @param stream excel文件流
* @param parseRowNumber 指定讀取行
* @return
*/
public static List<Map<String,String>> parseExcelToView(byte[] stream, Integer parseRowNumber) {
DynamicEasyExcelListener readListener = new DynamicEasyExcelListener();
EasyExcelFactory.read(new ByteArrayInputStream(stream)).registerReadListener(readListener).headRowNumber(parseRowNumber).sheet(0).doRead();
List<Map<Integer, String>> headList = readListener.getHeadList();
if(CollectionUtils.isEmpty(headList)){
throw new RuntimeException("Excel未包含表頭");
}
List<Map<Integer, String>> dataList = readListener.getDataList();
if(CollectionUtils.isEmpty(dataList)){
throw new RuntimeException("Excel未包含數(shù)據(jù)");
}
//獲取頭部,取最后一次解析的列頭數(shù)據(jù)
Map<Integer, String> excelHeadIdxNameMap = headList.get(headList.size() -1);
//封裝數(shù)據(jù)體
List<Map<String,String>> excelDataList = Lists.newArrayList();
for (Map<Integer, String> dataRow : dataList) {
Map<String,String> rowData = new LinkedHashMap<>();
excelHeadIdxNameMap.entrySet().forEach(columnHead -> {
rowData.put(columnHead.getValue(), dataRow.get(columnHead.getKey()));
});
excelDataList.add(rowData);
}
return excelDataList;
}
/**
* 文件導(dǎo)入測(cè)試
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
FileInputStream inputStream = new FileInputStream(new File("/Users/panzhi/Documents/easyexcel-export-user5.xlsx"));
byte[] stream = IoUtils.toByteArray(inputStream);
List<Map<String,String>> dataList = parseExcelToView(stream, 2);
System.out.println(JSONArray.toJSONString(dataList));
inputStream.close();
}
}為了方便后續(xù)的操作流程,在解析數(shù)據(jù)的時(shí)候,會(huì)將列名作為key!
三、小結(jié)
在實(shí)際的業(yè)務(wù)開(kāi)發(fā)過(guò)程中,根據(jù)參數(shù)動(dòng)態(tài)實(shí)現(xiàn) Excel 的導(dǎo)出導(dǎo)入還是非常廣的。
當(dāng)然,EasyExcel 的功能還不只上面介紹的那些內(nèi)容,還有基于模版進(jìn)行 excel的填充,web 端 restful 的導(dǎo)出導(dǎo)出,使用方法大致都差不多,更多的功能,會(huì)在后續(xù)的文章中再次介紹,如果有描述不對(duì)的地方,歡迎網(wǎng)友批評(píng)吐槽!
想要獲取項(xiàng)目源代碼的小伙伴,可以點(diǎn)擊:easyexcel,即可獲取取項(xiàng)目的源代碼。
四、參考
到此這篇關(guān)于SpringBoot 整合 EasyExcel 實(shí)現(xiàn)自由導(dǎo)入導(dǎo)出,太強(qiáng)了的文章就介紹到這了,更多相關(guān)SpringBoot 整合 EasyExcel導(dǎo)入導(dǎo)出內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot中EasyExcel實(shí)現(xiàn)Excel文件的導(dǎo)入導(dǎo)出
- SpringBoot 導(dǎo)出數(shù)據(jù)生成excel文件返回方式
- SpringBoot整合EasyExcel實(shí)現(xiàn)批量導(dǎo)入導(dǎo)出
- springboot實(shí)現(xiàn)excel表格導(dǎo)出幾種常見(jiàn)方法
- SpringBoot整合EasyExcel實(shí)現(xiàn)文件導(dǎo)入導(dǎo)出
- 使用VUE+SpringBoot+EasyExcel?整合導(dǎo)入導(dǎo)出數(shù)據(jù)的教程詳解
- SpringBoot+EasyPoi實(shí)現(xiàn)excel導(dǎo)出功能
- SpringBoot導(dǎo)出Excel的四種實(shí)現(xiàn)方式
- springboot實(shí)現(xiàn)對(duì)接poi 導(dǎo)出excel折線圖
相關(guān)文章
Java中jakarta.validation數(shù)據(jù)校驗(yàn)幾個(gè)主要依賴(lài)包講解
在Java開(kāi)發(fā)中,BeanValidationAPI提供了一套標(biāo)準(zhǔn)的數(shù)據(jù)驗(yàn)證機(jī)制,尤其是通過(guò)JakartaBeanValidation(原HibernateValidator)實(shí)現(xiàn),文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-09-09
Java8 將List轉(zhuǎn)換為用逗號(hào)隔開(kāi)的字符串的多種方法
這篇文章主要介紹了Java8 將List轉(zhuǎn)換為用逗號(hào)隔開(kāi)的字符串的幾種方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03
java中關(guān)于移位運(yùn)算符的demo與總結(jié)(推薦)
下面小編就為大家?guī)?lái)一篇java中關(guān)于移位運(yùn)算符的demo與總結(jié)(推薦)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-05-05
使用IDEA創(chuàng)建SpringBoot項(xiàng)目的方法步驟
這篇文章主要介紹了使用IDEA創(chuàng)建SpringBoot項(xiàng)目的方法步驟,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-05-05
SpringBoot集成Swagger3的實(shí)現(xiàn)
本文主要介紹了SpringBoot集成Swagger3的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-12-12
Springboot項(xiàng)目與vue項(xiàng)目整合打包的實(shí)現(xiàn)方式
這篇文章主要介紹了Springboot項(xiàng)目與vue項(xiàng)目整合打包的實(shí)現(xiàn)方式,本文通過(guò)兩種方式給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-07-07
SpringBoot監(jiān)控SQL運(yùn)行情況的流程步驟
Druid是Java語(yǔ)言中最好的數(shù)據(jù)庫(kù)連接池,雖然?HikariCP?的速度稍快,但是,Druid能夠提供強(qiáng)大的監(jiān)控和擴(kuò)展功能?,也是阿里巴巴的開(kāi)源項(xiàng)目,本文給大家介紹了SpringBoot監(jiān)控SQL運(yùn)行情況的流程步驟,需要的朋友可以參考下2024-03-03
Java?使用JDBC操作PI數(shù)據(jù)庫(kù)的方法
文章介紹在Windows系統(tǒng)下使用JDBC驅(qū)動(dòng)訪問(wèn)PI數(shù)據(jù)庫(kù)的方法,涵蓋環(huán)境要求(JRE8+)、安裝步驟(OLEDB、SQLDAS、JDBC驅(qū)動(dòng))、連接原理及SpringBoot集成方式,感興趣的朋友跟隨小編一起看看吧2025-08-08

