Spring boot實(shí)現(xiàn)低代碼量的Excel導(dǎo)入導(dǎo)出

Spring boot實(shí)現(xiàn)低代碼量的Excel導(dǎo)入導(dǎo)出

[TOC]

2024年4月12日

Java的web開發(fā)需要excel的導(dǎo)入導(dǎo)出工具,所以需要一定的工具類實(shí)現(xiàn),如果是使用easypoi、Hutool導(dǎo)入導(dǎo)出excel,會(huì)非常的損耗內(nèi)存,因此可以嘗試使用easyexcel解決大數(shù)據(jù)量的數(shù)據(jù)的導(dǎo)入導(dǎo)出,且可以通過(guò)Java8的函數(shù)式編程解決該問(wèn)題。

使用easyexcel,雖然不太會(huì)出現(xiàn)OOM的問(wèn)題,但是如果是大數(shù)據(jù)量的情況下也會(huì)有一定量的內(nèi)存溢出的風(fēng)險(xiǎn),所以我打算從以下幾個(gè)方面優(yōu)化這個(gè)問(wèn)題:

使用Java8的函數(shù)式編程實(shí)現(xiàn)低代碼量的數(shù)據(jù)導(dǎo)入

使用反射等特性實(shí)現(xiàn)單個(gè)接口導(dǎo)入任意excel

使用線程池實(shí)現(xiàn)大數(shù)據(jù)量的excel導(dǎo)入

通過(guò)泛型實(shí)現(xiàn)數(shù)據(jù)導(dǎo)出

maven導(dǎo)入

<!--EasyExcel相關(guān)依賴-->

<dependency>

<groupId>com.alibaba</groupId>

<artifactId>easyexcel</artifactId>

<version>3.0.5</version>

</dependency>

使用泛型實(shí)現(xiàn)對(duì)象的單個(gè)Sheet導(dǎo)入

先實(shí)現(xiàn)一個(gè)類,用來(lái)指代導(dǎo)入的特定的對(duì)象

@Data

@NoArgsConstructor

@AllArgsConstructor

@TableName("stu_info")

@ApiModel("學(xué)生信息")

//@ExcelIgnoreUnannotated 沒(méi)有注解的字段都不轉(zhuǎn)換

publicclassStuInfo{

privatestaticfinallongserialVersionUID=1L;

/**

* 姓名

*/

// 設(shè)置字體,此處代表使用斜體

// ?? @ContentFontStyle(italic = BooleanEnum.TRUE)

// 設(shè)置列寬度的注解,注解中只有一個(gè)參數(shù)value,value的單位是字符長(zhǎng)度,最大可以設(shè)置255個(gè)字符

@ColumnWidth(10)

// @ExcelProperty 注解中有三個(gè)參數(shù)value,index,converter分別代表表名,列序號(hào),數(shù)據(jù)轉(zhuǎn)換方式

@ApiModelProperty("姓名")

@ExcelProperty(value="姓名",order=0)

@ExportHeader(value="姓名",index=1)

privateStringname;

/**

* 年齡

*/

// ?? @ExcelIgnore不將該字段轉(zhuǎn)換成Excel

@ExcelProperty(value="年齡",order=1)

@ApiModelProperty("年齡")

@ExportHeader(value="年齡",index=2)

privateIntegerage;

/**

* 身高

*/

//自定義格式-位數(shù)

// ?? @NumberFormat("#.##%")

@ExcelProperty(value="身高",order=2)

@ApiModelProperty("身高")

@ExportHeader(value="身高",index=4)

privateDoubletall;

/**

* 自我介紹

*/

@ExcelProperty(value="自我介紹",order=3)

@ApiModelProperty("自我介紹")

@ExportHeader(value="自我介紹",index=3,ignore=true)

privateStringselfIntroduce;

/**

* 圖片信息

*/

@ExcelProperty(value="圖片信息",order=4)

@ApiModelProperty("圖片信息")

@ExportHeader(value="圖片信息",ignore=true)

privateBlobpicture;

/**

* 性別

*/

@ExcelProperty(value="性別",order=5)

@ApiModelProperty("性別")

privateIntegergender;

/**

* 入學(xué)時(shí)間

*/

//自定義格式-時(shí)間格式

@DateTimeFormat("yyyy-MM-dd HH:mm:ss:")

@ExcelProperty(value="入學(xué)時(shí)間",order=6)

@ApiModelProperty("入學(xué)時(shí)間")

privateStringintake;

/**

* 出生日期

*/

@ExcelProperty(value="出生日期",order=7)

@ApiModelProperty("出生日期")

privateStringbirthday;

}

