easypoi導入Excel最佳實踐


前言

本文原文鏈接地址:http://nullpointer.pw/easypoi%E5%AF%BC%E5%85%A5Excel%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5.html

一直以來,使用EasyPOI做了不少導入導出的需求,但是每次做完都是臨時去看官方文檔現學現用,正巧最近朋友遇到這么個需求,用到了EasyPOI來完成導入,我也正好整理整理EasyPOI的導入用法。

本文原文鏈接地址:http://nullpointer.pw/easypoi%E5%AF%BC%E5%85%A5Excel%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5.html

需求是這樣的:現在要在后台導入用戶的簡歷,簡歷的格式是這樣子的:

一個人有多個屬性,某些屬性如申請職位、薪資是單一屬性,即只會有一個值;某些屬性如工作經歷、教育經歷、獲獎情況是一組屬性,可能會有多組值。現在要將這批簡歷數據導入到庫中。

零、文件准備:

  • 示例Excel以及示例Excel2

  • 加入 EasyPOI 的依賴

    <dependency>
      <groupId>cn.afterturn</groupId>
      <artifactId>easypoi-web</artifactId>
      <version>3.2.0</version>
    </dependency>
    

一、定義EasyPOI導入實體類

首先定義一個 EasyPOI 實體類,單個屬性使用 @Excel注解聲明,name 屬性需要與Excel中的表頭保持一致,比如 姓名*中的 * 號就不能省略掉。一對多關系使用 @Collection 注解聲明,name 是最上方的表頭,對應的集合元素類型需要另外定義一個對象,這里因為篇幅問題,只展示一個工作經歷以及教育經歷對應的集合對象。

@Data
public class TalentUserInputEntity{

    @Excel(name = "姓名*")
    private String name;

    @Excel(name = "性別*")
    private Integer gender;

    @Excel(name = "手機號*")
    private String phone;

    @Excel(name = "開始工作時間*")
    private Date workTime;

    @Excel(name = "民族*")
    private String national;

    @Excel(name = "語言水平*")
    private String languageProficiency;

    @Excel(name = "出生日期*")
    private Date birth;

    @Excel(name = "職位*")
    private String jobsName;

    @Excel(name = "職位類型*")
    private String categoryName;

    @Excel(name = "薪資*")
    private Integer salary;

    @Excel(name = "工作地點*")
    private String workArea;

    @ExcelCollection(name = "工作經歷*")
    private List<ExperienceInputEntity> experienceList;

    @ExcelCollection(name = "教育經歷*")
    private List<EducationInputEntity> educationList;

    @ExcelCollection(name = "獲獎情況")
    private List<AwardsInputEntity> awardList;

    @ExcelCollection(name = "技能證書")
    private List<PunishmentInputEntity> punishmentList;

    @Excel(name = "特長")
    private String specialty;
}

// 工作經歷對象
@Data
public class ExperienceInputEntity {
    @Excel(name = "公司名稱*")
    private String companyName;

    @Excel(name = "所在行業*")
    private String industry;

    @Excel(name = "開始時間*")
    private Date beginTime;

    @Excel(name = "結束時間*")
    private Date finishTime;

    @Excel(name = "職位名稱*")
    private String jobTitle;

    @Excel(name = "所屬部門*")
    private String department;

    @Excel(name = "工作內容*")
    private String description;
}
// 教育經歷對象
@Data
public class EducationInputEntity {
    @Excel(name = "學校*")
    private String schoolName;

    @Excel(name = "學歷*")
    private Integer record;

    @Excel(name = "開始年份*")
    private Date beginTime;

    @Excel(name = "畢業年份*")
    private Date finishTime;

    @Excel(name = "專業*")
    private String profession;
}
// 省略其他

二、EasyPOI基礎導入

這里為方便演示,直接將導入結果轉成JSON打印輸出。

@PostMapping("/upload")
public Boolean upload(@RequestParam("file") MultipartFile multipartFile) throws Exception {
  ImportParams params = new ImportParams();
  params.setHeadRows(2);
  // params.setTitleRows(0);
  List<TalentUserInputEntity> result = ExcelImportUtil.importExcel(multipartFile.getInputStream(),
                                                                   TalentUserInputEntity.class, params);
  System.out.println(JSONUtil.toJsonStr(result));
  return true;
}

