Spring Boot 一個接口實現(xiàn)任意表的 Excel 導入導出
Java的web開發(fā)需要excel的導入導出工具,所以需要一定的工具類實現(xiàn),如果是使用easypoi、Hutool導入導出excel,會非常的損耗內(nèi)存,因此可以嘗試使用easyexcel解決大數(shù)據(jù)量的數(shù)據(jù)的導入導出,且可以通過Java8的函數(shù)式編程解決該問題。
使用easyexcel,雖然不太會出現(xiàn)OOM的問題,但是如果是大數(shù)據(jù)量的情況下也會有一定量的內(nèi)存溢出的風險,所以我打算從以下幾個方面優(yōu)化這個問題:
- 使用Java8的函數(shù)式編程實現(xiàn)低代碼量的數(shù)據(jù)導入
- 使用反射等特性實現(xiàn)單個接口導入任意excel
- 使用線程池實現(xiàn)大數(shù)據(jù)量的excel導入
- 通過泛型實現(xiàn)數(shù)據(jù)導出
maven導入
<!--EasyExcel相關(guān)依賴-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.0.5</version>
</dependency>
使用泛型實現(xiàn)對象的單個Sheet導入
先實現(xiàn)一個類,用來指代導入的特定的對象
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("stu_info")
@ApiModel("學生信息")
//@ExcelIgnoreUnannotated 沒有注解的字段都不轉(zhuǎn)換
publicclass StuInfo {
privatestaticfinallong serialVersionUID = 1L;
/**
* 姓名
*/
// 設(shè)置字體,此處代表使用斜體
// @ContentFontStyle(italic = BooleanEnum.TRUE)
// 設(shè)置列寬度的注解,注解中只有一個參數(shù)value,value的單位是字符長度,最大可以設(shè)置255個字符
@ColumnWidth(10)
// @ExcelProperty 注解中有三個參數(shù)value,index,converter分別代表表名,列序號,數(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;
/**
* 入學時間
*/
//自定義格式-時間格式
@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條存儲數(shù)據(jù)庫,實際使用中可以100條,然后清理list ,方便內(nèi)存回收
*/
privatestaticfinalint BATCH_COUNT = 100;
/**
* 緩存的數(shù)據(jù)
*/
private List<T> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
/**
* Predicate用于過濾數(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,請使用這個構(gòu)造方法。每次創(chuàng)建Listener的時候需要把spring管理的類傳進來
*
* @param demoDAO
*/
/**
* 這個每一條數(shù)據(jù)解析都會來調(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);
// 達到BATCH_COUNT了,需要去存儲一次數(shù)據(jù)庫,防止數(shù)據(jù)幾萬條數(shù)據(jù)在內(nèi)存,容易OOM
if (cachedDataList.size() >= BATCH_COUNT) {
try {
// 執(zhí)行具體消費邏輯
consumer.accept(cachedDataList);
} catch (Exception e) {
log.error("Failed to upload data!data={}", cachedDataList);
thrownew BizException("導入失敗");
}
// 存儲完成清理 list
cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
}
}
/**
* 所有數(shù)據(jù)解析完成了 都會來調(diào)用
*
* @param context
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 這里也要保存數(shù)據(jù),確保最后遺留的數(shù)據(jù)也存儲到數(shù)據(jù)庫
if (CollUtil.isNotEmpty(cachedDataList)) {
try {
// 執(zhí)行具體消費邏輯
consumer.accept(cachedDataList);
log.info("所有數(shù)據(jù)解析完成!");
} catch (Exception e) {
log.error("Failed to upload data!data={}", cachedDataList);
// 拋出自定義的提示信息
if (e instanceof BizException) {
throw e;
}
thrownew BizException("導入失敗");
}
}
}
}
Controller層的實現(xiàn)
@ApiOperation("只需要一個readListener,解決全部的問題")
@PostMapping("/update")
@ResponseBody
public R<String> aListener4AllExcel(MultipartFile file) throws IOException {
try {
EasyExcel.read(file.getInputStream(),
StuInfo.class,
new UploadDataListener<StuInfo>(
list -> {
// 校驗數(shù)據(jù)
ValidationUtils.validate(list);
// dao 保存···
//最好是手寫一個,不要使用mybatis-plus的一條條新增的邏輯
service.saveBatch(list);
log.info("從Excel導入數(shù)據(jù)一共 {} 行 ", list.size());
}))
.sheet()
.doRead();
} catch (IOException e) {
log.error("導入失敗", e);
thrownew BizException("導入失敗");
}
return R.success("SUCCESS");
}
但是這種方式只能實現(xiàn)已存對象的功能實現(xiàn),如果要新增一種數(shù)據(jù)的導入,那我們需要怎么做呢?關(guān)注公眾號:碼猿技術(shù)專欄,回復關(guān)鍵詞:1111 獲取阿里內(nèi)部java性能調(diào)優(yōu)手冊!
可以通過讀取成Map,根據(jù)順序?qū)氲綌?shù)據(jù)庫中。
通過實現(xiàn)單個Sheet中任意一種數(shù)據(jù)的導入
Controller層的實現(xiàn)
@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 {
//根據(jù)tableCode獲取這張表的字段,可以作為insert與劇中的信息
EasyExcel.read(file.getInputStream(),
new NonClazzOrientedListener(
list -> {
// 校驗數(shù)據(jù)
// ValidationUtils.validate(list);
// dao 保存···
log.info("從Excel導入數(shù)據(jù)一共 {} 行 ", 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條存儲數(shù)據(jù)庫,實際使用中可以100條,然后清理list ,方便內(nèi)存回收
*/
privatestaticfinalint BATCH_COUNT = 100;
private List<List<Object>> rowsList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
private List<Object> rowList = new ArrayList<>();
/**
* Predicate用于過濾數(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標識
*/
privateboolean 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í)行存儲程序");
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;
}
thrownew BizException("導入失敗");
} finally {
rowsList.clear();
}
}
}
這種方式可以通過把表中的字段順序存儲起來,通過配置數(shù)據(jù)和字段的位置實現(xiàn)數(shù)據(jù)的新增,那么如果出現(xiàn)了導出數(shù)據(jù)模板/手寫excel的時候順序和導入的時候順序不一樣怎么辦?
可以通過讀取header進行實現(xiàn),通過表頭讀取到的字段,和數(shù)據(jù)庫中表的字段進行比對,只取其中存在的數(shù)據(jù)進行排序添加
/**
* 這里會一行行的返回頭
*
* @param headMap
* @param context
*/
@Override
public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
//該方法必然會在讀取數(shù)據(jù)之前進行
Map<Integer, String> columMap = ConverterUtils.convertToStringMap(headMap, context);
//通過數(shù)據(jù)交互拿到這個表的表頭
// Map<String,String> columnList=dao.xxxx();
Map<String, String> columnList = new HashMap();
columMap.forEach((key, value) -> {
if (columnList.containsKey(value)) {
filterList.add(key);
}
});
//過濾到了只存在表里面的數(shù)據(jù),順序就不用擔心了,可以直接把filterList的數(shù)據(jù)用于排序,可以根據(jù)mybatis做一個動態(tài)sql進行應用
log.info("解析到一條頭數(shù)據(jù):{}", JSON.toJSONString(columMap));
// 如果想轉(zhuǎn)成成 Map<Integer,String>
// 方案1: 不要implements ReadListener 而是 extends AnalysisEventListener
// 方案2: 調(diào)用 ConverterUtils.convertToStringMap(headMap, context) 自動會轉(zhuǎn)換
}
那么這些問題都解決了,如果出現(xiàn)大數(shù)據(jù)量的情況,如果要極大的使用到cpu,該怎么做呢?
可以嘗試使用線程池進行實現(xiàn)
使用線程池進行多線程導入大量數(shù)據(jù)
Java中線程池的開發(fā)與使用與原理我可以單獨寫一篇文章進行講解,但是在這邊為了進行好的開發(fā)我先給出一套固定一點的方法。
由于ReadListener不能被注冊到IOC容器里面,所以需要在外面開啟。
詳情可見:https://juejin.cn/post/7251566038524133436
通過泛型實現(xiàn)對象類型的導出
public <T> void commonExport(String fileName, List<T> data, Class<T> clazz, HttpServletResponse response) throws IOException {
if (CollectionUtil.isEmpty(data)) {
data = new ArrayList<>();
}
//設(shè)置標題
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ù)的導出接口。
如果想要動態(tài)的下載任意一組數(shù)據(jù)怎么辦呢?可以使用這個方法。
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è)置標題
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);
}
什么?不僅想一個接口展示全部的數(shù)據(jù)與信息,還要增加篩選條件?這個后期可以單獨解決這個問題。