重寫ReadListener接口

@Slf4j

publicclassUploadDataListener<T>implementsReadListener<T>{

/**

* 每隔5條存儲(chǔ)數(shù)據(jù)庫(kù),實(shí)際使用中可以100條,然后清理list ,方便內(nèi)存回收

*/

privatestaticfinalintBATCH_COUNT=100;

/**

* 緩存的數(shù)據(jù)

*/

privateList<T>cachedDataList=ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);

/**

* Predicate用于過(guò)濾數(shù)據(jù)

*/

privatePredicate<T>predicate;

/**

* 調(diào)用持久層批量保存

*/

privateConsumer<Collection<T>>consumer;

publicUploadDataListener(Predicate<T>predicate,Consumer<Collection<T>>consumer) {

this.predicate=predicate;

this.consumer=consumer;

?? }

publicUploadDataListener(Consumer<Collection<T>>consumer) {

this.consumer=consumer;

?? }

/**

* 如果使用了spring,請(qǐng)使用這個(gè)構(gòu)造方法。每次創(chuàng)建Listener的時(shí)候需要把spring管理的類傳進(jìn)來(lái)

*

* @param demoDAO

*/

/**

* 這個(gè)每一條數(shù)據(jù)解析都會(huì)來(lái)調(diào)用

*

* @param data ?? one row value. Is is same as {@link AnalysisContext#readRowHolder()}

* @param context

*/

@Override

publicvoidinvoke(Tdata,AnalysisContextcontext) {

if(predicate!=null&&!predicate.test(data)) {

return;

? ? ?? }

cachedDataList.add(data);

// 達(dá)到BATCH_COUNT了,需要去存儲(chǔ)一次數(shù)據(jù)庫(kù),防止數(shù)據(jù)幾萬(wàn)條數(shù)據(jù)在內(nèi)存,容易OOM

if(cachedDataList.size()>=BATCH_COUNT) {

try{

// 執(zhí)行具體消費(fèi)邏輯

consumer.accept(cachedDataList);

}catch(Exceptione) {

log.error("Failed to upload data!data={}",cachedDataList);

thrownewBizException("導(dǎo)入失敗");

? ? ? ? ?? }

// 存儲(chǔ)完成清理 list

cachedDataList=ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);

? ? ?? }

?? }

/**

* 所有數(shù)據(jù)解析完成了 都會(huì)來(lái)調(diào)用

*

* @param context

*/

@Override

publicvoiddoAfterAllAnalysed(AnalysisContextcontext) {

// 這里也要保存數(shù)據(jù),確保最后遺留的數(shù)據(jù)也存儲(chǔ)到數(shù)據(jù)庫(kù)

if(CollUtil.isNotEmpty(cachedDataList)) {

try{

// 執(zhí)行具體消費(fèi)邏輯

consumer.accept(cachedDataList);

log.info("所有數(shù)據(jù)解析完成!");

}catch(Exceptione) {

log.error("Failed to upload data!data={}",cachedDataList);

// 拋出自定義的提示信息

if(einstanceofBizException) {

throwe;

? ? ? ? ? ? ?? }

thrownewBizException("導(dǎo)入失敗");

? ? ? ? ?? }

? ? ?? }

?? }

}

Controller層的實(shí)現(xiàn)

@ApiOperation("只需要一個(gè)readListener,解決全部的問(wèn)題")

@PostMapping("/update")

@ResponseBody

publicR<String>aListener4AllExcel(MultipartFilefile)throwsIOException{

try{

EasyExcel.read(file.getInputStream(),

StuInfo.class,

newUploadDataListener<StuInfo>(

list->{

// 校驗(yàn)數(shù)據(jù)

// ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?? ValidationUtils.validate(list);

// dao 保存···

//最好是手寫一個(gè),不要使用mybatis-plus的一條條新增的邏輯

service.saveBatch(list);

log.info("從Excel導(dǎo)入數(shù)據(jù)一共 {} 行 ",list.size());

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?? }))

.sheet()

.doRead();

}catch(IOExceptione) {

log.error("導(dǎo)入失敗",e);

thrownewBizException("導(dǎo)入失敗");

? ? ?? }

returnR.success("SUCCESS");

?? }

