寫一個easyexcel的工具類

1. 前言

最近阿里開源的excel讀寫項目EasyExcel又火了起來 . 原來是項目又開始維護了, 從1.x 更新到2.x 了 , 並且迭代迅速 , 目前已經更新到 2.1.0-beta3 版本.html

關於easyexcel , 其主要目的爲下降讀取excel時的內存損耗 , 簡化讀寫excel的api .java

同時2.x版本提供了不少新功能 , 具體你們能夠直接參考官方說明吧 , github文檔上寫十分詳細 (中文) , 這裏就不傳播一些沒啥必要的二三手知識了 , 並且目前該項目還在不停地迭代 , 給contributor一個star也是頗有必要的 . :smile:git

官方地址 : easyexcel倉庫地址 , easyexcel官網github


這裏我基於easyexcel 2.0.5版本簡單封裝了一個web讀寫excel的工具類 , 主要封裝了以下功能 :web

  • 經過註解自定義LocalDateTime的讀寫格式shell

  • 經過註解自定義枚舉類型的讀寫格式數據庫

  • 自定義BaseExcelListener抽象類封裝了經常使用的數據處理邏輯 , 以及補充讀取excel過程當中讀取發生錯誤被跳過的行號記錄 .api

  • 封裝了web的讀寫excel操做數組

  • ... (後續會持續更新封裝easyexcel提供的api , 但願可讓easyexcel更easy . )app

下面列舉主要功能以及相關示例 , 能夠直接看源碼 , 每一個方法都有寫完整的註釋 , 若是以爲寫得還湊合能看的話 , 給我這個剛畢業沒多久的小菜雞點個star唄 :smile: .

附上源碼地址 : github.com/aStudyMachi…

2. 主要功能

2.1 創建excel表每行數據與Java模型的映射

easyexcel讀寫excel能夠基於java 模型的方式 , 也可使用List<List<String>> 的方式讀寫excel , 這裏我讀寫操做使用基於java模型的方式 , 經過java類的屬性與excel每一列的數據進行對應

關鍵註解 : @ExcelProperty , @EnumFormat , @LocalDateTimeFormat

具體如何使用註解創建java模型與Excel表數據的映射能夠參考 com.wukun.module.easyexcel.pojo下的兩個java模型類Order 類與User

/** * @author WuKun * @since 2019/10/09 */
@Data
@AllArgsConstructor
@NoArgsConstructor //必需要保證無參構造方法存在,不然會報初始化對象失敗
public class User {

    /** * {@code @ExcelIgnore} 用於標識該字段不用作excel讀寫過程當中的數據轉換 */
    @ExcelIgnore
    private Integer userId;

    /** * <pre> * {@code @ExcelIgnore} 中的屬性 不建議 index 和 name 同時用 * * 要麼一個對象統一隻用index表示列號, * 例如 : {@code @ExcelProperty(index = 0)} * * 要麼一個對象統一隻用value去匹配列名 * 例如 : {@code @ExcelProperty("姓名")} * * 用名字去匹配,這裏須要注意,若是名字重複,會致使只有一個字段讀取到數據 * </pre> */
    @ExcelProperty("姓名")
    private String name;

    @ExcelProperty("年齡")
    private Integer age;

    @ExcelProperty("地址")
    private String address;

