关于前后端校验
关于前端表单校验
这里指的前端校验特指表单校验,即el-form组件,分为一般校验和自定义校验规则校验:
一般校验
<el-form :model="ruleForm" :rules="MyRules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
<el-form-item label="活动名称" prop="name">
<el-input v-model="ruleForm.name"></el-input>
</el-form-item>
</el-form>
这里的:rules="MyRules"绑定了一个校验规则属性,我们在data中定义这个MyRules数组,如下:
MyRules: {
name: [
{ required: true, message: '请输入活动名称', trigger: 'blur' },
{ min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur' }
],
}
"name"表示给哪个字段校验,可以为字段匹配很多校验规则
自定义校验规则校验
<el-form
:model="dataForm"
:rules="dataRule"
ref="dataForm"
@keyup.enter.native="dataFormSubmit()"
label-width="80px"
>
<el-form-item label="检索首字母" prop="firstLetter">
<el-input
v-model="dataForm.firstLetter"
placeholder="检索首字母"
></el-input>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input v-model.number="dataForm.sort" placeholder="排序"></el-input>
</el-form-item>
</el-form>
dataRule: {
firstLetter: [
{
validator: (rule, value, callback) => {
if (value == "") {
callback(new Error("首字母不能为空"));
} else if (!/^[a-zA-Z]$/.test(value)) {
callback(new Error("首字母必须到a-z或A-Z之间的单一字符"));
}
callback();
},
trigger: "blur",
},
],
sort: [
{
validator: (rule, value, callback) => {
if (value == "") {
callback(new Error("排序字段不能为空"));
} else if (!Number.isInteger(value * 1) || value < 0) {
callback(new Error("排序字段必须是整数并且非负"));
}
callback();
},
trigger: "blur",
},
],
},
关于后端校验(JSR303)
JSR303规定了数据校验的标准,在包:javax.validation.constraints下有很多用于校验的注解,以下就是一些截图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f7kG994P-1643636550183)(1643544476932.png)]
如何在springboot中使用JSR303相关注解:
-
给需要校验的类的字段添加注解,比如@NotBlank,如果没有通过校验则会显示默认message,如果想修改,可以更新message字段: /**
* 品牌名
*/
@NotBlank(message = "品牌名不能为空")
private String name;
-
在给后端提交数据的时候告诉SpringMVC这个数据需要校验 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MQ8tI8Wd-1643636550197)(1643544897258.png)] -
通过postman演示: {
"timestamp": "2022-01-30T12:24:53.679+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"NotBlank.brandEntity.name",
"NotBlank.name",
"NotBlank.java.lang.String",
"NotBlank"
],
"arguments": [
{
"codes": [
"brandEntity.name",
"name"
],
"arguments": null,
"defaultMessage": "name",
"code": "name"
}
],
"defaultMessage": "品牌名不能为空",
"objectName": "brandEntity",
"field": "name",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotBlank"
}
],
"message": "Validation failed for object='brandEntity'. Error count: 1",
"path": "/product/brand/save"
}
-
我们其实是想将校验结果放到我们自定义的R(即统一响应对象中)一并返给前端,这样我们就可以随意的自己获取到错误信息: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K1uUl0np-1643636550199)(1643545735883.png)] 我们只需要在Controller层多接收一个BindingResult字段用以拿到将错误结果封装的类即可: @RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand, BindingResult result){
if(result.hasErrors()){
Map<String,String> map = new HashMap<>();
result.getFieldErrors().forEach((item) ->{
//获取到错误提示
String message = item.getDefaultMessage();
//获取的发生错误的字段
String field = item.getField();
map.put(field,message);
});
return R.error(400,"提交的数据不合法").put("data",map);
}else{
brandService.save(brand);
}
return R.ok();
}
-
这时候再通过postman请求你会发现成了现在这样: {
"msg": "提交的数据不合法",
"code": 400,
"data": {
"name": "品牌名不能为空"
}
}
-
我们再完善完善我们的校验注解: /**
* 品牌id
*/
@NotNull(message = "修改必须指定品牌id")
@TableId
private Long brandId;
/**
* 品牌名
*/
@NotBlank(message = "品牌名必须提交")
private String name;
/**
* 品牌logo地址
*/
@URL(message = "logo必须是一个合法的url地址")
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
private Integer showStatus;
/**
* 检索首字母
*/
@NotEmpty
@Pattern(regexp="^[a-zA-Z]$",message = "检索首字母必须是一个字母")
private String firstLetter;
/**
* 排序
*/
@NotNull
@Min(value = 0,message = "排序必须大于等于0")
private Integer sort;
我们指定logo必须得提交,因此添加了非空注解,并且提交的必须符合url格式,因此该字段添加了两个注解。另外我们需要注意,@NotEmpty不能修饰Integer字段,这种情况我们就使用@NotNull
上述我们使用BindingResult接收错误结果,但我们发现得在每个Controller的方法中写那些封装结果的逻辑:
if(result.hasErrors()){
Map<String,String> map = new HashMap<>();
result.getFieldErrors().forEach((item) ->{
//获取到错误提示
String message = item.getDefaultMessage();
//获取的发生错误的字段
String field = item.getField();
map.put(field,message);
});
return R.error(400,"提交的数据不合法").put("data",map);
好像脑子有什么大问题似的,我们可以做一个统一的产后处理,即写一个集中处理所有异常的类。我们使用的是SpringMVC提供的ControllerAdvice功能。
自定义全局异常统一处理校验结果
-
新建全局异常处理类,并添加@ControllerAdvice注解,该注解可以指明哪些Controller可以通过该异常类捕获,比如我们写:@ControllerAdvice(basePackages = “com.fiji.shangpinhui.product.controller”) -
我们在刚才的Controller中接收BindingResult是为了捕获由于数据校验有问题而产生的异常,并封装到自己的响应对象R中,现在将异常统一交由全局异常类处理,因此不需要捕获异常,只考虑正确的逻辑即可。将代码都删除利索: @RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand/*BindingResult result*/){
// if(result.hasErrors()){
// Map<String,String> map = new HashMap<>();
// result.getFieldErrors().forEach((item) ->{
// //获取到错误提示
// String message = item.getDefaultMessage();
// //获取的发生错误的字段
// String field = item.getField();
// map.put(field,message);
// });
// return R.error(400,"提交的数据不合法").put("data",map);
// }else{
brandService.save(brand);
// }
return R.ok();
}
-
编写全局异常处理类: @ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handleValidException(MethodArgumentNotValidException e){
log.error("数据校验出现问题{},\r\n异常类型:{}",e.getMessage(),e.getClass());
BindingResult result = e.getBindingResult();
Map<String,String> map = new HashMap<>();
result.getFieldErrors().forEach((item) ->{
//获取到错误提示
String message = item.getDefaultMessage();
//获取的发生错误的字段
String field = item.getField();
map.put(field,message);
log.error("field:{},\r\n::message:{}",field,message);
});
return R.error(400,"提交的数据不合法").put("data",map);
}
还记得我们是如何知道异常处理器接受的是MethodArgumentNotValidException类型的异常么?我们最先接收道德是Exception这个巨大的异常类,通过e.getClass()打印出实际捕获到的异常,这点非常考察经验呀雷神牛逼!!之所以这么做我们是想只有在发生数据校验异常的时候走我们的handleValidException()这个全局异常处理方法,功能细化;除此之外,我们还会写一个兜底的方法,用来处理所有的异常 -
最后我们可以使用postman进行请求: {
"msg": "提交的数据不合法",
"code": 400,
"data": {
"brandId": "修改必须指定品牌id",
"name": "品牌名必须提交"
}
}
为全局异常定义不同类型的状态码
刚才我们为数据校验赋上了一个400的状态码,所有的状态码汇聚到一个枚举类,这样前端会根据不同的状态码给出不同的解决方式,这都是从公司中累积的经验与解决方案,比如我们使用如下规范的错误码:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xPVnNqN8-1643636550200)(1643630063402.png)]
我们可以编写全局异常枚举类来细化我们的异常响应:
public enum BizCodeEnum {
INVALID_EXCEPTION(10000,"数据校验错误"),
UNKNOW_EXCEPTION(10001,"系统为止异常");
private Integer code;
private String msg;
BizCodeEnum(Integer code, String msg){
this.code = code;
this.msg = msg;
}
public Integer getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
接着修改我们的全局异常代码,使用postman测试如下:
{
"msg": "数据校验错误",
"code": 10000,
"data": {
"brandId": "修改必须指定品牌id",
"name": "品牌名必须提交"
}
}
数据校验更高级的功能:分组校验
场景:对于品牌管理来讲,新增品牌和修改品牌需要校验的同一个字段的规则可能是不一样的,比如说品牌id,作为品牌表的主键,新增品牌时不需要进行非空校验,而修改品牌时必须要进行校验;再比如品牌的logo,如果是新增的话我们限制logo必须非空,而当我们修改品牌的时候,logo如果不发生变化就允许提交的时候为空,不提交,也就是说logo字段在不同的情况下校验规则不同。面对这种情况,我们使用JSR303提供给我们的分组校验功能。
想使用分组校验功能,那就得在我们熟悉的校验注解里面设置属性(message就是我们设置的一个属性),groups,用于标注我们的校验注解是用于哪一种情况才进行校验的,也就是属于哪一组。以下是实现步骤:
-
为校验注解标注什么情况需要校验。为此我们可以看到groups属性接收一个Class类型的数组。在这种情况我们一般会编写空接口,比如AddGroup.java或是UpdateGroup.java 用以指定是添加的分组还是更新时的分组。代码修改如下: /**
* 品牌id
*/
@NotNull(message = "修改必须指定品牌id",groups = {UpdateGroup.class})
@Null(message = "添加时不能指定品牌id",groups = {AddGroup.class})
@TableId
private Long brandId;
-
在Controller层将@Valid注解替换成Spring提供的 @Validated注解,里面有属性指明是哪个分组,这样就能确定是哪种校验了: public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brand/*BindingResult result*/){
-
使用postman进行测试两部分内容:a、分组校验是否生效,给save接口提交一个brandId;b、未指定分组的字段校验是否生效,给提交的logo一个错误格式的url。 你会发现分组校验确实好用,但与此同时,原来给logo字段添加的校验却失效了。没办法,现在我们想让它生效就得给他指定是哪一个组的。加上之后再用postman测试: {
"msg": "数据校验错误",
"code": 10000,
"data": {
"logo": "logo必须是一个合法的url地址"
}
}
-
最后说明以下@Validated注解,该注解当添加了分组时,会使标注了分组的校验注解生效,而当@Validated没有指明分组时,情况就反过来了,只会让没有标注分组的校验注解生效,这个很好验证
数据校验更高级的功能:自定义校验
场景:对于只能接收0或1的字段,我们可以使用@Pattern(regexp = “”)设置正则表达式进行校验。但如果校验规则过于复杂的话我们就可以使用自定义校验,它就像service里的一段可以复用的代码,在此将其抽取出来。我们就以品牌的showStatus举例,showStatus只能有0或1两种状态。原来想实现这样的校验规则,可能我们会添加@Max注解和@Min注解,并设置最大值为1,最小值为0
-
编写一个自定义的校验注解。我们想达到这样一种效果:那就是在showStatus上面标注一个比如说是@ListValue注解,并指明value是1,2;即@ListValue(value={1,2}),这样来控制showStatus取值范围。因此首先我们创建一个校验注解:ListValue。
-
根据JSR303规范,一个校验注解首先必须有三个属性:message(当校验出错以后错误信息去哪里取)、groups(支持分组校验的功能)、payload(自定义一些负载信息)。因此我们就仿照着@NotNull将这三个属性先复制过来 -
添加元注解信息。比较重要的是@Constraint注解,用于指定校验器。 -
我们看到一些地方爆红,我们需要导入Validation API <dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
-
编写一个自定义的校验器 @Constraint里面指明我们需要的校验器。点击进该注解我们看到里面有一个属性: Class<? extends ConstraintValidator<?, ?>>[] validatedBy();
就是我们需要指明的校验器,这个校验器你看到了是需要实现ConstraintValidator接口。ConstraintValidator接口含有两个泛型,一个是@Constraint所修饰的自定义校验器,那就是我们的ListValue注解,另一个是该自定义注解需要修饰的字段的类型,这里shoistValuewStatus是Integer类型,因此我们创建一个名为ListValueConstraintValidator的实现类如下: public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
}
而以下就是自定义校验器的完整代码: public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
private Set<Integer> set = new HashSet<>();
/**
* 初始化方法
* @param integer 这个值就是需要校验的实际值,比如前端提交了个3,我们就需要校验3是否在{0,1}里面
* @param constraintValidatorContext
* @return
*/
@Override
public boolean isValid(Integer integer, ConstraintValidatorContext constraintValidatorContext) {
return set.contains(integer);
}
//判断是否校验成功
@Override
public void initialize(ListValue constraintAnnotation) {
//value里面就是0和1
int[] value = constraintAnnotation.value();
for (int i : value) {
set.add(i);
}
}
}
-
关联自定义的校验注解和自定义的校验器,即: @Constraint(
validatedBy = {ListValueConstraintValidator.class}
)
-
使用自定义校验注解: @ListValue(value = {0,1},groups = {AddGroup.class},message = "必须在0、1之间")
private Integer showStatus;
-
使用postman测试 {
"msg": "数据校验错误",
"code": 10000,
"data": {
"showStatus": "必须在0、1之间"
}
}
-
最后插上那么一句,我们刚才那个自定义校验器只能校验Integer类型的,@Constraint可以指定多个不同的校验器,适配不同类型的校验
|