easyExcel+validation+正则实现excel导入校验

背景:导入excel的时候,除了数据库校验数据以外,还会碰到数据格式校验,如果直接报错,显得客户体验很不好,我想到的做法是,导入正确的数据,错误的数据重新导出,并在后面加上错误信息。
环境准备:
spring:springboot2.X
easyExcel:2.1.4
lombok(非必须)
easyExcel官网:https://alibaba-easyexcel.github.io/index.html

1、pom文件引入easyExcel
com.alibaba easyexcel 2.1.4

  1. <dependency>
  2. <groupId>org.apache.poi</groupId>
  3. <artifactId>poi</artifactId>
  4. <version>3.17</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.apache.poi</groupId>
  8. <artifactId>poi-ooxml</artifactId>
  9. <version>3.17</version>
  10. </dependency>
  11. <dependency>
  12. <groupId>cglib</groupId>
  13. <artifactId>cglib</artifactId>
  14. <version>3.1</version>
  15. </dependency>
  16. <dependency>
  17. <groupId>org.apache.poi</groupId>
  18. <artifactId>ooxml-schemas</artifactId>
  19. <version>1.1</version>
  20. </dependency>

2、通过easyExcel官方文档我们知道,easyExcel有一个监听器,用来读取excel数据,并且监听器不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
监听器实现如下:

  1. import com.alibaba.excel.annotation.ExcelProperty;
  2. import com.alibaba.excel.context.AnalysisContext;
  3. import com.alibaba.excel.event.AnalysisEventListener;
  4. import com.alibaba.excel.exception.ExcelAnalysisException;
  5. import lombok.Data;
  6. import org.apache.commons.lang3.StringUtils;
  7. import java.lang.reflect.Field;
  8. import java.util.*;
  9. /**
  10. * @author zhy
  11. * @title: EasyExcelListener
  12. * @projectName cec-moutai-bd-display
  13. * @description: easyExcel监听器
  14. * @date 2019/12/2318:28
  15. */
  16. @Data
  17. public class EasyExcelListener <T> extends AnalysisEventListener<T> {
  18. //成功结果集
  19. private List<ExcelImportSucObjectDto> successList = new ArrayList<>();
  20. //失败结果集
  21. private List<ExcelImportErrObjectDto> errList = new ArrayList<>();
  22. //处理逻辑service
  23. private ExcelCheckManager excelCheckManager;
  24. private List<T> list = new ArrayList<>();
  25. //excel对象的反射类
  26. private Class<T> clazz;
  27. public EasyExcelListener(ExcelCheckManager excelCheckManager){
  28. this.excelCheckManager = excelCheckManager;
  29. }
  30. public EasyExcelListener(ExcelCheckManager excelCheckManager,Class<T> clazz){
  31. this.excelCheckManager = excelCheckManager;
  32. this.clazz = clazz;
  33. }
  34. @Override
  35. public void invoke(T t, AnalysisContext analysisContext) {
  36. String errMsg;
  37. try {
  38. //根据excel数据实体中的javax.validation + 正则表达式来校验excel数据
  39. errMsg = EasyExcelValiHelper.validateEntity(t);
  40. } catch (NoSuchFieldException e) {
  41. errMsg = "解析数据出错";
  42. e.printStackTrace();
  43. }
  44. if (!StringUtils.isBlank(errMsg)){
  45. ExcelImportErrObjectDto excelImportErrObjectDto = new ExcelImportErrObjectDto(t, errMsg);
  46. errList.add(excelImportErrObjectDto);
  47. }else{
  48. list.add(t);
  49. }
  50. //每1000条处理一次
  51. if (list.size() > 1000){
  52. //校验
  53. ExcelImportResult result = excelCheckManager.checkImportExcel(list);
  54. successList.addAll(result.getSuccessDtos());
  55. errList.addAll(result.getErrDtos());
  56. list.clear();
  57. }
  58. // 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
  59. if (list.size() >= BATCH_COUNT) {
  60. saveData();
  61. // 存储完成清理 list
  62. list.clear();
  63. }
  64. }
  65. //所有数据解析完成了 都会来调用
  66. @Override
  67. public void doAfterAllAnalysed(AnalysisContext analysisContext) {
  68. ExcelImportResult result = excelCheckManager.checkImportExcel(list);
  69. successList.addAll(result.getSuccessDtos());
  70. errList.addAll(result.getErrDtos());
  71. list.clear();
  72. }
  73. /**
  74. * @description: 校验excel头部格式,必须完全匹配
  75. * @param headMap 传入excel的头部(第一行数据)数据的index,name
  76. * @param context
  77. * @throws
  78. * @return void
  79. * @author zhy
  80. * @date 2019/12/24 19:27
  81. */
  82. @Override
  83. public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
  84. super.invokeHeadMap(headMap, context);
  85. if (clazz != null){
  86. try {
  87. Map<Integer, String> indexNameMap = getIndexNameMap(clazz);
  88. Set<Integer> keySet = indexNameMap.keySet();
  89. for (Integer key : keySet) {
  90. if (StringUtils.isBlank(headMap.get(key))){
  91. throw new ExcelAnalysisException("解析excel出错,请传入正确格式的excel");
  92. }
  93. if (!headMap.get(key).equals(indexNameMap.get(key))){
  94. throw new ExcelAnalysisException("解析excel出错,请传入正确格式的excel");
  95. }
  96. }
  97. } catch (NoSuchFieldException e) {
  98. e.printStackTrace();
  99. }
  100. }
  101. }
  102. /**
  103. * @description: 获取注解里ExcelProperty的value,用作校验excel
  104. * @param clazz
  105. * @throws
  106. * @return java.util.Map<java.lang.Integer,java.lang.String>
  107. * @author zhy
  108. * @date 2019/12/24 19:21
  109. */
  110. public Map<Integer,String> getIndexNameMap(Class clazz) throws NoSuchFieldException {
  111. Map<Integer,String> result = new HashMap<>();
  112. Field field;
  113. Field[] fields=clazz.getDeclaredFields();
  114. for (int i = 0; i <fields.length ; i++) {
  115. field=clazz.getDeclaredField(fields[i].getName());
  116. field.setAccessible(true);
  117. ExcelProperty excelProperty=field.getAnnotation(ExcelProperty.class);
  118. if(excelProperty!=null){
  119. int index = excelProperty.index();
  120. String[] values = excelProperty.value();
  121. StringBuilder value = new StringBuilder();
  122. for (String v : values) {
  123. value.append(v);
  124. }
  125. result.put(index,value.toString());
  126. }
  127. }
  128. return result;
  129. }
  130. }

