SpringCloud微服務實戰——搭建企業級開發框架(三十):整合EasyExcel實現數據表格導入導出功能
- 2021 年 12 月 7 日
- 筆記
- SpringCloud, 微服務
批量上傳數據導入、數據統計分析導出,已經基本是系統必不可缺的一項功能,這裡從性能和易用性方面考慮,集成EasyExcel。EasyExcel是一個基於Java的簡單、省記憶體的讀寫Excel的開源項目,在儘可能節約記憶體的情況下支援讀寫百M的Excel:
Java解析、生成Excel比較有名的框架有Apache poi、jxl。但他們都存在一個嚴重的問題就是非常的耗記憶體,poi有一套SAX模式的API可以一定程度的解決一些記憶體溢出的問題,但POI還是有一些缺陷,比如07版Excel解壓縮以及解壓後存儲都是在記憶體中完成的,記憶體消耗依然很大。easyexcel重寫了poi對07版Excel的解析,一個3M的excel用POI sax解析依然需要100M左右記憶體,改用easyexcel可以降低到幾M,並且再大的excel也不會出現記憶體溢出;03版依賴POI的sax模式,在上層做了模型轉換的封裝,讓使用者更加簡單方便。(//github.com/alibaba/easyexcel/)
一、引入依賴的庫
1、在GitEgg-Platform項目中修改gitegg-platform-bom工程的pom.xml文件,增加EasyExcel的Maven依賴。
<properties>
......
<!-- Excel 數據導入導出 -->
<easyexcel.version>2.2.10</easyexcel.version>
</properties>
<dependencymanagement>
<dependencies>
......
<!-- Excel 數據導入導出 -->
<dependency>
<groupid>com.alibaba</groupid>
<artifactid>easyexcel</artifactid>
<version>${easyexcel.version}</version>
</dependency>
......
</dependencies>
</dependencymanagement>
2、修改gitegg-platform-boot工程的pom.xml文件,添加EasyExcel依賴。這裡考慮到數據導入導出是系統必備功能,所有引用springboot工程的微服務都需要用到EasyExcel,並且目前版本EasyExcel不支援LocalDateTime日期格式,這裡需要自定義LocalDateTimeConverter轉換器,用於在數據導入導出時支援LocalDateTime。
pom.xml文件
<dependencies>
......
<!-- Excel 數據導入導出 -->
<dependency>
<groupid>com.alibaba</groupid>
<artifactid>easyexcel</artifactid>
</dependency>
</dependencies>
自定義LocalDateTime轉換器LocalDateTimeConverter.java
package com.gitegg.platform.boot.excel;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.CellData;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
/**
* 自定義LocalDateStringConverter
* 用於解決使用easyexcel導出表格時候,默認不支援LocalDateTime日期格式
*
* @author GitEgg
*/
public class LocalDateTimeConverter implements Converter<localdatetime> {
/**
* 不使用{@code @DateTimeFormat}註解指定日期格式時,默認會使用該格式.
*/
private static final String DEFAULT_PATTERN = "yyyy-MM-dd HH:mm:ss";
@Override
public Class supportJavaTypeKey() {
return LocalDateTime.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
/**
* 這裡讀的時候會調用
*
* @param cellData excel數據 (NotNull)
* @param contentProperty excel屬性 (Nullable)
* @param globalConfiguration 全局配置 (NotNull)
* @return 讀取到記憶體中的數據
*/
@Override
public LocalDateTime convertToJavaData(CellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
DateTimeFormat annotation = contentProperty.getField().getAnnotation(DateTimeFormat.class);
return LocalDateTime.parse(cellData.getStringValue(),
DateTimeFormatter.ofPattern(Objects.nonNull(annotation) ? annotation.value() : DEFAULT_PATTERN));
}
/**
* 寫的時候會調用
*
* @param value java value (NotNull)
* @param contentProperty excel屬性 (Nullable)
* @param globalConfiguration 全局配置 (NotNull)
* @return 寫出到excel文件的數據
*/
@Override
public CellData convertToExcelData(LocalDateTime value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
DateTimeFormat annotation = contentProperty.getField().getAnnotation(DateTimeFormat.class);
return new CellData(value.format(DateTimeFormatter.ofPattern(Objects.nonNull(annotation) ? annotation.value() : DEFAULT_PATTERN)));
}
}
以上依賴及轉換器編輯好之後,點擊Platform的install,將依賴重新安裝到本地庫,然後GitEgg-Cloud就可以使用定義的依賴和轉換器了。
二、業務實現及測試
因為依賴的庫及轉換器都是放到gitegg-platform-boot工程下的,所以,所有使用到gitegg-platform-boot的都可以直接使用EasyExcel的相關功能,在GitEgg-Cloud項目下重新Reload All Maven Projects。這裡以gitegg-code-generator微服務項目舉例說明數據導入導出的用法。
1、EasyExcel可以根據實體類的註解來進行Excel的讀取和生成,在entity目錄下新建數據導入和導出的實體類模板文件。
文件導入的實體類模板DatasourceImport.java
package com.gitegg.code.generator.datasource.entity;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.annotation.write.style.ContentRowHeight;
import com.alibaba.excel.annotation.write.style.HeadRowHeight;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* <p>
* 數據源配置上傳
* </p>
*
* @author GitEgg
* @since 2021-08-18 16:39:49
*/
@Data
@HeadRowHeight(20)
@ContentRowHeight(15)
@ApiModel(value="DatasourceImport對象", description="數據源配置導入")
public class DatasourceImport {
@ApiModelProperty(value = "數據源名稱")
@ExcelProperty(value = "數據源名稱" ,index = 0)
@ColumnWidth(20)
private String datasourceName;
@ApiModelProperty(value = "連接地址")
@ExcelProperty(value = "連接地址" ,index = 1)
@ColumnWidth(20)
private String url;
@ApiModelProperty(value = "用戶名")
@ExcelProperty(value = "用戶名" ,index = 2)
@ColumnWidth(20)
private String username;
@ApiModelProperty(value = "密碼")
@ExcelProperty(value = "密碼" ,index = 3)
@ColumnWidth(20)
private String password;
@ApiModelProperty(value = "資料庫驅動")
@ExcelProperty(value = "資料庫驅動" ,index = 4)
@ColumnWidth(20)
private String driver;
@ApiModelProperty(value = "資料庫類型")
@ExcelProperty(value = "資料庫類型" ,index = 5)
@ColumnWidth(20)
private String dbType;
@ApiModelProperty(value = "備註")
@ExcelProperty(value = "備註" ,index = 6)
@ColumnWidth(20)
private String comments;
}
文件導出的實體類模板DatasourceExport.java
package com.gitegg.code.generator.datasource.entity;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.annotation.write.style.ContentRowHeight;
import com.alibaba.excel.annotation.write.style.HeadRowHeight;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import com.gitegg.platform.boot.excel.LocalDateTimeConverter;
import lombok.Data;
import java.time.LocalDateTime;
/**
* <p>
* 數據源配置下載
* </p>
*
* @author GitEgg
* @since 2021-08-18 16:39:49
*/
@Data
@HeadRowHeight(20)
@ContentRowHeight(15)
@ApiModel(value="DatasourceExport對象", description="數據源配置導出")
public class DatasourceExport {
@ApiModelProperty(value = "主鍵")
@ExcelProperty(value = "序號" ,index = 0)
@ColumnWidth(15)
private Long id;
@ApiModelProperty(value = "數據源名稱")
@ExcelProperty(value = "數據源名稱" ,index = 1)
@ColumnWidth(20)
private String datasourceName;
@ApiModelProperty(value = "連接地址")
@ExcelProperty(value = "連接地址" ,index = 2)
@ColumnWidth(20)
private String url;
@ApiModelProperty(value = "用戶名")
@ExcelProperty(value = "用戶名" ,index = 3)
@ColumnWidth(20)
private String username;
@ApiModelProperty(value = "密碼")
@ExcelProperty(value = "密碼" ,index = 4)
@ColumnWidth(20)
private String password;
@ApiModelProperty(value = "資料庫驅動")
@ExcelProperty(value = "資料庫驅動" ,index = 5)
@ColumnWidth(20)
private String driver;
@ApiModelProperty(value = "資料庫類型")
@ExcelProperty(value = "資料庫類型" ,index = 6)
@ColumnWidth(20)
private String dbType;
@ApiModelProperty(value = "備註")
@ExcelProperty(value = "備註" ,index = 7)
@ColumnWidth(20)
private String comments;
@ApiModelProperty(value = "創建日期")
@ExcelProperty(value = "創建日期" ,index = 8, converter = LocalDateTimeConverter.class)
@ColumnWidth(22)
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
}
2、在DatasourceController中新建上傳和下載方法:
/**
* 批量導出數據
* @param response
* @param queryDatasourceDTO
* @throws IOException
*/
@GetMapping("/download")
public void download(HttpServletResponse response, QueryDatasourceDTO queryDatasourceDTO) throws IOException {
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
// 這裡URLEncoder.encode可以防止中文亂碼 當然和easyexcel沒有關係
String fileName = URLEncoder.encode("數據源列表", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
List<datasourcedto> dataSourceList = datasourceService.queryDatasourceList(queryDatasourceDTO);
List<datasourceexport> dataSourceExportList = new ArrayList<>();
for (DatasourceDTO datasourceDTO : dataSourceList) {
DatasourceExport dataSourceExport = BeanCopierUtils.copyByClass(datasourceDTO, DatasourceExport.class);
dataSourceExportList.add(dataSourceExport);
}
String sheetName = "數據源列表";
EasyExcel.write(response.getOutputStream(), DatasourceExport.class).sheet(sheetName).doWrite(dataSourceExportList);
}
/**
* 下載導入模板
* @param response
* @throws IOException
*/
@GetMapping("/download/template")
public void downloadTemplate(HttpServletResponse response) throws IOException {
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
// 這裡URLEncoder.encode可以防止中文亂碼 當然和easyexcel沒有關係
String fileName = URLEncoder.encode("數據源導入模板", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
String sheetName = "數據源列表";
EasyExcel.write(response.getOutputStream(), DatasourceImport.class).sheet(sheetName).doWrite(null);
}
/**
* 上傳數據
* @param file
* @return
* @throws IOException
*/
@PostMapping("/upload")
public Result<!--?--> upload(@RequestParam("uploadFile") MultipartFile file) throws IOException {
List<datasourceimport> datasourceImportList = EasyExcel.read(file.getInputStream(), DatasourceImport.class, null).sheet().doReadSync();
if (!CollectionUtils.isEmpty(datasourceImportList))
{
List<datasource> datasourceList = new ArrayList<>();
datasourceImportList.stream().forEach(datasourceImport-> {
datasourceList.add(BeanCopierUtils.copyByClass(datasourceImport, Datasource.class));
});
datasourceService.saveBatch(datasourceList);
}
return Result.success();
}
3、前端導出(下載)設置,我們前端框架請求用的是axios,正常情況下,普通的請求成功或失敗返回的responseType為json格式,當我們下載文件時,請求返回的是文件流,這裡需要設置下載請求的responseType為blob。考慮到下載是一個通用的功能,這裡提取出下載方法為一個公共方法:首先是判斷服務端的返回格式,當一個下載請求返回的是json格式時,那麼說明這個請求失敗,需要處理錯誤新題並提示,如果不是,那麼走正常的文件流下載流程。
api請求
//請求的responseType設置為blob格式
export function downloadDatasourceList (query) {
return request({
url: '/gitegg-plugin-code/code/generator/datasource/download',
method: 'get',
responseType: 'blob',
params: query
})
}
導出/下載的公共方法
// 處理請求返回資訊
export function handleDownloadBlod (fileName, response) {
const res = response.data
if (res.type === 'application/json') {
const reader = new FileReader()
reader.readAsText(response.data, 'utf-8')
reader.onload = function () {
const { msg } = JSON.parse(reader.result)
notification.error({
message: '下載失敗',
description: msg
})
}
} else {
exportBlod(fileName, res)
}
}
// 導出Excel
export function exportBlod (fileName, data) {
const blob = new Blob([data])
const elink = document.createElement('a')
elink.download = fileName
elink.style.display = 'none'
elink.href = URL.createObjectURL(blob)
document.body.appendChild(elink)
elink.click()
URL.revokeObjectURL(elink.href)
document.body.removeChild(elink)
}
vue頁面調用
handleDownload () {
this.downloadLoading = true
downloadDatasourceList(this.listQuery).then(response => {
handleDownloadBlod('數據源配置列表.xlsx', response)
this.listLoading = false
})
},
4、前端導入(上傳的設置),前端無論是Ant Design of Vue框架還是ElementUI框架都提供了上傳組件,用法都是一樣的,在上傳之前需要組裝FormData數據,除了上傳的文件,還可以自定義傳到後台的參數。
上傳組件
<a-upload name="uploadFile" :show-upload-list="false" :before-upload="beforeUpload">
<a-button> <a-icon type="upload"> 導入 </a-icon></a-button>
</a-upload>
上傳方法
beforeUpload (file) {
this.handleUpload(file)
return false
},
handleUpload (file) {
this.uploadedFileName = ''
const formData = new FormData()
formData.append('uploadFile', file)
this.uploading = true
uploadDatasource(formData).then(() => {
this.uploading = false
this.$message.success('數據導入成功')
this.handleFilter()
}).catch(err => {
console.log('uploading', err)
this.$message.error('數據導入失敗')
})
},
以上步驟,就把EasyExcel整合完成,基本的數據導入導出功能已經實現,在業務開發過程中,可能會用到複雜的Excel導出,比如包含圖片、圖表等的Excel導出,這一塊需要根據具體業務需要,參考EasyExcel的詳細用法來訂製自己的導出方法。
源碼地址:
Gitee: //gitee.com/wmz1930/GitEgg
GitHub: //github.com/wmz1930/GitEgg