Spring Boot 3.4 一鍵搞定!接口實現任意表的 Excel 導入導出
在 Java Web 開發中,處理 Excel 文件的導入導出是常見且重要的需求,尤其是在大數據量的場景下,如何高效、安全地進行 Excel 文件的讀寫,直接影響到系統的性能與穩定性。傳統的工具如 EasyPoi 或 Hutool 提供了強大的功能,但在大規模數據處理時,這些工具常常面臨內存溢出(OOM)等性能瓶頸。為了解決這些問題,我們可以轉而使用 EasyExcel,它采用了低內存消耗的設計,能夠高效地處理海量數據的導入導出。
本文將介紹如何通過結合 Spring Boot 3.4 與 EasyExcel,實現一鍵搞定任意表的 Excel 導入導出。我們將通過使用 Java 8 的函數式編程特性、反射機制、以及多線程優化技術,進一步提升開發效率并確保系統的穩定性。特別地,在處理大數據量時,我們會通過批量存儲和線程池的方式,避免內存溢出問題,并進一步優化導入導出的性能。
優化策略
- 使用 Java 8 的函數式編程簡化數據導入
- 利用反射實現通用接口導入任意 Excel
- 通過線程池優化大數據量 Excel 導入性能
- 通過泛型支持多種數據導出格式
Maven 依賴
首先,需要在 pom.xml 文件中添加 EasyExcel 的依賴:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.0.5</version>
</dependency>
使用泛型實現對象的單個 Sheet 導入
首先,我們創建一個用于表示導入數據的類,假設是一個學生信息類:
package com.icoderoad.entity;
import com.alibaba.excel.annotation.ExcelProperty;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("stu_info")
@ApiModel("學生信息")
public class StuInfo {
private static final long serialVersionUID = 1L;
@ApiModelProperty("姓名")
@ExcelProperty(value = "姓名", order = 0)
private String name;
@ApiModelProperty("年齡")
@ExcelProperty(value = "年齡", order = 1)
private Integer age;
@ApiModelProperty("身高")
@ExcelProperty(value = "身高", order = 2)
private Double tall;
@ApiModelProperty("自我介紹")
@ExcelProperty(value = "自我介紹", order = 3)
private String selfIntroduce;
@ApiModelProperty("性別")
@ExcelProperty(value = "性別", order = 4)
private Integer gender;
@ApiModelProperty("入學時間")
@ExcelProperty(value = "入學時間", order = 5)
private String intake;
@ApiModelProperty("出生日期")
@ExcelProperty(value = "出生日期", order = 6)
private String birthday;
}
重寫 ReadListener 接口
為了處理數據導入過程中可能出現的內存溢出問題,我們重寫 ReadListener 接口,并將數據按批次進行存儲:
package com.icoderoad.listener;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.metadata.CellData;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.read.listener.ReadListener;
import com.icoderoad.exception.BizException;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;
@Slf4j
public class UploadDataListener<T> implements ReadListener<T> {
private static final int BATCH_COUNT = 100;
private List<T> cachedDataList = new ArrayList<>(BATCH_COUNT);
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;
}
@Override
public void invoke(T data, AnalysisContext context) {
if (predicate != null && !predicate.test(data)) {
return;
}
cachedDataList.add(data);
// When the batch size reaches BATCH_COUNT, trigger data storage
if (cachedDataList.size() >= BATCH_COUNT) {
try {
consumer.accept(cachedDataList);
} catch (Exception e) {
log.error("Data upload failed! Data={}", cachedDataList);
throw new BizException("Import failed");
}
cachedDataList.clear();
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
if (!cachedDataList.isEmpty()) {
try {
consumer.accept(cachedDataList);
log.info("All data parsing completed!");
} catch (Exception e) {
log.error("Data upload failed! Data={}", cachedDataList);
if (e instanceof BizException) {
throw e;
}
throw new BizException("Import failed");
}
}
}
}
Controller 層實現
在 Controller 層,我們使用 EasyExcel.read() 方法讀取上傳的文件,并通過 UploadDataListener 實現數據批量存儲:
package com.icoderoad.controller;
import com.alibaba.excel.EasyExcel;
import com.icoderoad.entity.StuInfo;
import com.icoderoad.listener.UploadDataListener;
import com.icoderoad.service.StuInfoService;
import com.icoderoad.util.ValidationUtils;
import com.icoderoad.exception.BizException;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
@Slf4j
@RestController
@RequestMapping("/excel")
public class ExcelController {
@Autowired
private StuInfoService service;
@ApiOperation("一鍵導入數據到 Excel")
@PostMapping("/update")
@ResponseBody
public R<String> importExcel(MultipartFile file) throws IOException {
try {
EasyExcel.read(file.getInputStream(), StuInfo.class, new UploadDataListener<StuInfo>(
list -> {
// 驗證數據
ValidationUtils.validate(list);
// 批量保存數據
service.saveBatch(list);
log.info("Imported {} rows of data from Excel", list.size());
}
)).sheet().doRead();
} catch (IOException e) {
log.error("Import failed", e);
throw new BizException("Import failed");
}
return R.success("Success");
}
}
處理任意數據表的導入
對于需要導入不同數據表的情況,我們可以通過傳遞表編碼以及文件來動態讀取數據,并進行適配:
@ApiOperation("通用數據表導入")
@PostMapping("/listenMapData")
@ResponseBody
public R<String> listenMapData(@RequestParam("tableCode") String tableCode, MultipartFile file) throws IOException {
try {
EasyExcel.read(file.getInputStream(), new NonClazzOrientedListener(
list -> {
log.info("Imported {} rows of data", list.size());
}
)).sheet().doRead();
} catch (IOException e) {
log.error("Import failed", e);
throw new BizException("Import failed");
}
return R.success("Success");
}
重寫 ReadListener 接口處理非類型化數據
當我們需要處理不同類型的表時,可以通過 Map 來處理數據,具體實現如下:
package com.icoderoad.listener;
import com.alibaba.excel.context.AnalysisContext;
import com.icoderoad.exception.BizException;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Predicate;
@Slf4j
public class NonClazzOrientedListener implements ReadListener<Map<Integer, String>> {
// 定義批次處理的大小
private static final int BATCH_COUNT = 100;
// 用于緩存行數據
private List<List<Object>> rowsList = new ArrayList<>(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;
}
@Override
public void invoke(Map<Integer, String> row, AnalysisContext analysisContext) {
// 判斷是否符合處理條件,如果有定義 Predicate,進行過濾
if (predicate != null && !predicate.test(row)) {
return;
}
// 清理 rowList,為下一行做準備
rowList.clear();
// 處理每一行的數據,將行數據添加到 rowList
row.forEach((k, v) -> {
log.debug("處理數據行,鍵:{},值:{}", k, v); // 中文日志輸出
rowList.add(v == null ? "" : v);
});
// 將處理過的 rowList 添加到 rowsList
rowsList.add(rowList);
// 當達到批次大小時,執行存儲操作
if (rowsList.size() >= BATCH_COUNT) {
processBatch();
}
}
// 批量處理數據,并清理緩存
private void processBatch() {
try {
log.debug("執行存儲邏輯,當前批次包含 {} 行數據", rowsList.size()); // 中文日志輸出
log.info("當前數據:{}", rowsList);
consumer.accept(rowsList);
} catch (Exception e) {
log.error("數據上傳失?。祿簕}", rowsList, e); // 中文日志輸出
if (e instanceof BizException) {
throw e;
}
throw new BizException("導入失敗");
} finally {
// 批次處理后清空緩存
rowsList.clear();
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
// 如果還有剩余數據沒有處理,執行最后一次存儲操作
if (!rowsList.isEmpty()) {
processBatch();
}
log.debug("所有數據處理并上傳完成。"); // 中文日志輸出
}
}
結論
通過 EasyExcel 和 Spring Boot 3.4 的完美結合,本文展示了一種高效且內存友好的 Excel 文件處理方案。無論是單一表格的導入導出,還是動態適配不同數據表的需求,我們都可以通過泛型和反射機制靈活實現。同時,利用線程池的方式優化大數據量處理,顯著提高了性能,避免了內存溢出(OOM)問題。通過本文的方法,你可以輕松實現任意表的數據導入導出,滿足各種業務需求,并為未來的大規模數據處理奠定堅實的基礎。
優化后的這兩部分旨在加強對文章主題的深入闡述,同時突出技術的實際應用價值和解決方案的優勢,增強文章的專業性和實踐性。如果你覺得還有其他可以進一步擴展或調整的地方,隨時告訴我!