强忍腰椎的疼痛,我写完了EasyExcel JSR303

一、前言

这几天腰椎出现问题,左腿走路也很难受,实在忍不住,请了半天的假休息,抽出一小点时间来写一篇博客,争取按日更新哦!?

这几天看到【码农小胖哥】一篇文章,介绍了EasyExcel、JSR303进行导入数据校验,我也跟着试着记录下来具体的实现过程,首先,我们需要了解一些基本的知识,比如JSR303是什么?应该怎么校验?等等来进行分析,具体看下面内容!

二、基本原理

1、什么是JSR303?

JSR303是一个校验的标准,叫做Bean Validation,Hibernate Validator 是 Bean Validation 的实现,主要用于后端验证,我们这里思考一个问题:为什么需要后端验证?

**假设:**小明在某个网站进行注册用户,它输入了邮箱、用户名、密码,小明通过debug删除前端的校验方法,绕过了前端的验证,用户名使用了一些非法字符

如果后端没有校验,那么就会保存成功

如果增加了后端验证,及时前端被绕过,但是后端也会进行校验,保证数据的有效性

之前,我还还没有接触到Hibernate Validator的时候,后端总是使用很多的if-else来进行校验,导致规则修改,所有的校验都会无效

使用该后端验证之后,我需要在实体上修改校验规则,那么其他使用该校验规则的接口也会进行改变

2、EasyExcel基本概念

EasyExcel是阿里巴巴为了解决目前市面上Excel解析工具所存在问题的一种解决方法,目前市面上的解析工具都存在一个严重的问题就是非常的耗内存,所以该工具孕育而生。

具体的可以查看:github.com/alibaba/eas…

在这里,我们主要是使用EasyExcel中的ReadListener接口,我们可以首先了解一下,他的主要方法:

public interface ReadListener<T> extends Listener {

    /**
     * 如果遇到异常,将会调用该方法
     * @param var1
     * @param var2
     * @throws Exception
     */
    void onException(Exception var1, AnalysisContext var2) throws Exception;

    /**
     * 读取表头信息
     * @param var1
     * @param var2
     */
    void invokeHead(Map<Integer, CellData> var1, AnalysisContext var2);

    /**
     * 读取每一行信息
     * @param var1
     * @param var2
     */
    void invoke(T var1, AnalysisContext var2);

    /**
     * 读取单元格额外的信息
     * @param var1
     * @param var2
     */
    void extra(CellExtra var1, AnalysisContext var2);

    /**
     * 读取完所有的Sheet之后,会调用该方法
     * @param var1
     */
    void doAfterAllAnalysed(AnalysisContext var1);

    /**
     * 用来配置是否读取下一行
     * @param var1
     * @return
     */
    boolean hasNext(AnalysisContext var1);
}
复制代码

我们在此案例中,主要是重写AnalysisEventListener抽象方法,他实现了ReadListener接口,也就是上面的接口

我们的思路是这样的:

1、使用invoke读取所有的单元格数据,使用list进行存储

2、当所有数据解析完之后,调用doAfterAllAnalysed对数据进行校验,并抛出异常

3、如果数据校验全部通过,那么我们就模拟将数据存储到数据库

三、Coding

1、导入依赖

Hibernate Validator

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
复制代码

EasyExcel

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>2.2.10</version>
</dependency>
复制代码

2、分组校验

我们需要定义一个实体,并且定义一个接口用于分组校验

package com.yangzinan.easyexceljsr303.entity;

import lombok.Data;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

@Data
public class User {

    @NotNull(groups = Excel.class,message = "用户名不能为空")
    @Size(groups = Excel.class,max = 12,min = 6,message = "用户名长度为6-12之间")
    private String username;

    @NotNull(groups = Excel.class,message = "姓名不能为空")
    private String name;

    @NotNull(groups = Excel.class,message = "电话号码不能为空")
    private String number;

    /**
     * 用于分组校验
     */
    public interface Excel{

    }
}
复制代码

3、校验处理器

上面的流程结束,这里我们就需要穿件一个校验处理器,主要用于校验参数是否按照实体中的规则

原来,我们一般都是使用注解@Valid在参数上(Bean),如果遇到错误,就会抛出异常,我们只需要对异常进行处理就可以,但是对于文件上传,我们就无法使用注解的方法,因为他接收的是一个MultipartFile,所以我们要对导入的数据校验

/**
 * 校验处理器
 * @param <T>
 */
@AllArgsConstructor
public class ExcelValidator<T> {
    private final Validator validator;
    private final Integer beginIndex;

    /**
     * 集合校验
     * TODO:思路是将集合校验转换为单个数据的校验
     * @param datas
     * @return
     */
    public List<String> validator(Collection<T> datas){
        Integer index = beginIndex + 1;
        List<String> message = new ArrayList<>();
        for(T data : datas){
            String msg = this.doValidator(data,index);
            //如果存在错误消息,放入数组中
            if(StringUtils.hasText(msg)){
                message.add(msg);
            }
            index++;
        }
        return message;
    }

    /**
     * 真正的校验处理方法
     * @param data
     * @param index
     * @return
     */
    public String doValidator(T data,Integer index){
        /**
         * User.Excel.class 这个为User中的接口,用于分组校验使用
         */
        Set<ConstraintViolation<T>> validate = validator.validate(data, User.Excel.class);
        //如果存在错误信息,就返回错误提示
        if(validate.size() > 0){
            return "第"+index+"行,触发约束条件:"+validate.stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(","));
        }
        return "";
    }
}
复制代码

