一、前言
这几天腰椎出现问题,左腿走路也很难受,实在忍不住,请了半天的假休息,抽出一小点时间来写一篇博客,争取按日更新哦!?
这几天看到【码农小胖哥】一篇文章,介绍了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的相关内容
加油,腰椎疼痛,我也会继续坚持
晚安?