但是這種方式只能實(shí)現(xiàn)已存對(duì)象的功能實(shí)現(xiàn),如果要新增一種數(shù)據(jù)的導(dǎo)入,那我們需要怎么做呢?

可以通過(guò)讀取成Map,根據(jù)順序?qū)氲綌?shù)據(jù)庫(kù)中。

通過(guò)實(shí)現(xiàn)單個(gè)Sheet中任意一種數(shù)據(jù)的導(dǎo)入

Controller層的實(shí)現(xiàn)

@ApiOperation("只需要一個(gè)readListener,解決全部的問(wèn)題")

@PostMapping("/listenMapDara")

@ResponseBody

publicR<String>listenMapDara(@ApiParam(value="表編碼",required=true)

@NotBlank(message="表編碼不能為空")

@RequestParam("tableCode")StringtableCode,

@ApiParam(value="上傳的文件",required=true)

@NotNull(message="上傳文件不能為空")MultipartFilefile)throwsIOException{

try{

//根據(jù)tableCode獲取這張表的字段,可以作為insert與劇中的信息

EasyExcel.read(file.getInputStream(),

newNonClazzOrientedListener(

list->{

// 校驗(yàn)數(shù)據(jù)

// ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?? ValidationUtils.validate(list);

// dao 保存···

log.info("從Excel導(dǎo)入數(shù)據(jù)一共 {} 行 ",list.size());

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?? }))

.sheet()

.doRead();

}catch(IOExceptione) {

log.error("導(dǎo)入失敗",e);

thrownewBizException("導(dǎo)入失敗");

? ? ?? }

returnR.success("SUCCESS");

?? }

重寫ReadListener接口

@Slf4j

publicclassNonClazzOrientedListenerimplementsReadListener<Map<Integer,String>>{

/**

* 每隔5條存儲(chǔ)數(shù)據(jù)庫(kù),實(shí)際使用中可以100條,然后清理list ,方便內(nèi)存回收

*/

privatestaticfinalintBATCH_COUNT=100;

privateList<List<Object>>rowsList=ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);

privateList<Object>rowList=newArrayList<>();

/**

* Predicate用于過(guò)濾數(shù)據(jù)

*/

privatePredicate<Map<Integer,String>>predicate;

/**

* 調(diào)用持久層批量保存

*/

privateConsumer<List>consumer;

publicNonClazzOrientedListener(Predicate<Map<Integer,String>>predicate,Consumer<List>consumer) {

this.predicate=predicate;

this.consumer=consumer;

?? }

publicNonClazzOrientedListener(Consumer<List>consumer) {

this.consumer=consumer;

?? }

/**

* 添加deviceName標(biāo)識(shí)

*/

privatebooleanflag=false;

@Override

publicvoidinvoke(Map<Integer,String>row,AnalysisContextanalysisContext) {

consumer.accept(rowsList);

rowList.clear();

row.forEach((k,v)->{

log.debug("key is {},value is {}",k,v);

rowList.add(v==null?"":v);

? ? ?? });

rowsList.add(rowList);

if(rowsList.size()>BATCH_COUNT) {

log.debug("執(zhí)行存儲(chǔ)程序");

log.info("rowsList is {}",rowsList);

rowsList.clear();

? ? ?? }

?? }

@Override

publicvoiddoAfterAllAnalysed(AnalysisContextanalysisContext) {

consumer.accept(rowsList);

if(CollUtil.isNotEmpty(rowsList)) {

try{

log.debug("執(zhí)行最后的程序");

log.info("rowsList is {}",rowsList);

}catch(Exceptione) {

log.error("Failed to upload data!data={}",rowsList);

// 拋出自定義的提示信息

if(einstanceofBizException) {

throwe;

? ? ? ? ? ? ?? }

thrownewBizException("導(dǎo)入失敗");

}finally{

rowsList.clear();

? ? ? ? ?? }

? ? ?? }

?? }

這種方式可以通過(guò)把表中的字段順序存儲(chǔ)起來(lái),通過(guò)配置數(shù)據(jù)和字段的位置實(shí)現(xiàn)數(shù)據(jù)的新增,那么如果出現(xiàn)了導(dǎo)出數(shù)據(jù)模板/手寫excel的時(shí)候順序和導(dǎo)入的時(shí)候順序不一樣怎么辦?