    /** * <pre> * {@code @EnumFormat} 註解 : * 做用 : 與 {@code @ExcelProperty(converter = EnumExcelConverter.class)} 搭配使用 * 轉換java枚舉與excel中指定的內容 * 屬性 : * - value : 要轉換的枚舉類class對象 * - fromExcel : 指定excel中用戶輸入的枚舉值的名字的字符串形式,與toJavaEnum中指定的枚舉值一一對應 * 如下面的示例來講,fromExcel指定的 "男" 對應 toJavaEnum中的 "MAN" , * 當excel中該列讀取到"男" 這個字符串時,會自動轉化爲枚舉{@code GenderEnum.MAN}, * 同理在寫excel時,若是該字段爲{@code GenderEnum.MAN} 時, 寫到excel時則轉化爲 "男" * - toJavaEnum : 如上所述 * * 注意 : fromExcel 與 toJavaEnum 這兩個屬性必須同時使用, 並且兩個屬性的字符串的數組長度必須相同, * 若兩個屬性都不指定 , 則默認 枚舉值名字符串轉化爲對應的枚舉 例如: "MAN" <--> {@code GenderEnum.MAN} * </pre> */
    @EnumFormat(value = GenderEnum.class,
            fromExcel = {"男", "女"},
            toJavaEnum = {"MAN", "WOMAN"}) // "男" <--> GenderEnum.MAN ; "女" <--> GenderEnum.WOMAN
    @ExcelProperty(value = "性別", converter = EnumExcelConverter.class)
    private GenderEnum gender;

    /** * <pre> * {@code @LocalDateTimeFormat} 註解 * 做用: 與 {@code @ExcelProperty(converter = LocalDateTimeExcelConverter.class)} 搭配使用, * 指定導入導出的時間格式. * 屬性 : * - value : 日期格式字符串 * </pre> */
    @ExcelProperty(value = "生日", converter = LocalDateTimeExcelConverter.class)
    @LocalDateTimeFormat("yyyy-MM-dd HH:mm:ss")
    private LocalDateTime birthday;
}

複製代碼

2.2 導出excel

2.2.1 相關API介紹

web導出excel 根據03 / 07版本分爲兩個不一樣的方法 ,分別爲EasyExcelUtil類中如下兩個方法 :

  • 導出03版本 : exportExcel2003Format(EasyExcelParams excelParams)

  • 導出07版本 : exportExcel2007Format(EasyExcelParams excelParams)

EasyExcelParams是使用EasyExcel導出excel須要設置的相關參數 , 包括須要導出的List<T>數據以及對應的Java模型 , 使用時根據實際狀況設置相應的參數便可.

/** * @author WuKun * @since 2019/10/14 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class EasyExcelParams implements Serializable {


    /** * excel文件名(不帶拓展名) */
    private String excelNameWithoutExt;
    /** * sheet名稱 */
    private String sheetName;

    /** * 數據 */
    private List data;

    /** * 數據模型類型 */
    private Class dataModelClazz;

    /** * 響應 */
    private HttpServletResponse response;


    public EasyExcelParams() {
    }

    /** * 檢查不容許爲空的屬性 * * @return this */
    public EasyExcelParams checkValid() {
        Assert.isTrue(ObjectUtils.allNotNull(excelNameWithoutExt, data, dataModelClazz, response), "導出excel參數不合法!");
        return this;
    }
}
複製代碼

2.2.2 導出excel示例

/** * 使用EasyExcelUtils 導出Excel 2007 * * @param response HttpServletResponse * @throws Exception exception */
@GetMapping("/easy2007")
public void easy2007(HttpServletResponse response) throws Exception {
    initData();

    //設置參數
    EasyExcelParams params = new EasyExcelParams().setResponse(response)
        .setExcelNameWithoutExt("Order(xlsx)")
        .setSheetName("第一張sheet")
        .setData(data)
        .setDataModelClazz(Order.class)
        .checkValid();

    long begin = System.currentTimeMillis();
    EasyExcelUtil.exportExcel2007(params);
    long end = System.currentTimeMillis();

    log.info("-----EasyExcelUtils : 導出成功,導出excel花費時間爲 : " + ((end - begin) / 1000) + "秒");
}

private void initData() {
    if (CollectionUtils.isEmpty(data)) {
        for (int i = 0; i < 60000; i++) {
            data.add(new Order().setPrice(BigDecimal.valueOf(11.11))
                     .setCreateTime(LocalDateTime.now()).setGoodsName("香蕉")
                     .setOrderId(i)
                     .setNum(11)
                     .setOrderStatus(OrderStatusEnum.PAYED));
        }
    }
}
複製代碼

2.3 讀取excel

2.3.1 相關API介紹

  • 讀取excel時用到的是EasyExcelUtilsreadExcel方法 ;