4、解析Excel中的数据

我们在开头已经提到,我们在这里使用AnalysisEventListener抽象方法,我们需要重写里面的一些方法

public class JdbcEventListener<T> extends AnalysisEventListener<T> {

    //数据最大阈值
    private final static Integer MAX_VALUE = 1000;

    //校验器
    private final ExcelValidator<T> excelValidator;

    private final Consumer<Collection<T>> batchConsumer;

    //临时数据处理
    private final List<T> list = new ArrayList<>();

    public JdbcEventListener(ExcelValidator<T> excelValidator, Consumer<Collection<T>> batchConsumer) {
        this.excelValidator = excelValidator;
        this.batchConsumer = batchConsumer;
    }

    /**
     * 异常处理
     * @param exception
     * @param context
     * @throws Exception
     */
    @Override
    public void onException(Exception exception, AnalysisContext context) throws Exception {
        list.size();
        throw exception;
    }

    /**
     * 解析数据
     * @param data
     * @param analysisContext
     */
    @Override
    public void invoke(T data, AnalysisContext analysisContext) {
        if(list.size() >= MAX_VALUE){
            throw new RuntimeException("单次上传数据量,不能超过:"+MAX_VALUE);
        }
        list.add(data);
    }

    /**
     * 解析完数据,调用该方法处理
     * @param analysisContext
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        if(!CollectionUtils.isEmpty(this.list)){
            List<String> validator = this.excelValidator.validator(this.list);
            //1.如果没有错误提示,直接进行存储数据库(这里模拟存储)
            if(CollectionUtils.isEmpty(validator)){
                this.batchConsumer.accept(this.list);
            }else{
                //2.如果有错误,直接进行返回错误信息
                throw new RuntimeException(validator.toString());
            }
        }
    }
}
复制代码

5、工具类

上面的校验方法也就结束,我们需要对EasyExcel中读取文件流的方法进行封装,获取文件的相关信息

@AllArgsConstructor
public class ExcelReader {
    private final Validator validator;

    /**
     * 读取excel数据
     * @param meta
     * @param <T>
     */
    public <T> void read(Meta<T> meta){
        ExcelValidator<T> excelValidator = new ExcelValidator<>(validator,meta.headRowNumber);
        JdbcEventListener<T> jdbcEventListener = new JdbcEventListener<>(excelValidator,meta.consumer);
        EasyExcel.read(meta.excelStream,meta.domain,jdbcEventListener)
                .headRowNumber(meta.headRowNumber)
                .sheet()
                .doRead();
    }

    /**
     * 匿名内部类
     * 解析需要的元数据ß
     */
    @Data
    public static class Meta<T>{
        private InputStream excelStream;
        private Integer headRowNumber;
        private Class<T> domain;
        private Consumer<Collection<T>> consumer;
    }
}
复制代码

根据官方文档,我们实现的JdbcEventListener不能被IOC进行管理,所以我们需要使用一个工具类,并且把工具类交给IOC管理

/**
 * ExcelReader注入IOC当中
 */
@Configuration
public class ExcelReaderConfig {

    @Bean
    public ExcelReader excelReader(Validator validator){
        return new ExcelReader(validator);
    }

}
复制代码

6、编写模拟存储数据库的方法

@Service
public class UserService<T>{

    public  <T> void saveBatch(T data){
        System.out.println("保存数据:"+data);
    }

}
复制代码

7、接口

/**
 * 上传文件
 * @param file
 * @return
 */
@PostMapping("/upload")
public Map<String,String> upload(@RequestPart MultipartFile file) throws IOException {
    InputStream inputStream = file.getInputStream();
    ExcelReader.Meta<User> userMeta = new ExcelReader.Meta<>();
    userMeta.setExcelStream(inputStream);
    userMeta.setDomain(User.class);
    userMeta.setHeadRowNumber(1);
    userMeta.setConsumer(userService::saveBatch);
    this.excelReader.read(userMeta);

    Map<String,String> info = new HashMap<>();
    info.put("code","200");
    info.put("message","上传数据成功");
    return info;
}
复制代码

四、Test

我们的整个案例已经搭建完成,那么我们需要测试相关数据,是否可以完成校验

我们首先测试一组符合规则的数据

username name number
123456 yangMic 12345678900
456778 24 12345678900
456778 Mic 12345678900

POSTMAN:

{
    "code": "200",
    "message": "上传数据成功"
}
复制代码

控制台:

保存数据:[User(username=123456, name=yangMic, number=12345678900), User(username=456778, name=24, number=13150702172), User(username=456778, name=Mic, number=12345678900)]
复制代码

我们测试一组不符合条件的数据

username name number
123 yangMic 12345678900
456 24 13150702172
234 Mic 12345678900

控制台:

[
  第2行,触发约束条件:用户名长度为6-12之间, 
  第3行,触发约束条件:用户名长度为6-12之间, 
  第4行,触发约束条件:用户名长度为6-12之间
]
复制代码

我们搭建的案例就这样结束了,我会争取下一篇文章开始写一些Activiti的相关内容

加油,腰椎疼痛,我也会继续坚持

晚安?

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享