這里需要注意表頭的行數設置一定要正確!否則集合數據將無法讀取,可以通過WPS或者office查看實際表頭所占用的行數,一定要區分表頭與標題的區別,表頭是列名稱,標題是表頭上面的文字,本文示例文件中沒有標題,所以setTitleRows為0

三、值替換

使用 postman 或者 Talend API Tester 等工具進行上傳 示例文件.xlsx,結果控制台輸出了異常,異常如下:

java.lang.NumberFormatException: For input string: "男"
...
cn.afterturn.easypoi.exception.excel.ExcelImportException: Excel 值獲取失敗

原因是因為數據庫中性別字段類型為 Integer 類型的,所以導入對象也設置成了 Integer,而 Excel 中填寫的是男/女漢字,自然就出錯了,這里就需要使用 easypoi 的 replace 方式來替換最終值,修改 gender 字段上的注解為:

// replace格式為 "替換前的值_替換后的值"
@Excel(name = "性別*", replace = {"男_0", "女_1"})
private Integer gender;

同理,薪資類型和教育經歷中的學歷需要替換需要的值

@Excel(name = "薪資*", replace = {"3K以下_1", "3K-5K_2", "5K-10K_3", "10K-20K_4", "20K-50K_5", "50K以上_6"})
private Integer salary;

@Excel(name = "學歷*", replace ={"初中及以下_1","中專_2","高中_3","大專_4","本科_5","碩士_6","博士_7"})
private Integer record;

再重新嘗試導入,控制台輸出了導入的JSON內容

[
    {
        "experienceList": [
            {
                "finishTime": 1571673600000,
                "companyName": "科技公司1",
                "jobTitle": "運營",
                "description": "運營方案處理",
                "industry": "互聯網",
                "beginTime": 1546358400000,
                "department": "運營部"
            },
            {
                "finishTime": 1571673600000,
                "companyName": "財務公司",
                "jobTitle": "會計",
                "description": "審計",
                "industry": "財務",
                "beginTime": 1574179200000,
                "department": "會計部"
            }
        ],
        "gender": 0,
        "languageProficiency": "英語四級",
        "jobsName": "銷售",
        "educationList": [
            {
                "profession": "計算機",
                "finishTime": 1530374400000,
                "record": 5,
                "beginTime": 1409500800000,
                "schoolName": "山東大學"
            },
            {
                "profession": "計算機",
                "finishTime": 1593532800000,
                "record": 6,
                "beginTime": 1409500800000,
                "schoolName": "山東大學"
            }
        ],
        "birth": 851097600000,
        "salary": 4,
        "workTime": 1549900800000,
        "categoryName": "銷售",
        "phone": "13122223333",
        "workArea": "浙江省金華市義烏市",
        "name": "張無忌",
        "national": "漢族",
        "punishmentList": [
            {},
            {}
        ],
        "awardList": [
            {
                "date": 1530374400000,
                "name": "國家獎學金",
                "description": "國家一級獎學金"
            },
            {}
        ]
    },
    {
        "specialty": "特長就是太多特長",
        "experienceList": [
            {
                "finishTime": 1571673600000,
                "companyName": "科技公司1",
                "jobTitle": "java開發",
                "description": "code",
                "industry": "互聯網",
                "beginTime": 1546358400000,
                "department": "開發部門"
            }
        ],
        "gender": 0,
        "languageProficiency": "英語八級",
        "jobsName": "java",
        "educationList": [
            {
                "profession": "計算機",
                "finishTime": 1530374400000,
                "record": 5,
                "beginTime": 1409500800000,
                "schoolName": "安徽大學"
            }
        ],
        "birth": 851097600000,
        "salary": 4,
        "workTime": 1549900800000,
        "categoryName": "開發",
        "phone": "18311111111",
        "workArea": "浙江省金華市義烏市",
        "name": "張小凡",
        "national": "漢族",
        "punishmentList": [
            {
                "date": 1530374400000,
                "description": "技能沒有"
            }
        ],
        "awardList": [
            {
                "date": 1530374400000,
                "name": "國家獎學金",
                "description": "國家一級獎學金"
            }
        ]
    },
    // 省略其他...
]

四、導入之基礎校驗

現在產品需要對導入的Excel進行校驗,不合法的Excel不允許入庫,需要返回具體的錯誤信息給前端,提示給用戶,錯誤信息中需要包含行號以及對應的錯誤。

