首頁 > 軟體

Spring Boot專案如何優雅實現Excel匯入與匯出功能

2022-06-10 22:02:22

背景

Excel 匯入與匯出是專案中經常用到的功能,在 Java 中常用 poi 實現 Excel 的匯入與匯出。由於 poi 佔用記憶體較大,在高並行下很容易發生 OOM 或者頻繁 fullgc,阿里基於 poi 開源了 EasyExcel 專案。

除了節約記憶體,EasyExcel 還簡化了 API,通過註解對映 Excel 單元格與物件欄位之間的關係,簡單的幾行程式碼就能搞定複雜的匯入匯出功能了。

EasyExcel 問題

看似一切美好,不過經常做 Excel 匯入與匯出就會發現,EasyExcel 還是沒那麼完美的。

首先,匯入與匯出 Excel 本質是上將 Excel 檔案內容與 Java 物件之間做一個對映,EasyExcel 做的只是在這兩者之間轉換。如果專案中的 Excel 匯入與匯出功能比較多,會產生大量的樣板式程式碼,使用體驗類似於 JDBC。

另外,匯入往往還伴隨著校驗,這是 EasyExcel 沒有支援的功能。如果需要校驗,要麼寫程式碼手動判斷,要麼呼叫 Java Validation 規範 定義的 API 判斷,這又會產生大量樣板式程式碼。

而且,當前 spring boot 已經成了必備的 Java 開發框架,easyexcel 也沒有進行整合。

分析與解決

匯入與匯出通常發生在 Web 環境,對於 Spring MVC 來說,可以將請求資訊轉換為任意型別的 contoller 方法引數,將 controller 方法返回值轉換為使用者端支援的內容。

如果能夠使用自定義的 controller 方法引數接收 Excel 檔案內容,將 controller 方法返回值轉換為 Excel 檔案響應,可以直接消除 Excel 匯入與匯出時的樣板式程式碼。

另外在將請求內容轉換為 controller 方法引數時還可以加入自定義的校驗邏輯。

由於 Excel 匯入與匯出樣板式程式碼、校驗問題與具體的業務邏輯無關,可以單獨抽象出來,我這裡在 EasyExcel 的基礎上封裝了一個 easyexcel-spring-boot-starter 的專案,大大降低了 EasyExcel 上手的門檻,對使用者來說只需要使用 EasyExcel 定義的註解提供對映關係就可以了,適用於簡單場景的匯入匯出。

專案程式碼已上傳 github easyexcel-spring-boot-starter 倉庫,點選連結即可查閱。下面就來看看怎樣使用吧。

Spring Boot Excel 匯入與匯出

依賴引入

首先需要引入依賴,座標如下。

<dependency>
    <groupId>com.zzuhkp</groupId>
    <artifactId>easyexcel-spring-boot-starter</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

不過很不幸的是目前還沒傳至中央倉庫,需要的小夥伴可自行上傳到私有倉庫或直接把程式碼嵌入自己的專案。

Excel 匯入

首先看下要匯入的 Excel 內容吧。

為了接收 Excel 檔案內容,我們需要定義一個對應的 Model 類。

@Data
public class DemoData {
    @ExcelProperty(index = 0)
    private Integer integer;

    @ExcelProperty(index = 1)
    private String string;

    @ExcelProperty(index = 2)
    private Date date;
}

基本匯入功能

然後使用 List<T> 引數接收即可。

@PostMapping("/list/obj")
public List<DemoData> listObj(@ExcelParam List<DemoData> list) {
    return list;
}

注意引數前新增了 @ExcelParam 註解,用來標識 Excel 檔案引數。這樣,一個匯入功能實現了,是不是很簡單呢?

預設情況下接收名稱為 file 的表單欄位作為 Excel 檔案,如果不滿足還可以修改。

@ExcelParam(value = "file", required = true)

進階匯入功能

有時候,我們可能比較關心物件對應 Excel 的後設資料,例如這個物件是第幾行記錄產生的,這個物件的欄位對應 Excel 第幾列,這個時候我們可以使用 ReadRows<T> 引數接收 Excel。

@PostMapping("/list/rows")
public ReadRows<DemoData> readRows(@ExcelParam ReadRows<DemoData> readRows) {
    return readRows;
}

ReadRows 使用兩個欄位記錄行對映關係與列對映關係。

public class ReadRows<T> {

    private ExcelReadHeadProperty excelReadHeadProperty;

    private List<ReadRow<T>> rows;
}

ExcelReadHeadProperty 是 EasyExcel 自帶的類,表示列對映關係的後設資料。ReadRow 是框架自定義的類,表示行對映關係的後設資料。

看下 ReadRow 定義吧。

public class ReadRow<T> {

    // 行索引,從 0 開始
    private final Integer rowIndex;

    // 行記錄對應物件
    private final T data;
}

使用 ExcelReadHeadProperty 獲取欄位對應列索引的範例程式碼如下。

// 物件欄位名稱 -> 從 0 開始的列索引
Map<String, Integer> fieldColumnIndexMap = readRows.getExcelReadHeadProperty().getHeadMap().values()
        .stream().collect(Collectors.toMap(Head::getFieldName, Head::getColumnIndex));

Excel 匯出

這裡對 Excel 的匯出進行了簡單的支援。將 List<T> 定義為 controller 方法返回值即可。

@ExcelResponse
@GetMapping("/list/download")
public List<DemoData> downloadList() {
    return Arrays.asList(new DemoData(1, "hello", new Date()), new DemoData(2, "excel", new Date()));
}