/** * 讀取 Excel(支持單個model的多個sheet) * * @param excel 文件 * @param rowModel 實體類映射 * @param listener 用於讀取excel的listener */
public static void readExcel(MultipartFile excel, Class rowModel, BaseExcelListener listener) {
    ExcelReader reader = getReader(excel, rowModel, listener);
    try {
        Assert.notNull(reader, "導入Excel失敗!");
        Integer totalSheetCount = reader.getSheets().size();
        for (Integer i = 0; i < totalSheetCount; i++) {
            reader.read(EasyExcel.readSheet(i).build());
        }
    } finally {
        // 這裏千萬別忘記關閉,讀的時候會建立臨時文件,到時磁盤會崩的
        Optional.ofNullable(reader).ifPresent(ExcelReader::finish);
    }
}
複製代碼
  • easyexcel的讀取操做須要自建一個類繼承AnalysisEventListener抽象類 , 這裏我建立BaseExcelListener類繼承並重寫讀取excel的相關方法 , 每一個方法的具體做用可直接查看方法頭部註釋 , 使用時直接建立一個listener類繼承BaseExcelListener便可 , 若是默認的BaseExcelListener不知足需求 , 也能夠直接自定義一個Listener 類繼承 BaseExcelListener並重寫相應方法.
/** * @author WuKun * @since 2019-10-10 * <p> * 因爲在實際中可能會根據不一樣的業務場景須要的讀取到的不一樣的excel表的數據進行不一樣操做, * 因此這裏將{@link BaseExcelListener}做爲全部listener的父類,根據讀取不一樣的java模型自定義一個listener類繼承{@link BaseExcelListener}, * 根據不一樣的業務場景選擇性對如下方法進行重寫,具體如{@link OrderListener}所示 * </p> * * <p>若是默認實現的方法不知足業務,則直接自定義一個listener繼承{@link BaseExcelListener},重寫一遍方法便可.</p> */
@Slf4j
public abstract class BaseExcelListener<Model> extends AnalysisEventListener<Model> {

    /** * 每隔N條存儲數據庫,實際使用中能夠3000條,而後清理list ,方便內存回收 */
    private static final int BATCH_COUNT = 3000;

    /** * 自定義用於暫時存儲data。 * 能夠經過實例獲取該值 * 能夠指定AnalysisEventListener的泛型來肯定List的存儲類型 */
    @Getter
    private List<Model> data = new ArrayList<>();

    /** * 讀取時拋出異常是否繼續讀取,默認true,表示跳過錯誤行繼續讀取 */
    @Setter
    private boolean continueAfterThrowing = true;


    /** * 讀取過程當中發生異常被跳過的行數記錄 * String 爲 sheetNo * List<Integer> 爲 錯誤的行數列表 */
    @Getter
    private Map<String, List<Integer>> errRowsMap = new HashMap<>();

    /** * 每解析一行會回調invoke()方法。 * 若是當前行無數據,該方法不會執行, * 也就是說若是導入的的excel表無數據,該方法不會執行, * 不須要對上傳的Excel表進行數據非空判斷 * * @param object 當前讀取到的行數據對應的java模型對象 * @param context 定義了獲取讀取excel相關屬性的方法 */
    @Override
    public void invoke(Model object, AnalysisContext context) {
        log.info("解析到一條數據:{}", object);
        // 數據存儲到list,供批量處理,或後續本身業務邏輯處理。
        data.add(object);

        //若是continueAfterThrowing 爲false 時保證數據插入的一致性
        if (data.size() >= BATCH_COUNT && continueAfterThrowing) {
            saveData();
            data.clear();
        }
    }

    /** * 入庫,繼承該類後實現該方法便可 */
    abstract void saveData();

