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

  批量上傳數據導入、數據統計分析導出,已經基本是系統必不可缺的一項功能,這裡從性能和易用性方面考慮,集成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