因為 EasyPOI 支持 Hibernate Validator ,所以直接使用就可以了,因為要將錯誤信息以及錯誤行號返回,所以需要用到 EasyPOI 的高級用法,實現 IExcelDataModelIExcelModel接口,IExcelDataModel負責設置行號,IExcelModel 負責設置錯誤信息。

修改導入實體類,增加字段 rowNumerrorMsg以及增加校驗注解。

如果使用到了 @Pattern 注解,則字段類型必須是 String 類型,否則會拋出異常,本文中的原 Integer 類型的 gender 修改成為 String 類型的 genderStrrecord 字段也修改為了 String 類型的 recordStr等等。同理如果校驗 Date 類型字段,先將類型改成String,正則表達式參考下文寫法。

這里需要注意,如果@Excel注解中設置了 replace 屬性,則Hibernate Validator 校驗的是替換后的值

@Data
public class TalentUserInputEntity implements IExcelDataModel, IExcelModel {
    // 時間格式校驗正則
    public static final String DATE_REGEXP = "(Mon|Tue|Wed|Thu|Fri|Sat|Sun)( )(Dec|Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov)( )\\d{2}( )(00:00:00)( )(CST)( )\\d{4}";

    /**
     * 行號
     */
    private int rowNum;

    /**
     * 錯誤消息
     */
    private String errorMsg;

    @Excel(name = "姓名*")
    @NotBlank(message = "[姓名]不能為空")
    private String name;

    @Excel(name = "性別*", replace = {"男_0", "女_1"})
    @Pattern(regexp = "[01]", message = "性別錯誤")
    private String genderStr;

    @Excel(name = "手機號*")
    private String phone;

    @Excel(name = "開始工作時間*")
    @Pattern(regexp = DATE_REGEXP, message = "[開始工作時間]時間格式錯誤")
    private String workTimeStr;

    @Excel(name = "民族*")
    @NotBlank(message = "[民族]不能為空")
    private String national;

    @Excel(name = "語言水平*")
    @NotBlank(message = "[語言水平]不能為空")
    private String languageProficiency;

    @Excel(name = "出生日期*")
    @Pattern(regexp = DATE_REGEXP, message = "[出生日期]時間格式錯誤")
    private String birthStr;

    @Excel(name = "職位*")
    @NotBlank(message = "[職位]不能為空")
    private String jobsName;

    @Excel(name = "職位類型*")
    @NotBlank(message = "[職位類型]不能為空")
    private String categoryName;

    @Excel(name = "薪資*", replace = {"3K以下_1", "3K-5K_2", "5K-10K_3", "10K-20K_4", "20K-50K_5", "50K以上_6"})
    @Pattern(regexp = "[123456]", message = "薪資信息錯誤")
    private String salaryStr;

    @Excel(name = "工作地點*")
    @NotBlank(message = "[工作地點]不能為空")
    private String workArea;

    @ExcelCollection(name = "工作經歷*")
    private List<ExperienceInputEntity> experienceList;

    @ExcelCollection(name = "教育經歷*")
    private List<EducationInputEntity> educationList;

    @ExcelCollection(name = "獲獎情況")
    private List<AwardsInputEntity> awardList;

    @ExcelCollection(name = "技能證書")
    private List<PunishmentInputEntity> punishmentList;

    @Excel(name = "特長")
    private String specialty;
}

// 工作經歷
@Data
public class ExperienceInputEntity {
    @Excel(name = "公司名稱*")
    private String companyName;

    @Excel(name = "所在行業*")
    private String industry;

    @Excel(name = "開始時間*")
    @Pattern(regexp = DATE_REGEXP, message = "[工作經歷][開始時間]時間格式錯誤")
    private String beginTimeStr;

    @Excel(name = "結束時間*")
    @Pattern(regexp = DATE_REGEXP, message = "[工作經歷][結束時間]時間格式錯誤")
    private String finishTimeStr;

    @Excel(name = "職位名稱*")
    private String jobTitle;

    @Excel(name = "所屬部門*")
    private String department;

    @Excel(name = "工作內容*")
    private String description;
}

// 教育經歷
@Data
public class EducationInputEntity {

    @Excel(name = "學校*")
    private String schoolName;

