Springboot整合easyexcel實(shí)現(xiàn)一個(gè)接口任意表的Excel導(dǎo)入導(dǎo)出
Java的web開(kāi)發(fā)需要excel的導(dǎo)入導(dǎo)出工具,所以需要一定的工具類(lèi)實(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)依賴(lài)--> <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),用來(lái)指代導(dǎo)入的特定的對(duì)象
@Data @NoArgsConstructor @AllArgsConstructor @TableName("stu_info") @ApiModel("學(xué)生信息") //@ExcelIgnoreUnannotated 沒(méi)有注解的字段都不轉(zhuǎn)換 public class StuInfo { private static final long serialVersionUID = 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) private String name; ? /** * 年齡 */ // @ExcelIgnore不將該字段轉(zhuǎn)換成Excel @ExcelProperty(value = "年齡",order = 1) @ApiModelProperty("年齡") @ExportHeader(value = "年齡",index = 2) private Integer age; ? /** * 身高 */ //自定義格式-位數(shù) // @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; ? /** * 入學(xué)時(shí)間 */ //自定義格式-時(shí)間格式 @DateTimeFormat("yyyy-MM-dd HH:mm:ss:") @ExcelProperty(value = "入學(xué)時(shí)間",order = 6) @ApiModelProperty("入學(xué)時(shí)間") private String intake; ? /** * 出生日期 */ @ExcelProperty(value = "出生日期",order = 7) @ApiModelProperty("出生日期") private String birthday; }
重寫(xiě)ReadListener接口
@Slf4j public class UploadDataListener<T> implements ReadListener<T> { ? /** * 每隔5條存儲(chǔ)數(shù)據(jù)庫(kù),實(shí)際使用中可以100條,然后清理list ,方便內(nèi)存回收 */ private static final int BATCH_COUNT = 100; ? /** * 緩存的數(shù)據(jù) */ private List<T> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT); ? /** * Predicate用于過(guò)濾數(shù)據(jù) */ private Predicate<T> predicate; ? /** * 調(diào)用持久層批量保存 */ 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,請(qǐng)使用這個(gè)構(gòu)造方法。每次創(chuàng)建Listener的時(shí)候需要把spring管理的類(lèi)傳進(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 public void invoke(T data, AnalysisContext context) { ? 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 (Exception e) { ? log.error("Failed to upload data!data={}", cachedDataList); throw new BizException("導(dǎo)入失敗"); } // 存儲(chǔ)完成清理 list cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT); } } ? /** * 所有數(shù)據(jù)解析完成了 都會(huì)來(lái)調(diào)用 * * @param context */ @Override public void doAfterAllAnalysed(AnalysisContext context) { ? // 這里也要保存數(shù)據(jù),確保最后遺留的數(shù)據(jù)也存儲(chǔ)到數(shù)據(jù)庫(kù) if (CollUtil.isNotEmpty(cachedDataList)) { ? try { // 執(zhí)行具體消費(fèi)邏輯 consumer.accept(cachedDataList); log.info("所有數(shù)據(jù)解析完成!"); } catch (Exception e) { ? log.error("Failed to upload data!data={}", cachedDataList); ? // 拋出自定義的提示信息 if (e instanceof BizException) { throw e; } ? throw new BizException("導(dǎo)入失敗"); } } } }
Controller層的實(shí)現(xiàn)
@ApiOperation("只需要一個(gè)readListener,解決全部的問(wèn)題") @PostMapping("/update") @ResponseBody public R<String> aListener4AllExcel(MultipartFile file) throws IOException { try { EasyExcel.read(file.getInputStream(), StuInfo.class, new UploadDataListener<StuInfo>( list -> { // 校驗(yàn)數(shù)據(jù) // ValidationUtils.validate(list); // dao 保存··· //最好是手寫(xiě)一個(gè),不要使用mybatis-plus的一條條新增的邏輯 service.saveBatch(list); log.info("從Excel導(dǎo)入數(shù)據(jù)一共 {} 行 ", list.size()); })) .sheet() .doRead(); } catch (IOException e) { ? log.error("導(dǎo)入失敗", e); throw new BizException("導(dǎo)入失敗"); } return R.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 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 { //根據(jù)tableCode獲取這張表的字段,可以作為insert與劇中的信息 EasyExcel.read(file.getInputStream(), new NonClazzOrientedListener( list -> { // 校驗(yàn)數(shù)據(jù) // ValidationUtils.validate(list); ? // dao 保存··· log.info("從Excel導(dǎo)入數(shù)據(jù)一共 {} 行 ", list.size()); })) .sheet() .doRead(); } catch (IOException e) { log.error("導(dǎo)入失敗", e); throw new BizException("導(dǎo)入失敗"); } return R.success("SUCCESS"); }
重寫(xiě)ReadListener接口
@Slf4j public class NonClazzOrientedListener implements ReadListener<Map<Integer, String>> { ? /** * 每隔5條存儲(chǔ)數(shù)據(jù)庫(kù),實(shí)際使用中可以100條,然后清理list ,方便內(nèi)存回收 */ private static final int BATCH_COUNT = 100; ? private List<List<Object>> rowsList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT); ? private List<Object> rowList = new ArrayList<>(); /** * Predicate用于過(guò)濾數(shù)據(jù) */ private Predicate<Map<Integer, String>> predicate; ? /** * 調(diào)用持久層批量保存 */ 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標(biāo)識(shí) */ private boolean flag = false; ? @Override public 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("執(zhí)行存儲(chǔ)程序"); log.info("rowsList is {}", rowsList); rowsList.clear(); } } ? @Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { consumer.accept(rowsList); if (CollUtil.isNotEmpty(rowsList)) { try { log.debug("執(zhí)行最后的程序"); log.info("rowsList is {}", rowsList); } catch (Exception e) { ? log.error("Failed to upload data!data={}", rowsList); ? // 拋出自定義的提示信息 if (e instanceof BizException) { throw e; } ? throw new BizException("導(dǎo)入失敗"); } finally { rowsList.clear(); } } }
這種方式可以通過(guò)把表中的字段順序存儲(chǔ)起來(lái),通過(guò)配置數(shù)據(jù)和字段的位置實(shí)現(xiàn)數(shù)據(jù)的新增,那么如果出現(xiàn)了導(dǎo)出數(shù)據(jù)模板/手寫(xiě)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 public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) { //該方法必然會(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 = new HashMap(); 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中線程池的開(kāi)發(fā)與使用與原理我可以單獨(dú)寫(xiě)一篇文章進(jìn)行講解,但是在這邊為了進(jìn)行好的開(kāi)發(fā)我先給出一套固定一點(diǎn)的方法。
由于ReadListener不能被注冊(cè)到IOC容器里面,所以需要在外面開(kāi)啟
詳情可見(jiàn)Spring Boot通過(guò)EasyExcel異步多線程實(shí)現(xiàn)大數(shù)據(jù)量Excel導(dǎo)入,百萬(wàn)數(shù)據(jù)30秒
通過(guò)泛型實(shí)現(xiàn)對(duì)象類(lèi)型的導(dǎo)出
public <T> void commonExport(String fileName, List<T> data, Class<T> clazz, HttpServletResponse response) throws IOException { if (CollectionUtil.isEmpty(data)) { data = new ArrayList<>(); } //設(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è)方法
public void exportFreely(String fileName, List<List<Object>> data, List<List<String>> head, HttpServletResponse response) throws IOException { if (CollectionUtil.isEmpty(data)) { data = new ArrayList<>(); } //設(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ú)寫(xiě)一篇文章解決這個(gè)問(wèn)題。
到此這篇關(guān)于Springboot整合easyexcel實(shí)現(xiàn)一個(gè)接口任意表的Excel導(dǎo)入導(dǎo)出的文章就介紹到這了,更多相關(guān)Springboot Excel導(dǎo)入導(dǎo)出內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot?整合?EasyExcel?實(shí)現(xiàn)自由導(dǎo)入導(dǎo)出功能
- SpringBoot整合EasyExcel實(shí)現(xiàn)批量導(dǎo)入導(dǎo)出
- SpringBoot整合easyExcel實(shí)現(xiàn)CSV格式文件的導(dǎo)入導(dǎo)出
- SpringBoot整合EasyExcel實(shí)現(xiàn)復(fù)雜Excel表格的導(dǎo)入導(dǎo)出
- 使用SpringBoot與EasyExcel實(shí)現(xiàn)復(fù)雜的導(dǎo)入導(dǎo)出
- SpringBoot整合EasyExcel實(shí)現(xiàn)導(dǎo)入導(dǎo)出功能
- SpringBoot中EasyExcel實(shí)現(xiàn)execl導(dǎo)入導(dǎo)出
- SpringBoot整合EasyExcel實(shí)現(xiàn)導(dǎo)入導(dǎo)出數(shù)據(jù)
- SpringBoot整合EasyExcel實(shí)現(xiàn)文件導(dǎo)入導(dǎo)出
相關(guān)文章
使用lombok注解導(dǎo)致mybatis-plus TypeHandler失效的解決
這篇文章主要介紹了使用lombok注解導(dǎo)致mybatis-plus TypeHandler失效的解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-07-07java利用htmlparser獲取html中想要的代碼具體實(shí)現(xiàn)
這篇文章主要介紹了java利用htmlparser獲取html中想要的代碼具體實(shí)現(xiàn),需要的朋友可以參考下2014-02-02SpringDataJPA實(shí)體類(lèi)關(guān)系映射配置方式
這篇文章主要介紹了SpringDataJPA實(shí)體類(lèi)關(guān)系映射配置方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12spring-security關(guān)于hasRole的坑及解決
這篇文章主要介紹了spring-security關(guān)于hasRole的坑及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-09-09Java實(shí)現(xiàn)讀取設(shè)置pdf屬性信息
這篇文章主要為大家詳細(xì)介紹了如何使用Java實(shí)現(xiàn)讀取設(shè)置pdf屬性信息,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-01-01Java通過(guò)JNI 調(diào)用動(dòng)態(tài)鏈接庫(kù)DLL操作
這篇文章主要介紹了Java通過(guò)JNI 調(diào)用動(dòng)態(tài)鏈接庫(kù)DLL操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-11-11Java項(xiàng)目中如何引入Hutool工具類(lèi)并正確使用它
Hutool是一個(gè)小而全的Java工具類(lèi)庫(kù),通過(guò)靜態(tài)方法封裝,降低相關(guān)API的學(xué)習(xí)成本,提高工作效率,使Java擁有函數(shù)式語(yǔ)言般的優(yōu)雅,這篇文章主要給大家介紹了關(guān)于Java項(xiàng)目中如何引入Hutool工具類(lèi)并正確使用它的相關(guān)資料,需要的朋友可以參考下2024-01-01Java基于Spire Cloud Excel把Excel轉(zhuǎn)換成PDF
這篇文章主要介紹了Java基于Spire Cloud Excel把Excel轉(zhuǎn)換成PDF,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-05-05