SpringCloud微服務實戰——搭建企業級開發框架(三十):整合EasyExcel實現資料表格匯入匯出功能

全棧程式猿 發表於 2021-12-07
框架 微服務 Spring Excel

  批量上傳資料匯入、資料統計分析匯出,已經基本是系統必不可缺的一項功能,這裡從效能和易用性方面考慮,整合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模式,在上層做了模型轉換的封裝,讓使用者更加簡單方便。(https://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: https://gitee.com/wmz1930/GitEgg

GitHub: https://github.com/wmz1930/GitEgg