    @Excel(name = "學歷*", replace = {"初中及以下_1", "中專_2", "高中_3", "大專_4", "本科_5", "碩士_6", "博士_7"})
    @Pattern(regexp = "[1234567]", message = "學歷信息錯誤")
    private String recordStr;

    @Excel(name = "開始年份*")
    @Pattern(regexp = DATE_REGEXP, message = "[教育經歷][開始年份]時間格式錯誤")
    private String beginTimeStr;

    @Excel(name = "畢業年份*")
    @Pattern(regexp = DATE_REGEXP, message = "[教育經歷][畢業年份]時間格式錯誤")
    private String finishTimeStr;

    @Excel(name = "專業*")
    private String profession;
}
// 其他省略...

修改完實體類后,修改導入處的代碼

@PostMapping("/upload")
public Boolean upload(@RequestParam("file") MultipartFile multipartFile) throws Exception {
  ImportParams params = new ImportParams();
  // 表頭設置為2行
  params.setHeadRows(2);
  // 標題行設置為0行,默認是0,可以不設置
  params.setTitleRows(0);
  // 開啟Excel校驗
  params.setNeedVerfiy(true);
  ExcelImportResult<TalentUserInputEntity> result = ExcelImportUtil.importExcelMore(multipartFile.getInputStream(),
                                                                                    TalentUserInputEntity.class, params);
  System.out.println("是否校驗失敗: " + result.isVerfiyFail());
  System.out.println("校驗失敗的集合:" + JSONObject.toJSONString(result.getFailList()));
  System.out.println("校驗通過的集合:" + JSONObject.toJSONString(result.getList()));
  for (TalentUserInputEntity entity : result.getFailList()) {
    String msg = "第" + entity.getRowNum() + "行的錯誤是:" + entity.getErrorMsg();
    System.out.println(msg);
  }
  return true;
}

為了方便測試,我基於正確的Excel另存一份個別字段有誤的 示例Excel2 並上傳,解析結果為:

這里貼出部分校驗失敗集合數據,可以看到第50行和61行是原Excel中錯誤的數據,也已經打印了出來,但是教育經歷中的教育水平也是錯誤的,卻沒被打印出來,查看源碼發現,EasyPOI 對 Collection 中的對象並沒有進行校驗,我們在下文中解決。

五、導入值自定義校驗之重復值校驗

上文所作的校驗只是一些基本的校驗,可能會有諸如Excel中重復行校驗,Excel中數據與數據庫重復校驗等等。這種校驗就無法通過 Hibernate Validator 來完成,只能寫代碼來實現校驗邏輯了。

首先從簡單的Excel數據與數據庫值重復校驗開始。為了便於演示,就不引入數據庫了,直接Mock一些數據用來判斷是否重復。

@Service
public class MockTalentDataService {
    private static List<TalentUser> talentUsers = new ArrayList<>();
    static {
        TalentUser u1 = new TalentUser(1L, "凌風", "18311342567");
        TalentUser u2 = new TalentUser(2L, "張三", "18512343567");
        TalentUser u3 = new TalentUser(3L, "李四", "18902343267");
        talentUsers.add(u1);
        talentUsers.add(u2);
        talentUsers.add(u3);
    }

    /**
     * 校驗是否重復
     */
    public boolean checkForDuplicates(String name, String phone) {
        // 姓名與手機號相等個數不等於0則為重復
        return talentUsers.stream().anyMatch(e -> e.getName().equals(name) && e.getPhone().equals(phone));
    }
}

其中Mock數據中 ID 為 1 的數據與示例Excel2 中的數據是重復的。
EasyPOI 提供了校驗的接口,這需要我們自己寫一個用於校驗的類。在這個類中,可以對導入時的每一行數據進行校驗,框架通過 ExcelVerifyHandlerResult 對象來判斷是否校驗通過,校驗不通過需要傳遞 ErrorMsg。

@Component
public class TalentImportVerifyHandler implements IExcelVerifyHandler<TalentUserInputEntity> {

    @Resource
    private MockTalentDataService mockTalentDataService;

    @Override
    public ExcelVerifyHandlerResult verifyHandler(TalentUserInputEntity inputEntity) {
        StringJoiner joiner = new StringJoiner(",");
        // 根據姓名與手機號判斷數據是否重復
        String name = inputEntity.getName();
        String phone = inputEntity.getPhone();
        // mock 數據庫
        boolean duplicates = mockTalentDataService.checkForDuplicates(name, phone);
        if (duplicates) {
            joiner.add("數據與數據庫數據重復");
        }
        if (joiner.length() != 0) {
            return new ExcelVerifyHandlerResult(false, joiner.toString());
        }
        return new ExcelVerifyHandlerResult(true);
    }
}