3、成功结果集对象:ExcelImportSucObjectDto

  1. import lombok.Data;
  2. /**
  3. * @author zhy
  4. * @title: ExcelImportErrObjectDto
  5. * @projectName cec-moutai-bd-display
  6. * @description: excel单条数据导入结果
  7. * @date 2019/12/2318:23
  8. */
  9. @Data
  10. public class ExcelImportSucObjectDto {
  11. private Object object;
  12. }

4、失败结果集对象:ExcelImportErrObjectDto

  1. import lombok.Data;
  2. /**
  3. * @author zhy
  4. * @title: ExcelImportErrObjectDto
  5. * @projectName cec-moutai-bd-display
  6. * @description: excel单条数据导入结果
  7. * @date 2019/12/2318:23
  8. */
  9. @Data
  10. public class ExcelImportErrObjectDto {
  11. private Object object;
  12. private String errMsg;
  13. public ExcelImportErrObjectDto(){}
  14. public ExcelImportErrObjectDto(Object object,String errMsg){
  15. this.object = object;
  16. this.errMsg = errMsg;
  17. }
  18. }

5、业务校验接口:ExcelCheckManager。需要校验excel业务的service接口可以继承这个接口,并在实现类中实现自己的方法,返回的是成功失败的结果集 ExcelImportResult

  1. import java.util.List;
  2. /**
  3. * @author zhy
  4. * @title: ExcelCheckManager
  5. * @projectName cec-moutai-bd-display
  6. * @description: excel校验接口
  7. * @date 2019/12/2317:44
  8. */
  9. public interface ExcelCheckManager<T> {
  10. /**
  11. * @description: 校验方法
  12. * @param objects
  13. * @throws
  14. * @return com.cec.moutai.common.easyexcel.ExcelImportResult
  15. * @author zhy
  16. * @date 2019/12/24 14:57
  17. */
  18. <T> ExcelImportResult checkImportExcel(List<T> objects);
  19. }
  1. import lombok.Data;
  2. import java.util.ArrayList;
  3. import java.util.List;
  4. /**
  5. * @author zhy
  6. * @title: ExcelImportErrObjectDto
  7. * @projectName cec-moutai-bd-display
  8. * @description: excel数据导入结果
  9. * @date 2019/12/2318:23
  10. */
  11. @Data
  12. public class ExcelImportResult {
  13. private List<ExcelImportSucObjectDto> successDtos;
  14. private List<ExcelImportErrObjectDto> errDtos;
  15. public ExcelImportResult(List<ExcelImportSucObjectDto> successDtos,List<ExcelImportErrObjectDto> errDtos){
  16. this.successDtos =successDtos;
  17. this.errDtos = errDtos;
  18. }
  19. public ExcelImportResult(List<ExcelImportErrObjectDto> errDtos){
  20. this.successDtos =new ArrayList<>();
  21. this.errDtos = errDtos;
  22. }
  23. }

