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

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

最近在做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.      * 读取每行excel表头时会执行此方法
  13.      *
  14.      * @param headMap
  15.      * @param context
  16.      */
  17.     void invokeHead(Map<Integer, CellData> headMap, AnalysisContext context);
  18.      /**
  19.      * 读取每行数据的时候回执行此方法 
  20.      *
  21.      * @param data
  22.      *            one row value. Is is same as {@link AnalysisContext#readRowHolder()}
  23.      * @param context
  24.      *            analysis context
  25.      */
  26.     void invoke(T data, AnalysisContext context);
  27.      /**
  28.      * 如果有额外的单元格信息返回就用此方法处理
  29.      *
  30.      * @param extra
  31.      *            extra information
  32.      * @param context
  33.      *            analysis context
  34.      */
  35.     void extra(CellExtra extra, AnalysisContext context);
  36.      /**
  37.      * 在整个excel sheet解析完毕后执行的逻辑。
  38.      *
  39.      * @param context
  40.      */
  41.     void doAfterAllAnalysed(AnalysisContext context);
  42.      /**
  43.      * 用来控制是否读取下一行的策略
  44.      *
  45.      * @param context
  46.      * @return
  47.      */
  48.     boolean hasNext(AnalysisContext context);
  49. }

其抽象实现AnalysisEventListener<T>提供更加符合需要的抽象,我会进一步实现这个抽象来实现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相关的教程可以查看我这一篇文章

实现过程

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

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

封装最终的工具

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


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

我们把这个工具注入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);
  10. }

编写接口

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


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

总结

今天演示了如何将easyexcelJSR303结合起来,其实原理很简单,你只需要找到两个技术的结合点,并把它们组合起来即可,你学到了吗?请多多点赞、关注、转发、再看,多多支持:码农小胖哥 学习更多有用的技巧。

Spring Security 实战干货:WebSecurity和HttpSecurity的关系

2021-04-19

爆料:Spring 2021年的一些发展方向

2021-04-17


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值