修改校驗處代碼,設置校驗類對象。

@Resource
private TalentImportVerifyHandler talentImportVerifyHandler;

@PostMapping("/upload")
public Boolean upload(@RequestParam("file") MultipartFile multipartFile) throws Exception {
  ImportParams params = new ImportParams();
  // 表頭設置為2行
  params.setHeadRows(2);
  // 標題行設置為0行,默認是0,可以不設置
  params.setTitleRows(0);
  // 開啟Excel校驗
  params.setNeedVerfiy(true);
  params.setVerifyHandler(talentImportVerifyHandler);
  ExcelImportResult<TalentUserInputEntity> result = ExcelImportUtil.importExcelMore(multipartFile.getInputStream(),
                                                                                    TalentUserInputEntity.class, params);
  System.out.println("是否校驗失敗: " + result.isVerfiyFail());
  System.out.println("校驗失敗的集合:" + JSONObject.toJSONString(result.getFailList()));
  System.out.println("校驗通過的集合:" + JSONObject.toJSONString(result.getList()));
  for (TalentUserInputEntity entity : result.getFailList()) {
    int line = entity.getRowNum() + 1;
    String msg = "第" + line + "行的錯誤是:" + entity.getErrorMsg();
    System.out.println(msg);
  }
  return true;
}

上傳 示例Excel2 文件測試,結果輸出:

而第七行的數據正是與Mock中的數據相重復的。

六、導入值自定義校驗之Collection對象校驗

上文中還有一個待解決的問題,就是Collection中的對象添加了Hibernate Validator 注解校驗但是並未生效的問題,現在就來解決一下。上一步中實現了導入對象的校驗類,校驗類會校驗Excel中的每一條數據, 那我是不是可以直接在校驗類中校驗Collection中對象了呢?實踐證明行不通,因為這個校驗類的verifyHandler方法只會被調用一次,所以Collection中只有一條記錄。既然這里行不通的話,就只能對導入結果再進行校驗了。

因為Collection中的數據EasyPOI校驗不到,所以有問題的數據也可能會被框架放到result.getList()中而不是result.getFailList() 中,為了校驗需要將兩個集合合並為一個集合,使用 EasyPOI 自帶的工具類 PoiValidationUtil 進行校驗 Collection 中的對象。

@Resource
private TalentImportVerifyHandler talentImportVerifyHandler;

@PostMapping("/upload")
public Boolean upload(@RequestParam("file") MultipartFile multipartFile) throws Exception {
  ImportParams params = new ImportParams();
  // 表頭設置為2行
  params.setHeadRows(2);
  // 標題行設置為0行,默認是0,可以不設置
  params.setTitleRows(0);
  // 開啟Excel校驗
  params.setNeedVerfiy(true);
  params.setVerifyHandler(talentImportVerifyHandler);
  ExcelImportResult<TalentUserInputEntity> result = ExcelImportUtil.importExcelMore(multipartFile.getInputStream(),
                                                                                    TalentUserInputEntity.class, params);
  System.out.println("是否校驗失敗: " + result.isVerfiyFail());
  System.out.println("校驗失敗的集合:" + JSONObject.toJSONString(result.getFailList()));
  System.out.println("校驗通過的集合:" + JSONObject.toJSONString(result.getList()));

  // 合並結果集
  List<TalentUserInputEntity> resultList = new ArrayList<>();
  resultList.addAll(result.getFailList());
  resultList.addAll(result.getList());
  for (TalentUserInputEntity inputEntity : resultList) {
    StringJoiner joiner = new StringJoiner(",");
    joiner.add(inputEntity.getErrorMsg());
    // 校驗Collection的元素
    inputEntity.getExperienceList().forEach(e -> verify(joiner, e));
    inputEntity.getEducationList().forEach(e -> verify(joiner, e));
    inputEntity.getAwardList().forEach(e -> verify(joiner, e));
    inputEntity.getPunishmentList().forEach(e -> verify(joiner, e));
    inputEntity.setErrorMsg(joiner.toString());
  }

  for (TalentUserInputEntity entity : result.getFailList()) {
    int line = entity.getRowNum() + 1;
    String msg = "第" + line + "行的錯誤是:" + entity.getErrorMsg();
    System.out.println(msg);
  }
  return true;
}