6、数据校验逻辑实现:通过正则校验excel数据的功能,后续也可以在此扩展加入其他校验接口。
@ExcelProperty是easyExcel自带的注解
@ColumnWidth也是easyExcel的注解代表单元格宽度
@Length代表的是字符串长度,max代表的是最长允许多长
@Pattern就是正则表达式注解了,regexp代表的是正则表达式,message代表是,没有匹配成功返回的错误信息
关于validation的注解可以参考此篇大佬的博客:https://blog.csdn.net/weixin_42546729/article/details/89364431

可以发现,我这里的接收对象都是String类型的,也正因为是这样,才能通过正则表达式去校验各种格式。
所以在excel类和真正的实体类之间的转换,我是用fastjson的JSON.parseObject来进行转换的。所以需要保证excel类和数据库实体的字段名要保持一致,数据格式,尤其是日期格式,要保持一致。

  1. import com.alibaba.excel.annotation.ExcelProperty;
  2. import com.alibaba.excel.annotation.write.style.ColumnWidth;
  3. import com.cec.moutai.common.easyexcel.ExcelPatternMsg;
  4. import lombok.Data;
  5. import org.hibernate.validator.constraints.Length;
  6. import javax.validation.constraints.DecimalMin;
  7. import javax.validation.constraints.Pattern;
  8. /**
  9. * @author zhy
  10. * @title: CustomerExcelDto
  11. * @projectName cec-moutai-bd-display
  12. * @description: 客户excel对象
  13. * @date 2019/12/2410:35
  14. */
  15. @Data
  16. public class CustomerExcelDto {
  17. @ExcelProperty(index = 0,value = "客户全称")
  18. @ColumnWidth(30)
  19. private String name;
  20. //客户简称
  21. @ExcelProperty(index = 1,value = "客户简称")
  22. @ColumnWidth(30)
  23. @Length(max = 100)
  24. private String shortName;
  25. // 客户编码
  26. @ExcelProperty(index = 2,value = "客户编码(导入必填)")
  27. @ColumnWidth(30)
  28. private String code;
  29. @ExcelProperty(index = 3,value = "客户分类")
  30. private String custclassName;
  31. // 法人
  32. @ExcelProperty(index = 6,value = "法定代表人")
  33. @Length(max = 100)
  34. private String legalbody;
  35. // 营业期限
  36. @ExcelProperty(index = 24,value = "营业期限(导入格式:yyyy-MM-dd)")
  37. @Pattern(regexp = ExcelPatternMsg.DATE2,message = ExcelPatternMsg.DATE2_MSG)
  38. private String businessEndDate;
  39. }

