Spring Boot 一個接口實現任意表的 Excel 導入導出

Java的web開發需要excel的導入導出工具,所以需要一定的工具類實現,如果是使用easypoi、Hutool導入導出excel,會非常的損耗內存,因此可以嘗試使用easyexcel解決大數據量的數據的導入導出,且可以通過Java8的函數式編程解決該問題。

使用easyexcel,雖然不太會出現OOM的問題,但是如果是大數據量的情況下也會有一定量的內存溢出的風險,所以我打算從以下幾個方面優化這個問題:

  • 使用Java8的函數式編程實現低代碼量的數據導入

  • 使用反射等特性實現單個接口導入任意excel

  • 使用線程池實現大數據量的excel導入

  • 通過泛型實現數據導出

maven導入

<!--EasyExcel相關依賴-->
<dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.0.5</version>
</dependency>

使用泛型實現對象的單個Sheet導入

先實現一個類,用來指代導入的特定的對象

@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("stu_info")
@ApiModel("學生信息")
//@ExcelIgnoreUnannotated 沒有注解的字段都不轉換
publicclass?StuInfo?{privatestaticfinallong?serialVersionUID =?1L;/*** 姓名*/// 設置字體,此處代表使用斜體
// ? ?@ContentFontStyle(italic = BooleanEnum.TRUE)// 設置列寬度的注解,注解中只有一個參數value,value的單位是字符長度,最大可以設置255個字符@ColumnWidth(10)// @ExcelProperty 注解中有三個參數value,index,converter分別代表表名,列序號,數據轉換方式@ApiModelProperty("姓名")@ExcelProperty(value =?"姓名",order =?0)@ExportHeader(value =?"姓名",index =?1)private?String name;/*** 年齡*/
// ? ?@ExcelIgnore不將該字段轉換成Excel@ExcelProperty(value =?"年齡",order =?1)@ApiModelProperty("年齡")@ExportHeader(value =?"年齡",index =?2)private?Integer age;/*** 身高*///自定義格式-位數
// ? ?@NumberFormat("#.##%")@ExcelProperty(value =?"身高",order =?2)@ApiModelProperty("身高")@ExportHeader(value =?"身高",index =?4)private?Double tall;/*** 自我介紹*/@ExcelProperty(value =?"自我介紹",order =?3)@ApiModelProperty("自我介紹")@ExportHeader(value =?"自我介紹",index =?3,ignore =?true)private?String selfIntroduce;/*** 圖片信息*/@ExcelProperty(value =?"圖片信息",order =?4)@ApiModelProperty("圖片信息")@ExportHeader(value =?"圖片信息",ignore =?true)private?Blob picture;/*** 性別*/@ExcelProperty(value =?"性別",order =?5)@ApiModelProperty("性別")private?Integer gender;/*** 入學時間*///自定義格式-時間格式@DateTimeFormat("yyyy-MM-dd HH:mm:ss:")@ExcelProperty(value =?"入學時間",order =?6)@ApiModelProperty("入學時間")private?String intake;/*** 出生日期*/@ExcelProperty(value =?"出生日期",order =?7)@ApiModelProperty("出生日期")private?String birthday;}

重寫ReadListener接口

@Slf4j
publicclass?UploadDataListener<T>?implements?ReadListener<T>?{/*** 每隔5條存儲數據庫,實際使用中可以100條,然后清理list ,方便內存回收*/privatestaticfinalint?BATCH_COUNT =?100;/*** 緩存的數據*/private?List<T> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);/*** Predicate用于過濾數據*/private?Predicate<T> predicate;/*** 調用持久層批量保存*/private?Consumer<Collection<T>> consumer;public?UploadDataListener(Predicate<T> predicate, Consumer<Collection<T>> consumer)?{this.predicate = predicate;this.consumer = consumer;}public?UploadDataListener(Consumer<Collection<T>> consumer)?{this.consumer = consumer;}/*** 如果使用了spring,請使用這個構造方法。每次創建Listener的時候需要把spring管理的類傳進來**?@param?demoDAO*//*** 這個每一條數據解析都會來調用**?@param?data ? ?one row value. Is is same as {@link?AnalysisContext#readRowHolder()}*?@param?context*/@Overridepublic?void?invoke(T data, AnalysisContext context)?{if?(predicate !=?null?&& !predicate.test(data)) {return;}cachedDataList.add(data);// 達到BATCH_COUNT了,需要去存儲一次數據庫,防止數據幾萬條數據在內存,容易OOMif?(cachedDataList.size() >= BATCH_COUNT) {try?{// 執行具體消費邏輯consumer.accept(cachedDataList);}?catch?(Exception e) {log.error("Failed to upload data!data={}", cachedDataList);thrownew?BizException("導入失敗");}// 存儲完成清理 listcachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);}}/*** 所有數據解析完成了 都會來調用**?@param?context*/@Overridepublic?void?doAfterAllAnalysed(AnalysisContext context)?{// 這里也要保存數據,確保最后遺留的數據也存儲到數據庫if?(CollUtil.isNotEmpty(cachedDataList)) {try?{// 執行具體消費邏輯consumer.accept(cachedDataList);log.info("所有數據解析完成!");}?catch?(Exception e) {log.error("Failed to upload data!data={}", cachedDataList);// 拋出自定義的提示信息if?(e?instanceof?BizException) {throw?e;}thrownew?BizException("導入失敗");}}}
}

Controller層的實現

@ApiOperation("只需要一個readListener,解決全部的問題")
@PostMapping("/update")
@ResponseBody
public?R<String>?aListener4AllExcel(MultipartFile file)?throws?IOException?{try?{EasyExcel.read(file.getInputStream(),StuInfo.class,new?UploadDataListener<StuInfo>(list?->?{// 校驗數據ValidationUtils.validate(list);// dao 保存···//最好是手寫一個,不要使用mybatis-plus的一條條新增的邏輯service.saveBatch(list);log.info("從Excel導入數據一共 {} 行 ", list.size());})).sheet().doRead();}?catch?(IOException e) {log.error("導入失敗", e);thrownew?BizException("導入失敗");}return?R.success("SUCCESS");
}

但是這種方式只能實現已存對象的功能實現,如果要新增一種數據的導入,那我們需要怎么做呢?關注公眾號:碼猿技術專欄,回復關鍵詞:1111 獲取阿里內部java性能調優手冊!

可以通過讀取成Map,根據順序導入到數據庫中。

通過實現單個Sheet中任意一種數據的導入

Controller層的實現

@ApiOperation("只需要一個readListener,解決全部的問題")
@PostMapping("/listenMapDara")
@ResponseBody
public?R<String>?listenMapDara(@ApiParam(value =?"表編碼", required =?true)@NotBlank(message =?"表編碼不能為空")@RequestParam("tableCode")?String tableCode,@ApiParam(value =?"上傳的文件", required =?true)@NotNull(message =?"上傳文件不能為空")?MultipartFile file)?throws?IOException?{try?{//根據tableCode獲取這張表的字段,可以作為insert與劇中的信息EasyExcel.read(file.getInputStream(),new?NonClazzOrientedListener(list -> {// 校驗數據
// ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?ValidationUtils.validate(list);// dao 保存···log.info("從Excel導入數據一共 {} 行 ", list.size());})).sheet().doRead();}?catch?(IOException e) {log.error("導入失敗", e);thrownew?BizException("導入失敗");}return?R.success("SUCCESS");
}

重寫ReadListener接口

@Slf4j
publicclass?NonClazzOrientedListener?implements?ReadListener<Map<Integer,?String>>?{/*** 每隔5條存儲數據庫,實際使用中可以100條,然后清理list ,方便內存回收*/privatestaticfinalint?BATCH_COUNT =?100;private?List<List<Object>> rowsList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);private?List<Object> rowList =?new?ArrayList<>();/*** Predicate用于過濾數據*/private?Predicate<Map<Integer, String>> predicate;/*** 調用持久層批量保存*/private?Consumer<List> consumer;public?NonClazzOrientedListener(Predicate<Map<Integer, String>> predicate, Consumer<List> consumer)?{this.predicate = predicate;this.consumer = consumer;}public?NonClazzOrientedListener(Consumer<List> consumer)?{this.consumer = consumer;}/*** 添加deviceName標識*/privateboolean?flag =?false;@Overridepublic?void?invoke(Map<Integer, String> row, AnalysisContext analysisContext)?{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("執行存儲程序");log.info("rowsList is {}", rowsList);rowsList.clear();}}@Overridepublic?void?doAfterAllAnalysed(AnalysisContext analysisContext)?{consumer.accept(rowsList);if?(CollUtil.isNotEmpty(rowsList)) {try?{log.debug("執行最后的程序");log.info("rowsList is {}", rowsList);}?catch?(Exception e) {log.error("Failed to upload data!data={}", rowsList);// 拋出自定義的提示信息if?(e?instanceof?BizException) {throw?e;}thrownew?BizException("導入失敗");}?finally?{rowsList.clear();}}}

這種方式可以通過把表中的字段順序存儲起來,通過配置數據和字段的位置實現數據的新增,那么如果出現了導出數據模板/手寫excel的時候順序和導入的時候順序不一樣怎么辦?

可以通過讀取header進行實現,通過表頭讀取到的字段,和數據庫中表的字段進行比對,只取其中存在的數據進行排序添加

/*** 這里會一行行的返回頭**?@param?headMap*?@param?context*/
@Override
public?void?invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context)?{//該方法必然會在讀取數據之前進行Map<Integer, String> columMap = ConverterUtils.convertToStringMap(headMap, context);//通過數據交互拿到這個表的表頭
// ? ? ? ?Map<String,String> columnList=dao.xxxx();Map<String, String> columnList =?new?HashMap();columMap.forEach((key, value) -> {if?(columnList.containsKey(value)) {filterList.add(key);}});//過濾到了只存在表里面的數據,順序就不用擔心了,可以直接把filterList的數據用于排序,可以根據mybatis做一個動態sql進行應用log.info("解析到一條頭數據:{}", JSON.toJSONString(columMap));// 如果想轉成成 Map<Integer,String>// 方案1: 不要implements ReadListener 而是 extends AnalysisEventListener// 方案2: 調用 ConverterUtils.convertToStringMap(headMap, context) 自動會轉換
}

那么這些問題都解決了,如果出現大數據量的情況,如果要極大的使用到cpu,該怎么做呢?

可以嘗試使用線程池進行實現

使用線程池進行多線程導入大量數據

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

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

詳情可見

https://juejin.cn/post/7251566038524133436

通過泛型實現對象類型的導出

public?<T>?void?commonExport(String fileName, List<T> data, Class<T> clazz, HttpServletResponse response)?throws?IOException?{if?(CollectionUtil.isEmpty(data)) {data =?new?ArrayList<>();}//設置標題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);
}

直接使用該方法可以作為公共的數據的導出接口

如果想要動態的下載任意一組數據怎么辦呢?可以使用這個方法

public?void?exportFreely(String fileName, List<List<Object>> data, List<List<String>> head, HttpServletResponse response)?throws?IOException?{if?(CollectionUtil.isEmpty(data)) {data =?new?ArrayList<>();}//設置標題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);}

什么?不僅想一個接口展示全部的數據與信息,還要增加篩選條件?這個后期可以單獨解決這個問題。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/74450.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/74450.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/74450.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

QT原子變量:QAtomicInteger、QAtomicPointer、QAtomicFlag

引言&#xff1a;原子變量為何重要&#xff1f; 在多線程編程中&#xff0c;共享數據的原子性訪問是保證線程安全的核心。傳統互斥鎖雖然有效&#xff0c;但會帶來性能損耗和死鎖風險。QT提供的原子類型&#xff08;QAtomicInteger、QAtomicPointer、QAtomicFlag&#xff09;通…

大模型金融企業場景落地應用

一、商業銀行體系 1. 江蘇銀行 企業背景&#xff1a;江蘇銀行是總部位于江蘇南京的全國性股份制商業銀行&#xff0c;在城商行中資產規模位居前列&#xff0c;積極擁抱金融科技&#xff0c;將數字化轉型作為核心戰略之一。近年來&#xff0c;江蘇銀行持續加大在人工智能、大數…

卡特蘭數在數據結構上面的運用

原理 Catalan數是一個數列&#xff0c;其第n項表示n個不同結點可以構成的二叉排序樹的數量。Catalan數的第n項公式為&#xff1a; &#xfffc; 其中&#xff0c;&#xfffc;是組合數&#xff0c;表示從2n個元素中選擇n個元素的組合數。 Catalan數的原理可以通過以下方式理解&…

影視后期工具學習之PR(中)

pr剪輯之旅----聲音設計 第五課 鏡頭語言和綠幕摳像 超級鍵效果(超級鍵通過簡單的吸管取色和參數調整,即可實現專業級摳像與合成效果。無論是綠幕替換背景,還是創意雙重曝光,都能輕松駕馭。建議結合「Alpha 通道」視圖觀察透明區域,逐步優化細節,最終導出高質量視頻。)…

使用BootStrap 3的原創的模態框組件,沒法彈出!估計是原創的bug

最近在給客戶開發一個CRM系統&#xff0c;其中用到了BOOTSTRAP的模態框。版本是3。由于是剛開始用該框架。所以在正式部署到項目中前&#xff0c;需要測試一下&#xff0c;找到框架中的如下部分。需要說明的是。我用的asp.net mvc框架開發。測試也是在asp.net mvc環境下。 復制…

Camera2 與 CameraX 閑談

目錄 &#x1f4c2; 前言 1. &#x1f531; Camera2 2. &#x1f531; CameraX 3. &#x1f531; Camera2 與 CameraX 1&#xff09;使用復雜度與開發效率 2&#xff09;控制能力與應用場景 3&#xff09;設備兼容性與穩定性 4&#xff09;更新與維護 4. &#x1f4a0…

【大語言模型_8】vllm啟動的模型通過fastapi封裝增加api-key驗證

背景&#xff1a; vllm推理框架啟動模型不具備api-key驗證。需借助fastapi可以實現該功能 代碼實現&#xff1a; rom fastapi import FastAPI, Header, HTTPException, Request,Response import httpx import logging# 創建 FastAPI 應用 app FastAPI() logging.basicConfig(…

基于SpringBoot的名著閱讀網站

作者&#xff1a;計算機學姐 開發技術&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源碼”。 專欄推薦&#xff1a;前后端分離項目源碼、SpringBoot項目源碼、Vue項目源碼、SSM項目源碼、微信小程序源碼 精品專欄&#xff1a;…

Langchain 自定義工具和內置工具

使用介紹 自定義工具時的元素概念介紹 在Langchain中&#xff0c;工具&#xff08;Tool&#xff09;是與語言模型交互的基本單元。以下是自定義工具時的關鍵元素&#xff1a; name 定義&#xff1a;工具的名稱&#xff0c;用于唯一標識該工具。作用&#xff1a;當工具被集成…

Gitee上庫常用git命令

Gitee上庫常用git命令 1、Fork 項目2、個人倉庫修改3、追加提交4、創建PR5、多筆commit合一 1、Fork 項目 2、個人倉庫修改 git add . // -s 表示自動添加郵箱簽名信息&#xff0c;-m表示其后跟隨commit描述 git commit -sm “add transition freeze” git push origin [目標…

Java 大視界 -- Java 大數據在智慧農業精準灌溉與施肥決策中的應用(144)

&#x1f496;親愛的朋友們&#xff0c;熱烈歡迎來到 青云交的博客&#xff01;能與諸位在此相逢&#xff0c;我倍感榮幸。在這飛速更迭的時代&#xff0c;我們都渴望一方心靈凈土&#xff0c;而 我的博客 正是這樣溫暖的所在。這里為你呈上趣味與實用兼具的知識&#xff0c;也…

Redux,React-redux。基礎

狀態管理庫&#xff0c;集中式存儲狀態&#xff0c;管理狀態 ? redux //簡單實現 redux源碼 export function createStore(reducer) {// reducer由用戶編寫&#xff0c; 必須是一個函數&#xff0c;dispatch的時候&#xff0c;reducer要執行if (typeof reducer ! function) t…

5.2 位運算專題:LeetCode 268. 丟失的數字

1. 題目鏈接 LeetCode 268. 丟失的數字 2. 題目描述 給定一個包含 [0, n] 范圍內 n 個不同整數的數組 nums&#xff08;實際長度為 n&#xff09;&#xff0c;找出數組中缺失的那個數字。 示例&#xff1a; 輸入&#xff1a;nums [3,0,1] → 輸出&#xff1a;2&#xff08;…

基于第三方庫的人臉識別系統的設計與實現

標題:基于第三方庫的人臉識別系統的設計與實現 內容:1.摘要 本文針對傳統人臉識別系統開發復雜、效率低的問題&#xff0c;旨在設計并實現基于第三方庫的人臉識別系統。通過選用合適的第三方人臉識別庫&#xff0c;利用其成熟的算法和接口&#xff0c;簡化系統開發流程。對收集…

【Android】VehiclePropertyAccess引起CarService崩潰

VehiclePropertyAccess引起CarService崩潰 VehiclePropertyAccess VehiclePropertyAccess屬性&#xff0c;用于定義車輛屬性的訪問權限。權限包括 讀&#xff1a;READ&#xff0c;只可以讀取&#xff0c;不能寫入。 VehiclePropertyAccess:READ寫&#xff1a;WRITE&#xf…

【Go】Go語言并發模型:MPG

Go 語言并發模型&#xff1a;MPG Go 的并發模型主要由三個部分構成&#xff1a; M (Machine) 系統線程&#xff0c;用于實際執行任務。 P (Processor) 邏輯處理器&#xff0c;負責管理和調度 goroutine。每個 P 擁有一個本地隊列和關聯的全局 G 隊列。 G (Goroutine) Go 語言…

SpringCloud配置中心:Config Server與配置刷新機制

文章目錄 引言一、Config Server基礎架構1.1 Server端配置1.2 配置文件命名規則 二、Config Client配置2.1 Client端配置2.2 配置注入與使用 三、配置刷新機制3.1 手動刷新配置3.2 使用Spring Cloud Bus實現自動刷新3.3 配置倉庫Webhook自動觸發刷新 四、高級配置管理策略4.1 配…

PyTorch生成式人工智能實戰:從零打造創意引擎

PyTorch生成式人工智能實戰&#xff1a;從零打造創意引擎 0. 前言1. 生成式人工智能1.1 生成式人工智能簡介1.2 生成式人工智能技術 2. Python 與 PyTorch2.1 Python 編程語言2.2 PyTorch 深度學習庫 3. 生成對抗網絡3.1 生成對抗網絡概述3.2 生成對抗網絡應用 4. Transformer4…

allure結合pytest生成測試報告

結合 pytest 和 Allure 可以生成詳細而美觀的測試報告&#xff0c;幫助測試人員和開發者更好地理解測試結果。這包括測試的執行情況、步驟、附件&#xff08;如截圖&#xff09;、分類以及優先級標記。下面是如何在 pytest 中使用 Allure 生成測試報告的步驟&#xff1a; 安裝…

STM32標準庫開發中斷流程

在STM32標準外設庫&#xff08;SPL&#xff09;開發中&#xff0c;外設中斷的處理流程通常如下&#xff1a; 一、標準庫外設中斷處理流程 &#xff08;1&#xff09;使能外設時鐘 在使用任何外設之前&#xff0c;都必須打開外設的時鐘。例如&#xff0c;使用USART1的中斷&…