private void verify(StringJoiner joiner, Object object) {
  String validationMsg = PoiValidationUtil.validation(object, null);
  if (StringUtils.isNotEmpty(validationMsg)) {
    joiner.add(validationMsg);
  }
}

上傳 示例Excel2 ,結果如下:

七、導入值自定義校驗之Excel重復行校驗

上文中對Excel中數據與數據庫數據進行重復校驗,可有些需求是要求數據庫在入庫前需要對Excel的的重復行進行校驗。這需要在校驗類中完成,但校驗類中並沒有全部行的數據,該如何實現呢?博主的做法是將導入的數據放到 ThreadLocal 中進行暫存,從而達到在校驗類中校驗Excel重復行的目的。ThreadLocal使用注意完之后一定要及時清理!

首先定義什么叫重復行,完全相同的兩行是重復行,本文中設定name 與 phone 相同的行為重復行,由於只需要比較這兩個字段,所以我們需要重寫導入對象的equals與hashCode方法。

@Data
public class TalentUserInputEntity implements IExcelDataModel, IExcelModel {
    // 時間格式校驗正則
    public static final String DATE_REGEXP = "(Mon|Tue|Wed|Thu|Fri|Sat|Sun)( )(Dec|Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov)( )\\d{2}( )(00:00:00)( )(CST)( )\\d{4}";

    /**
     * 行號
     */
    private int rowNum;

    /**
     * 錯誤消息
     */
    private String errorMsg;

    @Excel(name = "姓名*")
    @NotBlank(message = "[姓名]不能為空")
    private String name;

    @Excel(name = "性別*", replace = {"男_0", "女_1"})
    @Pattern(regexp = "[01]", message = "性別錯誤")
    private String genderStr;

    @Excel(name = "手機號*")
    @Pattern(regexp = "[0-9]{11}", message = "手機號不正確")
    private String phone;

    @Excel(name = "開始工作時間*")
    @Pattern(regexp = DATE_REGEXP, message = "[開始工作時間]時間格式錯誤")
    private String workTimeStr;

    @Excel(name = "民族*")
    @NotBlank(message = "[民族]不能為空")
    private String national;

    @Excel(name = "語言水平*")
    @NotBlank(message = "[語言水平]不能為空")
    private String languageProficiency;

    @Excel(name = "出生日期*")
    @Pattern(regexp = DATE_REGEXP, message = "[出生日期]時間格式錯誤")
    private String birthStr;

    @Excel(name = "職位*")
    @NotBlank(message = "[職位]不能為空")
    private String jobsName;

    @Excel(name = "職位類型*")
    @NotBlank(message = "[職位類型]不能為空")
    private String categoryName;

    @Excel(name = "薪資*", replace = {"3K以下_1", "3K-5K_2", "5K-10K_3", "10K-20K_4", "20K-50K_5", "50K以上_6"})
    @Pattern(regexp = "[123456]", message = "薪資信息錯誤")
    private String salaryStr;

    @Excel(name = "工作地點*")
    @NotBlank(message = "[工作地點]不能為空")
    private String workArea;

    @ExcelCollection(name = "工作經歷*")
    private List<ExperienceInputEntity> experienceList;

    @ExcelCollection(name = "教育經歷*")
    private List<EducationInputEntity> educationList;

    @ExcelCollection(name = "獲獎情況")
    private List<AwardsInputEntity> awardList;

    @ExcelCollection(name = "技能證書")
    private List<PunishmentInputEntity> punishmentList;

    @Excel(name = "特長")
    private String specialty;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        TalentUserInputEntity that = (TalentUserInputEntity) o;
        return Objects.equals(name, that.name) &&
                Objects.equals(phone, that.phone);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, phone);
    }
}

修改校驗類代碼,實現重復行的校驗邏輯

@Component
public class TalentImportVerifyHandler implements IExcelVerifyHandler<TalentUserInputEntity> {

    private final ThreadLocal<List<TalentUserInputEntity>> threadLocal = new ThreadLocal<>();

    @Resource
    private MockTalentDataService mockTalentDataService;