可以通過(guò)讀取header進(jìn)行實(shí)現(xiàn),通過(guò)表頭讀取到的字段,和數(shù)據(jù)庫(kù)中表的字段進(jìn)行比對(duì),只取其中存在的數(shù)據(jù)進(jìn)行排序添加

/**

* 這里會(huì)一行行的返回頭

*

* @param headMap

* @param context

*/

@Override

publicvoidinvokeHead(Map<Integer,ReadCellData<?>>headMap,AnalysisContextcontext) {

//該方法必然會(huì)在讀取數(shù)據(jù)之前進(jìn)行

Map<Integer,String>columMap=ConverterUtils.convertToStringMap(headMap,context);

//通過(guò)數(shù)據(jù)交互拿到這個(gè)表的表頭

// ? ? ?? Map<String,String> columnList=dao.xxxx();

Map<String,String>columnList=newHashMap();

columMap.forEach((key,value)->{

if(columnList.containsKey(value)) {

filterList.add(key);

? ? ? ? ?? }

? ? ?? });

//過(guò)濾到了只存在表里面的數(shù)據(jù),順序就不用擔(dān)心了,可以直接把filterList的數(shù)據(jù)用于排序,可以根據(jù)mybatis做一個(gè)動(dòng)態(tài)sql進(jìn)行應(yīng)用

log.info("解析到一條頭數(shù)據(jù):{}",JSON.toJSONString(columMap));

// 如果想轉(zhuǎn)成成 Map<Integer,String>

// 方案1: 不要implements ReadListener 而是 extends AnalysisEventListener

// 方案2: 調(diào)用 ConverterUtils.convertToStringMap(headMap, context) 自動(dòng)會(huì)轉(zhuǎn)換

?? }

那么這些問(wèn)題都解決了,如果出現(xiàn)大數(shù)據(jù)量的情況,如果要極大的使用到cpu,該怎么做呢?

可以嘗試使用線程池進(jìn)行實(shí)現(xiàn)

使用線程池進(jìn)行多線程導(dǎo)入大量數(shù)據(jù)

Java中線程池的開發(fā)與使用與原理我可以單獨(dú)寫一篇文章進(jìn)行講解,但是在這邊為了進(jìn)行好的開發(fā)我先給出一套固定一點(diǎn)的方法。

由于ReadListener不能被注冊(cè)到IOC容器里面,所以需要在外面開啟

詳情可見Spring Boot通過(guò)EasyExcel異步多線程實(shí)現(xiàn)大數(shù)據(jù)量Excel導(dǎo)入,百萬(wàn)數(shù)據(jù)30秒

通過(guò)泛型實(shí)現(xiàn)對(duì)象類型的導(dǎo)出

public<T>voidcommonExport(StringfileName,List<T>data,Class<T>clazz,HttpServletResponseresponse)throwsIOException{

if(CollectionUtil.isEmpty(data)) {

data=newArrayList<>();

? ? ?? }

//設(shè)置標(biāo)題

fileName=URLEncoder.encode(fileName,"UTF-8");

response.setContentType("application/vnd.ms-excel");

response.setCharacterEncoding("utf-8");

response.setHeader("Content-disposition","attachment;filename="+fileName+".xlsx");

EasyExcel.write(response.getOutputStream()).head(clazz).sheet("sheet1").doWrite(data);

?? }

直接使用該方法可以作為公共的數(shù)據(jù)的導(dǎo)出接口

如果想要?jiǎng)討B(tài)的下載任意一組數(shù)據(jù)怎么辦呢?可以使用這個(gè)方法

publicvoidexportFreely(StringfileName,List<List<Object>>data,List<List<String>>head,HttpServletResponseresponse)throwsIOException{

if(CollectionUtil.isEmpty(data)) {

data=newArrayList<>();

? ? ?? }

//設(shè)置標(biāo)題

fileName=URLEncoder.encode(fileName,"UTF-8");

response.setContentType("application/vnd.ms-excel");

response.setCharacterEncoding("utf-8");

response.setHeader("Content-disposition","attachment;filename="+fileName+".xlsx");

EasyExcel.write(response.getOutputStream()).head(head).sheet("sheet1").doWrite(data);

?? }

什么?不僅想一個(gè)接口展示全部的數(shù)據(jù)與信息,還要增加篩選條件?這個(gè)后期我可以單獨(dú)寫一篇文章解決這個(gè)問(wèn)題。

今天的分享就到這里了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容