    /** * 解析監聽器 * 每一個sheet解析結束會執行該方法 */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        saveData();
        log.info("/*------- 當前sheet讀取完畢,sheetNo : {} , 讀取錯誤的行號列表 : {} -------*/",
                getCurrentSheetNo(context), JSON.toJSONString(errRowsMap));
        data.clear();//解析結束銷燬不用的資源
    }

    /** * 在轉換異常 獲取其餘異常下會調用本接口。拋出異常則中止讀取。若是這裏不拋出異常則 繼續讀取下一行。 * 若是不重寫該方法,默認拋出異常,中止讀取 * * @param exception exception * @param context context */
    @Override
    public void onException(Exception exception, AnalysisContext context) {
        if (!continueAfterThrowing) {
            throw new IllegalArgumentException(exception);
        }

        Integer sheetNo = getCurrentSheetNo(context);
        Integer rowIndex = context.readRowHolder().getRowIndex();
        log.error("/*------- 讀取發生錯誤! 錯誤SheetNo:{},錯誤行號:{} -------*/ ", sheetNo, rowIndex, exception);

        List<Integer> errRowNumList = errRowsMap.get(String.valueOf(sheetNo));
        if (Objects.isNull(errRowNumList)) {
            errRowNumList = new ArrayList<>();
            errRowNumList.add(rowIndex);
            errRowsMap.put(String.valueOf(sheetNo), errRowNumList);
        } else {
            errRowNumList.add(rowIndex);
        }
    }

    /** * 獲取當前讀取的sheet no * * @param context * @return */
    private Integer getCurrentSheetNo(AnalysisContext context) {
        return context.readSheetHolder().getSheetNo();
    }

}
複製代碼
  • 讀取時不區分03或07版本 , 底層會自動判斷 ;

2.3.2 讀取excel示例

  1. 自定義一個listener類繼承BaseExcelListener
package com.luwei.module.easyexcel.listener;

import com.wukun.module.easyexcel.pojo.User;
import lombok.extern.slf4j.Slf4j;

/** * @author WuKun * @since 2019/10/10 */
@Slf4j
public class UserListener extends BaseExcelListener<User> {
    /** * 這裏須要注意入庫使用到的Service或者DAO層須要使用到的相關方法時, * 不要經過Spring 使用{@code @Autowired}注入,同時該Listener也不要交由Spring IOC進行管理 * 直接經過構造方法傳入相關`xxxService` 或者 `xxxMapper` */
    private UserService userService;

    public UserListener(UserService userService) {
        this.userService = userService;
    }
    
    @Override
    void saveData() {
        // 批量插入數據
        userService.saveBatchUsers(this.getData())
        log.info("/*------- 寫入數據 -------*/");
    }
}
複製代碼
  1. 調用工具方法
@Autowired
private UserService userService;

/** * 讀取測試 * * @param excel excel文件 */
@PostMapping("/readExcel")
public void readExcel(@RequestParam MultipartFile excel) {
    EasyExcelUtil.readExcel(excel, User.class, new UserListener(userService));
}
複製代碼

3. 注意事項

  • java模型必需要保證無參構造方法存在 , 不然會在讀寫excel時報沒法初始化java模型對象的異常

  • 使用java模型讀取excel時不能對Java模型使用@Accessors(chain = true)註解, 會致使數據沒法轉換 (easyexcel 2.x的API該問題已解決)

  • sheetNo 從 0開始 , 行號不包括表頭 , 例如log中打印的是第9行, 實際在excel中對應的是第10行

    2019-10-20 15:34:57.236  INFO 38012 --- [nio-8081-exec-8] c.l.e.listener.BaseExcelListener         : /*------- 當前sheet讀取完畢,sheetNo : 1 , 讀取錯誤的行號列表 : {"1":[9]} -------*/
    複製代碼

    image.png


做者 : 一臺學習機

廣州蘆葦科技Java開發團隊

蘆葦科技-廣州專業互聯網軟件服務公司

抓住每一處細節 ,創造每個美好

關注咱們的公衆號,瞭解更多

想和咱們一塊兒奮鬥嗎?lagou搜索「 蘆葦科技 」或者投放簡歷到 server@talkmoney.cn 加入咱們吧

關注咱們,你的評論和點贊對咱們最大的支持

相關文章
相關標籤/搜索