7、正则表达式实现类:ExcelPatternMsg

  1. import java.util.regex.Pattern;
  2. /**
  3. * @author zhy
  4. * @title: ExcelPatternMsg
  5. * @projectName cec-moutai-bd-display
  6. * @description: excel正则表达式,以及错误信息
  7. * @date 2019/12/2614:22
  8. */
  9. public class ExcelPatternMsg {
  10. //只能输入整数或者小数
  11. public static final String DECIMAL = "^[0-9]+\\.{0,1}[0-9]{0,2}$";
  12. public static final String DECIMAL_MSG = "只能输入整数或者小数";
  13. //日期格式 yyyy/MM/dd
  14. public static final String DATE1 = "(([0-9]{3}[1-9]|[0-9]{2}[1-9][0-9]{1}|[0-9]{1}[1-9][0-9]{2}|[1-9][0-9]{3})\\/(((0[13578]|1[02])\\/(0[1-9]|[12][0-9]|3[01]))|"+
  15. "((0[469]|11)\\/(0[1-9]|[12][0-9]|30))|(02\\/(0[1-9]|[1][0-9]|2[0-8]))))|((([0-9]{2})(0[48]|[2468][048]|[13579][26])|"+
  16. "((0[48]|[2468][048]|[3579][26])00))\\/02\\/29)$";
  17. public static final String DATE1_MSG = "输入正确的日期格式:yyyy/MM/dd";
  18. //日期格式 yyyy-MM-dd
  19. public static final String DATE2 = "(([0-9]{3}[1-9]|[0-9]{2}[1-9][0-9]{1}|[0-9]{1}[1-9][0-9]{2}|[1-9][0-9]{3})-(((0[13578]|1[02])-(0[1-9]|[12][0-9]|3[01]))|"+
  20. "((0[469]|11)-(0[1-9]|[12][0-9]|30))|(02-(0[1-9]|[1][0-9]|2[0-8]))))|((([0-9]{2})(0[48]|[2468][048]|[13579][26])|"+
  21. "((0[48]|[2468][048]|[3579][26])00))-02-29)$";
  22. public static final String DATE2_MSG = "输入正确的日期格式:yyyy-MM-dd";
  23. //日期格式 yyyyMMdd
  24. public static final String DATE3 = "(([0-9]{3}[1-9]|[0-9]{2}[1-9][0-9]{1}|[0-9]{1}[1-9][0-9]{2}|[1-9][0-9]{3})(((0[13578]|1[02])(0[1-9]|[12][0-9]|3[01]))|"+
  25. "((0[469]|11)(0[1-9]|[12][0-9]|30))|(02(0[1-9]|[1][0-9]|2[0-8]))))|((([0-9]{2})(0[48]|[2468][048]|[13579][26])|"+
  26. "((0[48]|[2468][048]|[3579][26])00))0229)$";
  27. public static final String DATE3_MSG = "输入正确的日期格式:yyyyMMdd";
  28. //日期格式 yyyy-MM-dd HH:mm:ss
  29. public static final String DATE_TIME1 = "^((([0-9]{3}[1-9]|[0-9]{2}[1-9][0-9]{1}|[0-9]{1}[1-9][0-9]{2}|[1-9][0-9]{3})-(((0[13578]|1[02])-(0[1-9]|[12][0-9]|3[01]))|" +
  30. "((0[469]|11)-(0[1-9]|[12][0-9]|30))|(02-(0[1-9]|[1][0-9]|2[0-8]))))|((([0-9]{2})(0[48]|[2468][048]|[13579][26])|((0[48]|[2468][048]|[3579][26])00))-02-29))\\s+([0-1]?[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$";
  31. public static final String DATE_TIME1_MSG = "输入正确的日期格式:yyyy-MM-dd HH:mm:ss";
  32. //日期格式 yyyy/MM/dd HH:mm:ss
  33. public static final String DATE_TIME2 = "((([0-9]{3}[1-9]|[0-9]{2}[1-9][0-9]{1}|[0-9]{1}[1-9][0-9]{2}|[1-9][0-9]{3})\\/(((0[13578]|1[02])\\/(0[1-9]|[12][0-9]|3[01]))|"+
  34. "((0[469]|11)\\/(0[1-9]|[12][0-9]|30))|(02\\/(0[1-9]|[1][0-9]|2[0-8]))))|((([0-9]{2})(0[48]|[2468][048]|[13579][26])|"+
  35. "((0[48]|[2468][048]|[3579][26])00))\\/02\\/29))\\s([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$";
  36. public static final String DATE_TIME2_MSG = "输入正确的日期格式:yyyy/MM/dd HH:mm:ss";
  37. //日期格式 yyyyMMddHHmmss
  38. public static final String DATE_TIME3 = "((([0-9]{3}[1-9]|[0-9]{2}[1-9][0-9]{1}|[0-9]{1}[1-9][0-9]{2}|[1-9][0-9]{3})(((0[13578]|1[02])(0[1-9]|[12][0-9]|3[01]))|"+
  39. "((0[469]|11)(0[1-9]|[12][0-9]|30))|(02(0[1-9]|[1][0-9]|2[0-8]))))|((([0-9]{2})(0[48]|[2468][048]|[13579][26])|((0[48]|[2468][048]|[3579][26])00))0229))"+
  40. "([0-1][0-9]|2[0-3])([0-5][0-9])([0-5][0-9])$";
  41. public static final String DATE_TIME3_MSG = "输入正确的日期格式:yyyyMMddHHmmss";
  42. //日期格式 yyyyMMddHHmmssSSS
  43. public static final String DATE_TIME4 = "((([0-9]{3}[1-9]|[0-9]{2}[1-9][0-9]{1}|[0-9]{1}[1-9][0-9]{2}|[1-9][0-9]{3})(((0[13578]|1[02])(0[1-9]|[12][0-9]|3[01]))|"+
  44. "((0[469]|11)(0[1-9]|[12][0-9]|30))|(02(0[1-9]|[1][0-9]|2[0-8]))))|((([0-9]{2})(0[48]|[2468][048]|[13579][26])|"+
  45. "((0[48]|[2468][048]|[3579][26])00))0229))([0-1][0-9]|2[0-3])([0-5][0-9])([0-5][0-9])([0-9]{3})$";
  46. public static final String DATE_TIME4_MSG = "输入正确的日期格式:yyyyMMddHHmmssSSS";
  47. //日期格式 yyyyMMdd HH:mm:ss
  48. public static final String DATE_TIME5 = "((([0-9]{3}[1-9]|[0-9]{2}[1-9][0-9]{1}|[0-9]{1}[1-9][0-9]{2}|[1-9][0-9]{3})(((0[13578]|1[02])(0[1-9]|[12][0-9]|3[01]))|"+
  49. "((0[469]|11)(0[1-9]|[12][0-9]|30))|(02(0[1-9]|[1][0-9]|2[0-8]))))|((([0-9]{2})(0[48]|[2468][048]|[13579][26])|"+
  50. "((0[48]|[2468][048]|[3579][26])00))0229))\\s([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$";
  51. public static final String DATE_TIME5_MSG = "输入正确的日期格式:yyyyMMdd HH:mm:ss";
  52. //数字和字母
  53. public static final String NUMBER_LETTER = "^[a-z0-9A-Z]+$";
  54. public static final String NUMBER_LETTER_MSG = "只能输入数字和字母";
  55. //数字
  56. public static final String NUMBER = "^[0-9]*$";
  57. public static final String NUMBER_MSG = "只能输入数字";
  58. public static void main(String[] args) {
  59. System.out.println(Pattern.matches(DATE1,"2020/02/30"));
  60. }
  61. }

