1.后端服务校验
1.1JSR-303介绍
JSR是Java Specification Requests的缩写,意思是Java 规范提案。是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。
  JSR-303 是JAVA EE 6 中的一项子规范,叫做Bean Validation,Hibernate Validator 是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。
  Hibernate Validator 是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。
Hibernate 中填充一部分
2.后端校验实现
1.需要在commons服务中添加对应的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.4.12</version>
</dependency>
2.在需要校验的Bean的字段头部添加对应的注解
3.通过@Valid注解来开启JSR303的校验
4.测试,通过postman提交空的数据
当我们提交一个非空的数据是可以通过的
5.校验不合法的提示信息,在Controller中通过 BindingResult对象来获取校验的结果信息,然后解析出来后封装为R对象响应
@RequestMapping("/save")
//@RequiresPermissions("product:brand:save")
public R save(@Valid @RequestBody BrandEntity brand, BindingResult result){
if(result.hasErrors()){
// 提交的数据经过JSR303校验后有非法的字段
Map<String,String> map = new HashMap<>();
List<FieldError> fieldErrors = result.getFieldErrors();
for (FieldError fieldError : fieldErrors) {
// 获取非法数据的 field
String field = fieldError.getField();
// 获取非法的field的提示信息
String defaultMessage = fieldError.getDefaultMessage();
map.put(field,defaultMessage);
}
return R.error(400,"提交的品牌表单数据不合法").put("data",map);
}
brandService.save(brand);
return R.ok();
6.测试
然后完善其他字段你的校验规则
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@TableId
private Long brandId;
/**
* 品牌名
*/
//@NotEmpty
//@NotNull
@NotBlank(message = "品牌的名称不能为空")
private String name;
/**
* 品牌logo地址
*/
@NotBlank(message = "logo不能为空")
@URL(message = "logo必须是一个合法URL地址")
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
private Integer showStatus;
/**
* 检索首字母
*/
@NotBlank(message = "检索首字母不能为空")
@Pattern(regexp = "/^[a-zA-Z]$/",message = "检索首字母必须是单个的字母")
private String firstLetter;
/**
* 排序
*/
@NotNull(message = "排序不能为null")
@Min(value = 0,message = "排序不能小于0")
private Integer sort;
}
2. 统一的异常处理
1.在SpringMVC中的统一异常处理我们通过ControllerAdvice来处理(在通用模块中)
/**
* 统一的异常处理类
*/
/*@ResponseBody
@ControllerAdvice*/
@Slf4j
@RestControllerAdvice(basePackages = "com.msb.mall.product.controller")
public class ExceptionControllerAdvice {
/**
* 处理验证异常的方法
* @param e
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handlerValidExecption(MethodArgumentNotValidException e){
Map<String,String> map = new HashMap<>();
e.getFieldErrors().forEach((fieldError)->{
map.put(fieldError.getField(),fieldError.getDefaultMessage());
});
return R.error(400,"提交的数据不合法").put("data",map);
}
/**
* 系统其他的异常处理
* @param throwable
* @return
*/
@ExceptionHandler(Throwable.class)
public R handlerExecption(Throwable throwable){
log.error("错误信息:",throwable);
return R.error(400,"未知异常信息").put("data",throwable.getMessage());
}
}
2. 响应编码的规制制订,因为随着后面的业务越来越复杂,我们在响应异常信息的时候尽量准确的给客户端有用的提示信息。
通用的错误列表,响应的编码统一为5位数字,前面两位约定为业务场景,最后三位约定为错误码
10:表示通用
/001:参数格式错误 10001
/002:未知异常 10002
11:商品
12:订单
13:物流
14:会员
.....
3.定义对应的枚举类
package com.msb.common.exception;
/**
* 错误编码和错误信息的枚举类
*/
public enum BizCodeEnume {
UNKNOW_EXCEPTION(10000,"系统未知异常"),
VALID_EXCEPTION(10001,"参数格式异常");
private int code;
private String msg;
BizCodeEnume(int code,String msg){
this.code = code;
this.msg = msg;
}
public int getCode(){
return code;
}
public String getMsg(){
return msg;
}
}
4.在统一异常处理中我们就可以使用通用的编码
package com.msb.mall.product.exception;
import com.msb.common.exception.BizCodeEnume;
import com.msb.common.utils.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
/**
* 统一的异常处理类
*/
/*@ResponseBody
@ControllerAdvice*/
@Slf4j
@RestControllerAdvice(basePackages = "com.msb.mall.product.controller")
public class ExceptionControllerAdvice {
/**
* 处理验证异常的方法
* @param e
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handlerValidExecption(MethodArgumentNotValidException e){
Map<String,String> map = new HashMap<>();
e.getFieldErrors().forEach((fieldError)->{
map.put(fieldError.getField(),fieldError.getDefaultMessage());
});
//return R.error(400,"提交的数据不合法").put("data",map);
return R.error(BizCodeEnume.VALID_EXCEPTION.getCode(), BizCodeEnume.VALID_EXCEPTION.getMsg())
.put("data",map);
}
/**
* 系统其他的异常处理
* @param throwable
* @return
*/
@ExceptionHandler(Throwable.class)
public R handlerExecption(Throwable throwable){
log.error("错误信息:",throwable);
//return R.error(400,"未知异常信息").put("data",throwable.getMessage());
return R.error(BizCodeEnume.UNKNOW_EXCEPTION.getCode(), BizCodeEnume.UNKNOW_EXCEPTION.getMsg())
.put("data",throwable.getMessage());
}
}
5.测试
3.分组校验
在实际的业务场景中同一个Entity的校验可能会有不同的规则,比如添加数据品牌id必须为空,而更新数据品牌Id必须不为空,针对这种情况我们需要使用分组校验来实现
1.创建不同的接口
package com.msb.common.valid.group;
/**
* 添加的接口
*/
public interface AddGroupInterface {
}
package com.msb.common.valid.group;
/**
* 更新的接口
*/
public interface UpdateGroupInteface {
}
2.在Entity中指定分组规则
package com.msb.mall.product.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import com.msb.common.valid.group.AddGroupInterface;
import com.msb.common.valid.group.UpdateGroupInteface;
import com.msb.common.valid.ListValue;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import javax.validation.constraints.*;
/**
* 品牌
*
* @author qhl
* @email 1970405838@.qqcom
* @date 2023-01-09 20:42:37
*/
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@TableId
@NotNull(message = "更新时品牌id不能为空",groups = UpdateGroupInteface.class)
@Null(message = "添加时品牌id必须为空",groups = AddGroupInterface.class)
//@NotNull(message = "更新时品牌id不能为空")
private Long brandId;
/**
* 品牌名
*/
@NotBlank(message = "产品名称不能为空",groups = AddGroupInterface.class)//只能作用于String类型上且不能为null不能是“” 可以为“null”
@NotBlank(message = "产品名称不能为空")
private String name;
/**
* 品牌logo地址
*/
@URL(message = "logo的URL不合法",groups = AddGroupInterface.class)
@NotBlank(message = "logo不能为空",groups = {AddGroupInterface.class,UpdateGroupInteface.class})
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
@NotNull(message = "显示状态不能为空",groups = AddGroupInterface.class)
@ListValue(val={0,1},groups = {AddGroupInterface.class,UpdateGroupInteface.class})
private Integer showStatus;
/**
* 检索首字母
*/
@NotBlank(message = "检索首字母不能为空",groups = AddGroupInterface.class)
@Pattern(regexp = "^[a-zA-Z]$",message = "检索首字母必须是单个的字母",groups = {UpdateGroupInteface.class,AddGroupInterface.class})
private String firstLetter;
/**
* 排序
*/
@NotNull(message = "排序不能为null",groups = AddGroupInterface.class)
@Min(value = 0,message = "排序不能小于0",groups = {UpdateGroupInteface.class,AddGroupInterface.class})
private Integer sort;
}
3.通过@Validated注解来实现分组校验
/**
* 保存
*/
@RequestMapping("/save")
// @RequiresPermissions("product:brand:save")
public R save(/**@Valid*/ @Validated(AddGroupInterface.class)@RequestBody BrandEntity brand ) {
brandService.save(brand);
return R.ok();
}
/**
* 修改
*/
@RequestMapping("/update")
//@RequiresPermissions("product:brand:update")
public R update(@Validated(UpdateGroupInteface.class)@RequestBody BrandEntity brand){
brandService.updateById(brand);
return R.ok();
}
4.自定义校验注解
面临特殊的校验需要我们可以通过正则表达式来处理,当然我们也可以通过自定义校验注解的方式来实现。
1.创建自定义的校验注解
/**
* 自定义的校验注解
*/
@Documented
@Constraint(validatedBy = { })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {
String message() default "{com.msb.common.valid.ListValue.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
int[] val() default {};
}
2.对应需要创建提示信息的属性文件
3.创建一个自定义的校验器
package com.msb.common.valid;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.lang.annotation.Annotation;
import java.sql.ClientInfoStatus;
import java.util.HashSet;
/**
* 对应的校验注解的校验器
*/
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
private HashSet<Integer> set = new HashSet<>();
/**
* 初始化的方法
* 举例:@ListValue(val={1,0})
* 获取到 1 0
* @param constraintAnnotation
*/
@Override
public void initialize(ListValue constraintAnnotation) {
int[] val = constraintAnnotation.val();// 0 1
for (int i : val) {
set.add(i);
}
}
/**
* 判断校验是否成功的方法
* @param value 客户端传递的对应的属性的值 判断value是否在0 , 1 中
* @param context
* @return
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return set.contains(value);
}
}
4.关联自定义的校验注解和校验器
5.提取的知识点
5.1@Validated和@Valid区别:
1. 分组
@Validated:提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制,这个网上也有资料,不详述。@Valid:作为标准JSR-303规范,还没有吸收分组的功能。
2. 注解地方
@Validated:可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上
@Valid:可以用在方法、构造函数、方法参数和成员属性(字段)上
两者是否能用于成员属性(字段)上直接影响能否提供嵌套验证的功能。
3. 嵌套验证
在比较两者嵌套验证时,先说明下什么叫做嵌套验证。比如我们现在有个实体叫做Item:
public class Item {
@NotNull(message = "id不能为空")
@Min(value = 1, message = "id必须为正整数")
private Long id;
@NotNull(message = "props不能为空")
@Size(min = 1, message = "至少要有一个属性")
private List<Prop> props;
}
Item带有很多属性,属性里面有属性id,属性值id,属性名和属性值,如下所示:
public class Prop {
@NotNull(message = "pid不能为空")
@Min(value = 1, message = "pid必须为正整数")
private Long pid;
@NotNull(message = "vid不能为空")
@Min(value = 1, message = "vid必须为正整数")
private Long vid;
@NotBlank(message = "pidName不能为空")
private String pidName;
@NotBlank(message = "vidName不能为空")
private String vidName;
}
属性这个实体也有自己的验证机制,比如属性和属性值id不能为空,属性名和属性值不能为空等。
现在我们有个ItemController接受一个Item的入参,想要对Item进行验证,如下所示:
@RestController
public class ItemController {
@RequestMapping("/item/add")
public void addItem(@Validated Item item, BindingResult bindingResult) {
doSomething();
}
}
在上图中,如果Item实体的props属性不额外加注释,只有@NotNull和@Size,无论入参采用@Validated还是@Valid验证,Spring Validation框架只会对Item的id和props做非空和数量验证,不会对props字段里的Prop实体进行字段验证,也就是@Validated和@Valid加在方法参数前,都不会自动对参数进行嵌套验证。也就是说如果传的List<Prop>中有Prop的pid为空或者是负数,入参验证不会检测出来。
为了能够进行嵌套验证,必须手动在Item实体的props字段上明确指出这个字段里面的实体也要进行验证。由于@Validated不能用在成员属性(字段)上,但是@Valid能加在成员属性(字段)上,而且@Valid类注解上也说明了它支持嵌套验证功能,那么我们能够推断出:@Valid加在方法参数时并不能够自动进行嵌套验证,而是用在需要嵌套验证类的相应字段上,来配合方法参数上@Validated或@Valid来进行嵌套验证。
我们修改Item类如下所示:
public class Item {
@NotNull(message = "id不能为空")
@Min(value = 1, message = "id必须为正整数")
private Long id;
@Valid // 嵌套验证必须用@Valid
@NotNull(message = "props不能为空")
@Size(min = 1, message = "props至少要有一个自定义属性")
private List<Prop> props;
}
然后我们在ItemController的addItem函数上再使用@Validated或者@Valid,就能对Item的入参进行嵌套验证。此时Item里面的props如果含有Prop的相应字段为空的情况,Spring Validation框架就会检测出来,bindingResult就会记录相应的错误。
总结一下@Validated和@Valid在嵌套验证功能上的区别:
@Validated:用在方法入参上无法单独提供嵌套验证功能。不能用在成员属性(字段)上,也无法提示框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证。
@Valid:用在方法入参上无法单独提供嵌套验证功能。能够用在成员属性(字段)上,提示验证框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证。
5.2@NotNull与@NotBlank
@NotBlank只能作用于String类型上且不能为null不能是“” 可以为“null”
@NotNull不能为null,不能为"",也不能为"null",可以作用在所有字段上