需要注意的是使用 @ExcelResponse 註解表示響應內容為 Excel 檔案。預設情況,下載的檔名稱為 default.xlxs,寫入到名稱為 Sheet1 的工作表中。如果不滿足需求可以修改。

@ExcelResponse(fileName = "測試檔案", sheetName = "工作表1")

Excel 匯入引數校驗

引數校驗是 Excel 匯入常用的功能,這裡進行了強有力的支援,使用體驗如原生 spring boot 校驗般順滑。

開啟校驗

與 spring boot 原生使用方式一樣,將 @Validated@Valid 註解新增到 @ExcelParam 引數上即可。

@PostMapping("/list/obj")
public List<DemoData> listObj(@ExcelParam @Validated List<DemoData> list) {
    return list;
}

校驗規則定義

Bean Validation 定義校驗規則

預設情況下框架使用 JSR-303 Bean Validation 規範定義的校驗註解校驗,需要手動引入 spring-boot-starter-validation,可通過設定環境變數 easyexcel.validator.default.enable=false 關閉。

@Data
public class DemoData {
    @NotNull(message = "引數不能為空")
    private Integer integer;

    private String string;

    private Date date;
}

另外還可以自定義註解對物件校驗。

... 省略其他元註解
@Constraint(validatedBy = {DemoDataValid.DemoDataValidator.class})
public @interface DemoDataValid {
		... 省略註解屬性
		
    class DemoDataValidator implements ConstraintValidator<DemoDataValid, DemoData> {

        @Override
        public boolean isValid(DemoData value, ConstraintValidatorContext context) {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate("測試物件校驗").addConstraintViolation();
            return false;
        }
    }
}
@DemoDataValid
public class DemoData {
    ... 省略屬性
}

ExcelValidator 介面定義校驗規則

Bean Validation 註解只能校驗單個欄位或物件,如果需要對所有的物件進行校驗,可以實現框架定義的 ExcelValidator 介面,然後將實現定義為 Spring Bean。

這個介面定義如下。

public interface ExcelValidator<T> {
    ExcelValidErrors validate(ReadRows<T> readRows);
}

ExcelValidErrors 用於接收校驗的錯誤資訊,分別使用介面 ExcelValidObjectErrorExcelValidFieldError 介面定義行錯誤資訊和單元格錯誤資訊。

public class ExcelValidErrors {
	// 行錯誤資訊或單元格錯誤資訊列表
    private final List<ExcelValidObjectError> errors;
}

public interface ExcelValidObjectError {
    // 獲取行號,從 1 開始
    Integer getRow();

    // 獲取錯誤訊息
    String getMessage();
}

public interface ExcelValidFieldError extends ExcelValidObjectError {
    // 獲取列,從 1 開始
    Integer getColumn();
}

例如,如果需要對所有的 DemoData 校驗 integer 欄位的值不能重複,可以使用如下的程式碼。

@Component
public class CustomExcelValidator implements ExcelValidator<DemoData> {
    @Override
    public ExcelValidErrors validate(ReadRows<DemoData> readRows) {
        ExcelValidErrors errors = new ExcelValidErrors();

        Map<Integer, List<ReadRow<DemoData>>> group = readRows.getRows().stream()
                .collect(Collectors.groupingBy(item -> item.getData().getInteger()));

        for (Map.Entry<Integer, List<ReadRow<DemoData>>> entry : group.entrySet()) {
            if (entry.getValue().size() > 1) {
                for (ReadRow<DemoData> readRow : entry.getValue()) {
                    errors.addError(new DefaultExcelObjectError(readRow.getRowIndex() + 1, "引數重複"));
                }
            }
        }
        return errors;
    }
}

校驗結果接收

與 Spring MVC 設計類似,這裡也提供了兩種接收校驗結果的方式。

異常捕獲接收校驗結果

開啟校驗後,如果校驗結果中包含錯誤,會將錯誤資訊封裝到 ExcelValidException,並丟擲異常,可以通過全域性異常捕獲的方式收集錯誤資訊。

@RestControllerAdvice
public class GlobalExceptionControllerAdvice {
    @ExceptionHandler(ExcelValidException.class)
    public String handleException(ExcelValidException e) {
        ExcelValidErrors errors = e.getErrors();
        return JSON.toJSONString(errors);
    }
}

controller 方法引數接收校驗結果

如果不想通過異常捕獲的方式接收校驗的錯誤資訊,還可以將錯誤資訊新增到 @ExcelParam 引數的後面,範例程式碼如下。

@PostMapping("/list/obj")
public List<DemoData> listObj(@ExcelParam @Validated List<DemoData> list, ExcelValidErrors errors) {
    if (errors.hasErrors()) {
        String messages = errors.getAllErrors().stream().map(ExcelValidObjectError::getMessage).collect(Collectors.joining(" | "));
        throw new RuntimeException("發現異常:" + messages);
    }
    return list;
}

總結

easyexcel-spring-boot-starter 綜合應用了前面文章介紹的各種 Spring 知識,程式碼量並不大,對實現感興趣的小夥伴可自行查閱程式碼。由於這個框架是把 Excel 中所有的行資料收集到記憶體,因此只適合一些比較簡單的場景。

到此這篇關於Spring Boot專案如何優雅實現Excel匯入與匯出功能的文章就介紹到這了,更多相關SpringBoot實現Excel匯入匯出內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com