封装了一个Excel导入加校验的工具,同事们用了都说好

开发 开发工具
最近在做Excel导入功能,产品要求对导入数据先进行校验然后再入库。于是简单封装了一个工具,结果兄弟们用了都说好,今天就把思路分享出来。

 [[394532]]

最近太忙了,刚刚到家赶紧抽空赶一篇,不知道能不能帮到你。

最近在做Excel导入功能,产品要求对导入数据先进行校验然后再入库。于是简单封装了一个工具,结果兄弟们用了都说好,今天就把思路分享出来。

easyexcel 库

我们都知道POI是Java操作Excel的基础库。为了通用性并没有做定制,而且还有一些局限性。经过一番调研决定采用二次封装库easyexcel来进行业务开发。

  1. <dependency> 
  2.     <groupId>com.alibaba</groupId> 
  3.     <artifactId>easyexcel</artifactId> 
  4.     <version>${easyexcel.version}</version> 
  5. </dependency> 

 

easyexcel将读取Excel的生命周期抽象为了几个阶段,方便我们在各个阶段注入你想要实现的逻辑。这几个阶段包含在ReadListener接口中

  1. public interface ReadListener<T> extends Listener { 
  2.     /** 
  3.      * 当任何一个侦听器执行错误报告时,所有侦听器都将接收此方法。 如果在此处引发异常,则整个读取将终止。 
  4.      * 这里是处理读取excel异常的 
  5.      * 
  6.      * @param exception 
  7.      * @param context 
  8.      * @throws Exception 
  9.      */ 
  10.     void onException(Exception exception, AnalysisContext context) throws Exception; 
  11.  
  12.     /** 
  13.      * 读取每行excel表头时会执行此方法 
  14.      * 
  15.      * @param headMap 
  16.      * @param context 
  17.      */ 
  18.     void invokeHead(Map<Integer, CellData> headMap, AnalysisContext context); 
  19.  
  20.     /** 
  21.      * 读取每行数据的时候回执行此方法  
  22.      * 
  23.      * @param data 
  24.      *            one row value. Is is same as {@link AnalysisContext#readRowHolder()} 
  25.      * @param context 
  26.      *            analysis context 
  27.      */ 
  28.     void invoke(T data, AnalysisContext context); 
  29.  
  30.     /** 
  31.      * 如果有额外的单元格信息返回就用此方法处理 
  32.      * 
  33.      * @param extra 
  34.      *            extra information 
  35.      * @param context 
  36.      *            analysis context 
  37.      */ 
  38.     void extra(CellExtra extra, AnalysisContext context); 
  39.  
  40.     /** 
  41.      * 在整个excel sheet解析完毕后执行的逻辑。 
  42.      * 
  43.      * @param context 
  44.      */ 
  45.     void doAfterAllAnalysed(AnalysisContext context); 
  46.  
  47.     /** 
  48.      * 用来控制是否读取下一行的策略 
  49.      * 
  50.      * @param context 
  51.      * @return 
  52.      */ 
  53.     boolean hasNext(AnalysisContext context); 

其抽象实现AnalysisEventListener提供更加符合需要的抽象,我会进一步实现这个抽象来实现Excel的导入和校验。

在你了解一个框架的抽象接口后,尽量要去看一下它有没有能满足你需要的实现。

另外这里要多说一点,接口中的AnalysisContext包含了很多有用的上下文元信息,比如 当前行、当前的配置策略、excel整体结构等信息,你可以在需要的时候调用这些信息。

JSR303校验

最开始自己写了一个抽象的校验工具,最后发现每一个字段都要编写其具体的校验逻辑,如果一个Excel的字段量爆炸,这对开发来说就可能是噩梦。这使我想到了业界已经有的规范-JSR303校验规范,它将数据模型(Model)和校验(Validation)各自抽象,非常灵活,而且工作量明显降低。我们只需要找到和esayexcel生命周期结合的地方就行了。我们只需要引入以下依赖就能在Spring Boot项目中集成JSR303校验:

  1. <dependency> 
  2.     <groupId>org.springframework.boot</groupId> 
  3.     <artifactId>spring-boot-starter-validation</artifactId> 
  4. </dependency> 

关于JSR303相关的教程可以查看我这一篇文章。

实现过程

我们可以在解析每个字段的时候校验,这对应ReadListener的invoke(T data, AnalysisContext context)方法,这种方式可以实现当字段校验触发约束时就停止excel解析的策略;另一种可以在Excel解析完毕后执行校验,对应doAfterAllAnalysed(AnalysisContext context)。这里以第二种为例我们来实现一下。

我们在编写代码时,尽量职责单一,一个类或者一个方法尽量只干一个事,这样让自己的代码足够清晰。

编写校验处理类

这里我把解析和校验分开实现,先编写JSR303校验工具。这里假设已经有了校验器javax.validation.Validator的实现,稍后我会讲这个实现从哪里注入。

  1. import cn.felord.validate.Excel; 
  2. import lombok.AllArgsConstructor; 
  3. import org.springframework.util.StringUtils; 
  4.  
  5. import javax.validation.ConstraintViolation; 
  6. import javax.validation.Validator; 
  7. import java.util.*; 
  8. import java.util.stream.Collectors; 
  9.  
  10. /** 
  11.  *  excel 校验工具 
  12.  * 
  13.  * @param <T> the type parameter 
  14.  * @author felord.cn 
  15.  * @since 2021 /4/14 14:14 
  16.  */ 
  17. @AllArgsConstructor 
  18. public class ExcelValidator<T> { 
  19.  
  20.     private final Validator validator; 
  21.     private final Integer beginIndex; 
  22.  
  23.  
  24.     /** 
  25.      *  集合校验 
  26.      * 
  27.      * @param data 待校验的集合 
  28.      * @return list 
  29.      */ 
  30.     public List<String> validate(Collection<T> data) { 
  31.         int index = beginIndex + 1; 
  32.         List<String> messages = new ArrayList<>(); 
  33.         for (T datum : data) { 
  34.             String validated = this.doValidate(index, datum); 
  35.             if (StringUtils.hasText(validated)) { 
  36.                 messages.add(validated); 
  37.             } 
  38.             index++; 
  39.         } 
  40.         return messages; 
  41.     } 
  42.      
  43.     /** 
  44.      * 这里是校验的根本方法 
  45.      * 
  46.      * @param index 本条数据所在的行号 
  47.      * @param data 待校验的某条数据 
  48.      * @return 对数据的校验异常进行提示,如果有触发校验规则的会封装提示信息。 
  49.      */ 
  50.     private String doValidate(int index, T data) { 
  51.         // 这里使用了JSR303的的校验器,同时使用了分组校验,Excel为分组标识 
  52.         Set<ConstraintViolation<T>> validate = validator.validate(data, Excel.class); 
  53.         return validate.size()>0 ? "第" + index + 
  54.                 "行,触发约束:" + validate.stream() 
  55.                 .map(ConstraintViolation::getMessage) 
  56.                 .collect(Collectors.joining(",")): ""
  57.     } 

上面就是整个校验的逻辑,如果校验通过不提示任何信息,如果校验不通过把校验的约束信息封装返回。这里的Validator是从哪里来的呢?当Spring Boot集成了JSR303会有一个Validator实现被自动注入Spring IoC,我们可以利用它。

实现AnalysisEventListener

这个完全是easyexcel的功能了,我们只需要实现最开始提到的Excel抽象解析监听器接口AnalysisEventListener,并将解析字段加入集合,等完全解析完毕后再进行校验。这里如果校验不通过就会抛出携带校验信息的异常,异常经过处理返回前端提示。

切记:AnalysisEventListener的实现不能注入Spring IoC。

  1. import cn.hutool.json.JSONUtil; 
  2. import com.alibaba.excel.context.AnalysisContext; 
  3. import com.alibaba.excel.event.AnalysisEventListener; 
  4. import cn.felord.exception.ServiceException; 
  5. import org.springframework.util.CollectionUtils; 
  6.  
  7. import java.util.ArrayList; 
  8. import java.util.Collection; 
  9. import java.util.List; 
  10. import java.util.function.Consumer; 
  11.  
  12. /** 
  13.  * 该类不可被Spring托管 
  14.  * 
  15.  * @param <T> the type parameter 
  16.  * @author felord.cn 
  17.  * @since 2021 /4/14 14:19 
  18.  */ 
  19. public class JdbcEventListener<T> extends AnalysisEventListener<T> { 
  20.     /** 
  21.      * Excel总条数阈值 
  22.      */ 
  23.     private static final Integer MAX_SIZE = 10000; 
  24.     /** 
  25.      * 校验工具 
  26.      */ 
  27.     private final ExcelValidator<T> excelValidator; 
  28.     /** 
  29.      * 如果校验通过消费解析得到的excel数据 
  30.      */ 
  31.     private final Consumer<Collection<T>> batchConsumer; 
  32.     /** 
  33.      * 解析数据的临时存储容器 
  34.      */ 
  35.     private final List<T> list = new ArrayList<>(); 
  36.  
  37.     /** 
  38.      * Instantiates a new Jdbc event listener. 
  39.      * 
  40.      * @param excelValidator Excel校验工具 
  41.      * @param batchConsumer  Excel解析结果批量消费工具,可实现为写入数据库等消费操作 
  42.      */ 
  43.     public JdbcEventListener(ExcelValidator<T> excelValidator, Consumer<Collection<T>> batchConsumer) { 
  44.         this.excelValidator = excelValidator; 
  45.         this.batchConsumer = batchConsumer; 
  46.     } 
  47.  
  48.     @Override 
  49.     public void onException(Exception exception, AnalysisContext context) throws Exception { 
  50.         list.clear(); 
  51.         throw exception; 
  52.     } 
  53.  
  54.     @Override 
  55.     public void invoke(T data, AnalysisContext context) { 
  56.         // 如果没有超过阈值就把解析的excel字段加入集合 
  57.         if (list.size() >= MAX_SIZE) { 
  58.             throw new ServiceException("单次上传条数不得超过:" + MAX_SIZE); 
  59.         } 
  60.         list.add(data); 
  61.     } 
  62.  
  63.     @Override 
  64.     public void doAfterAllAnalysed(AnalysisContext context) { 
  65.         //全部解析完毕后 对集合进行校验并消费 
  66.         if (!CollectionUtils.isEmpty(this.list)) { 
  67.             List<String> validated = this.excelValidator.validate(this.list); 
  68.             if (CollectionUtils.isEmpty(validated)) { 
  69.                 this.batchConsumer.accept(this.list); 
  70.             } else { 
  71.                 throw new ServiceException(JSONUtil.toJsonStr(validated)); 
  72.             } 
  73.         } 
  74.     } 

封装最终的工具

这里参考esayexcel的文档封装成一个通用的Excel读取工具

  1. import com.alibaba.excel.EasyExcel; 
  2. import lombok.AllArgsConstructor; 
  3. import lombok.Data; 
  4.  
  5. import javax.validation.Validator; 
  6. import java.io.InputStream; 
  7. import java.util.Collection; 
  8. import java.util.function.Consumer; 
  9.  
  10. /** 
  11.  * excel读取工具 
  12.  * 
  13.  * @author felord.cn 
  14.  * @since 2021 /4/14 15:10 
  15.  */ 
  16. @AllArgsConstructor 
  17. public class ExcelReader { 
  18.     private final Validator validator; 
  19.  
  20.     /** 
  21.      * Read Excel. 
  22.      * 
  23.      * @param <T>  the type parameter 
  24.      * @param meta the meta 
  25.      */ 
  26.     public <T> void read(Meta<T> meta) { 
  27.         ExcelValidator<T> excelValidator = new ExcelValidator<>(validator, meta.headRowNumber); 
  28.         JdbcEventListener<T> readListener = new JdbcEventListener<>(excelValidator, meta.consumer); 
  29.         EasyExcel.read(meta.excelStream, meta.domain, readListener) 
  30.                 .headRowNumber(meta.headRowNumber) 
  31.                 .sheet() 
  32.                 .doRead(); 
  33.     } 
  34.  
  35.  
  36.     /** 
  37.      * 解析需要的元数据 
  38.      * 
  39.      * @param <T> the type parameter 
  40.      */ 
  41.     @Data 
  42.     public static class Meta<T> { 
  43.         /** 
  44.          * excel 文件流 
  45.          */ 
  46.         private InputStream excelStream; 
  47.         /** 
  48.          * excel头的行号,参考easyexcel的api和你的实际情况 
  49.          */ 
  50.         private Integer headRowNumber; 
  51.         /** 
  52.          * 对应excel封装的数据类,需要参考easyexcel教程 
  53.          */ 
  54.         private Class<T> domain; 
  55.         /** 
  56.          * 解析结果的消费函数 
  57.          */ 
  58.         private Consumer<Collection<T>> consumer; 
  59.     } 
  60.  

我们把这个工具注入Spring IoC,方便我们使用。

  1. /** 
  2.  * Excel 读取工具 
  3.  * 
  4.  * @param validator the validator 
  5.  * @return the excel reader 
  6.  */ 
  7. @Bean 
  8. public ExcelReader excelReader(Validator validator) { 
  9.     return new ExcelReader(validator); 

编写接口

这里Excel的数据类ExcelData就不赘述了,过于简单!去看esayexcel的文档即可。编写一个Spring MVC接口示例,没错就是这么简单。

  1. @Autowired 
  2. private  ExcelReader excelReader; 
  3. @Autowired 
  4. private  DataService dataService; 
  5.  
  6. @PostMapping("/excel/import"
  7. public Rest<?> importManufacturerInfo(@RequestPart MultipartFile file) throws IOException { 
  8.     InputStream inputStream = file.getInputStream(); 
  9.     ExcelReader.Meta<ExcelData> excelDataMeta = new ExcelReader.Meta<>(); 
  10.     excelDataMeta.setExcelStream(inputStream); 
  11.     excelDataMeta.setDomain(ExcelData.class); 
  12.     excelDataMeta.setHeadRowNumber(2); 
  13.     // 批量写入数据库的逻辑 
  14.     excelDataMeta.setConsumer(dataService::saveBatch); 
  15.     this.excelReader.read(excelDataMeta); 
  16.     return RestBody.ok(); 

总结

今天演示了如何将easyexcel和JSR303结合起来,其实原理很简单,你只需要找到两个技术的结合点,并把它们组合起来即可,你学到了吗?

本文转载自微信公众号「码农小胖哥」,可以通过以下二维码关注。转载本文请联系码农小胖哥公众号。

 

责任编辑:武晓燕 来源: 码农小胖哥
相关推荐

2022-03-23 08:01:04

Python语言代码

2023-04-26 01:29:05

OkHttp3工具方式

2011-05-06 14:19:29

ExcelSQL Server

2023-09-26 11:59:48

ChatGPT人工智能

2023-04-26 08:19:48

Nacos高可用开发

2022-11-13 08:06:05

ArthasEverythingMaven help

2015-07-27 10:34:55

大数据大忽悠

2020-11-09 14:10:38

Bug安全代码

2021-06-30 09:56:24

MySQL数据库索引

2022-05-07 07:33:55

TypeScript条件类型

2020-03-02 19:08:21

JVMJDKJRE

2015-07-03 11:16:14

编程一个手镯

2020-11-23 11:30:00

IDEA技巧开发

2021-07-19 09:42:45

Spring Boot@ValueJava

2022-12-20 08:32:02

2019-07-26 09:20:21

ClusterShel命令Linux

2022-04-29 06:54:48

TS 映射类型User 类型

2020-02-22 21:51:43

程序员Microsoft SServerSQL

2017-04-18 14:25:54

Excel实战数据

2021-09-27 10:07:39

Python 开发编程语言
点赞
收藏

51CTO技术栈公众号