EasyExcelValiHelper.validateEntity(t)的写法

  1. import com.alibaba.excel.annotation.ExcelProperty;
  2. import javax.validation.ConstraintViolation;
  3. import javax.validation.Validation;
  4. import javax.validation.Validator;
  5. import javax.validation.groups.Default;
  6. import java.lang.reflect.Field;
  7. import java.util.ArrayList;
  8. import java.util.List;
  9. import java.util.Set;
  10. public class EasyExcelValiHelper {
  11. private static Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
  12. public static <T> String validateEntity(T obj) throws NoSuchFieldException, SecurityException {
  13. StringBuilder result = new StringBuilder();
  14. Set<ConstraintViolation<T>> set = validator.validate(obj, Default.class);
  15. if (set != null && set.size() != 0) {
  16. for (ConstraintViolation<T> cv : set) {
  17. Field declaredField = obj.getClass().getDeclaredField(cv.getPropertyPath().toString());
  18. ExcelProperty annotation = declaredField.getAnnotation(ExcelProperty.class);
  19. result.append(annotation.value()[0]+cv.getMessage()).append(";");
  20. }
  21. }
  22. return result.toString();
  23. }
  24. }

8、控制层入口写法:

  1. @PostMapping(value = "importExcel")
  2. @ApiOperation(value = "客户信息导入")
  3. @SysServiceLog(operateContent = "客户信息导入", operateType = Globals.Log_Type_UPLOAD)
  4. public Result importExcel(HttpServletResponse response,@RequestParam MultipartFile file) throws IOException {
  5. EasyExcelListener easyExcelListener = new EasyExcelListener(customerManager,CustomerExcelDto.class);
  6. EasyExcel.read(file.getInputStream(),CustomerExcelDto.class,easyExcelListener).sheet().doRead();
  7. //错误结果集
  8. List<ExcelImportErrObjectDto> errList = easyExcelListener.getErrList();
  9. if (errList.size() > 0){
  10. List<CustomerCompleteDtoImprotResult> completeDtoImprotResults = errList.stream().map(excelImportErrObjectDto -> {
  11. CustomerCompleteDtoImprotResult customerCompleteDtoImprotResult = BeanUtils.convert(excelImportErrObjectDto.getObject(), CustomerCompleteDtoImprotResult.class);
  12. customerCompleteDtoImprotResult.setErrMsg(excelImportErrObjectDto.getErrMsg());
  13. return customerCompleteDtoImprotResult;
  14. }).collect(Collectors.toList());
  15. //导出excel
  16. EasyExcelUtils.webWriteExcel(response,completeDtoImprotResults,CustomerCompleteDtoImprotResult.class,"客户信息");
  17. }
  18. return addSucResult();
  19. }

9、导出工具封装:EasyExcelUtils

  1. import com.alibaba.excel.EasyExcel;
  2. import com.alibaba.excel.write.metadata.style.WriteCellStyle;
  3. import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
  4. import org.apache.poi.ss.formula.functions.T;
  5. import org.apache.poi.ss.usermodel.IndexedColors;
  6. import javax.servlet.ServletOutputStream;
  7. import javax.servlet.http.HttpServletResponse;
  8. import java.io.IOException;
  9. import java.util.List;
  10. /**
  11. * @author zhy
  12. * @title: EasyExcelUtils
  13. * @projectName cec-moutai-bd-display
  14. * @description: easyExcel工具类
  15. * @date 2019/12/2411:35
  16. */
  17. public class EasyExcelUtils {
  18. public static void webWriteExcel(HttpServletResponse response, List objects, Class clazz, String fileName) throws IOException {
  19. String sheetName = fileName;
  20. webWriteExcel(response,objects,clazz,fileName,sheetName);
  21. }
  22. public static void webWriteExcel(HttpServletResponse response, List objects, Class clazz, String fileName, String sheetName) throws IOException {
  23. response.setContentType("application/vnd.ms-excel");
  24. response.setCharacterEncoding("utf-8");
  25. response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
  26. // 头的策略
  27. WriteCellStyle headWriteCellStyle = new WriteCellStyle();
  28. // 背景设置为白
  29. headWriteCellStyle.setFillForegroundColor(IndexedColors.WHITE.getIndex());
  30. // 内容的策略
  31. WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
  32. HorizontalCellStyleStrategy horizontalCellStyleStrategy =
  33. new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);
  34. ServletOutputStream outputStream = response.getOutputStream();
  35. try {
  36. EasyExcel.write(outputStream, clazz).registerWriteHandler(horizontalCellStyleStrategy).sheet(sheetName).doWrite(objects);
  37. }catch (Exception e){
  38. e.printStackTrace();
  39. }finally {
  40. outputStream.close();
  41. }
  42. }
  43. }