    @Override
    public ExcelVerifyHandlerResult verifyHandler(TalentUserInputEntity inputEntity) {
        StringJoiner joiner = new StringJoiner(",");
        // 根據姓名與手機號判斷數據是否重復
        String name = inputEntity.getName();
        String phone = inputEntity.getPhone();
        // mock 數據庫
        boolean duplicates = mockTalentDataService.checkForDuplicates(name, phone);
        if (duplicates) {
            joiner.add("數據與數據庫數據重復");
        }

        List<TalentUserInputEntity> threadLocalVal = threadLocal.get();
        if (threadLocalVal == null) {
            threadLocalVal = new ArrayList<>();
        }

        threadLocalVal.forEach(e -> {
            if (e.equals(inputEntity)) {
                int lineNumber = e.getRowNum() + 1;
                joiner.add("數據與第" + lineNumber + "行重復");
            }
        });
        // 添加本行數據對象到ThreadLocal中
        threadLocalVal.add(inputEntity);
        threadLocal.set(threadLocalVal);
        if (joiner.length() != 0) {
            return new ExcelVerifyHandlerResult(false, joiner.toString());
        }
        return new ExcelVerifyHandlerResult(true);
    }

    public ThreadLocal<List<TalentUserInputEntity>> getThreadLocal() {
        return threadLocal;
    }
}

由於校驗類中使用了ThreadLocal,因此需要及時釋放,修改導入處的代碼。

@Resource
private TalentImportVerifyHandler talentImportVerifyHandler;

@PostMapping("/upload")
public Boolean upload(@RequestParam("file") MultipartFile multipartFile) throws Exception {
  ExcelImportResult<TalentUserInputEntity> result;
  try {
    ImportParams params = new ImportParams();
    // 表頭設置為2行
    params.setHeadRows(2);
    // 標題行設置為0行,默認是0,可以不設置
    params.setTitleRows(0);
    // 開啟Excel校驗
    params.setNeedVerfiy(true);
    params.setVerifyHandler(talentImportVerifyHandler);
    result = ExcelImportUtil.importExcelMore(multipartFile.getInputStream(),
                                             TalentUserInputEntity.class, params);
  } finally {
    // 清除threadLocal 防止內存泄漏
    ThreadLocal<List<TalentUserInputEntity>> threadLocal = talentImportVerifyHandler.getThreadLocal();
    if (threadLocal != null) {
      threadLocal.remove();
    }
  }
  System.out.println("是否校驗失敗: " + result.isVerfiyFail());
  System.out.println("校驗失敗的集合:" + JSONObject.toJSONString(result.getFailList()));
  System.out.println("校驗通過的集合:" + JSONObject.toJSONString(result.getList()));

  // 合並結果集
  List<TalentUserInputEntity> resultList = new ArrayList<>();
  resultList.addAll(result.getFailList());
  resultList.addAll(result.getList());
  for (TalentUserInputEntity inputEntity : resultList) {
    StringJoiner joiner = new StringJoiner(",");
    joiner.add(inputEntity.getErrorMsg());
    // 校驗Collection的元素
    inputEntity.getExperienceList().forEach(e -> verify(joiner, e));
    inputEntity.getEducationList().forEach(e -> verify(joiner, e));
    inputEntity.getAwardList().forEach(e -> verify(joiner, e));
    inputEntity.getPunishmentList().forEach(e -> verify(joiner, e));
    inputEntity.setErrorMsg(joiner.toString());
  }

  for (TalentUserInputEntity entity : result.getFailList()) {
    int line = entity.getRowNum() + 1;
    String msg = "第" + line + "行的錯誤是:" + entity.getErrorMsg();
    System.out.println(msg);
  }
  return true;
}

private void verify(StringJoiner joiner, Object object) {
  String validationMsg = PoiValidationUtil.validation(object, null);
  if (StringUtils.isNotEmpty(validationMsg)) {
    joiner.add(validationMsg);
  }
}

導入示例Excel2,結果如下:

至此,我們就完成了導入的大部分需求。

結語

這篇博客花費了我不少時間來寫,文中的代碼也上傳到了GitHub上,並且在寫文章的同時,學習了一下Git tag 的使用,讀者可以在 GitHub 的分支切換可以切換到不同tag看代碼。

本文原文鏈接地址:http://nullpointer.pw/easypoi%E5%AF%BC%E5%85%A5Excel%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM