数据校验 JSR303校验

目录

1. 校验需求 

2. 校验方法

2.1 拼接式校验

2.1.1 定义

2.1.2 举例

2.1.3 缺点

2.2 声明式校验 JSR303

2.2.1 定义

2.2.2 相关注解

扫描二维码关注公众号,回复: 14931869 查看本文章

2.2.3 Hibernate Validator 扩展注解

2.2.4 基本使用

2.2.5 分组校验(多场景的复杂校验)和自定义校验


1. 校验需求 

        对前端传来入参做数据合法性检查。

2. 校验方法

2.1 拼接式校验

2.1.1 定义

        在业务成对request中的参数一个个做非空判断。

2.1.2 举例

        Stu.java

@Data
@EqualsAndHashCode(callSuper = false)
public class Stu implements Serializable {
    private static final long serialVersionUID = 1L;

    @TableId(value = "id",type = IdType.AUTO)
    private int id;
    private String name;
    private int age;
    private String gender;
    private String address;
    private Date birth;
}

        StuServiceImpl.java

@Service
public class StuServiceImpl extends ServiceImpl<StuMapper, Stu> implements StuServiceI {
    @Override
    public Result add(Stu stu) {
        if(stu.getName()==null){
            return Result.fail("学生名不能为空");
        }
        if(stu.getAddress()==null){
            return Result.fail("学生地址不能为空");
        }
        this.save(stu);
        return Result.success();
    }
}

2.1.3 缺点

        如果实体类字段很多,不易维护,给业务层增加负担。

2.2 声明式校验 JSR303

2.2.1 定义

        声明式校验,也称JSR303校验。

        JRS303是Java为Bean数据合法性校验提供的标准框架,JSR303通过在Bean属性上标注类似于@NotNull、@NotBlank实现数据合法性校验。

2.2.2 相关注解

注解 功能
@Null 验证对象是否为 null
@NotNull 验证对象是否不为 null
@AssertTrue 验证 Boolean 对象是否为 true
@AssertTrue 验证 Boolean 对象是否为 false
@Max(value) 验证 Number 和 String 对象是否小于等于指定值
@Min(value) 验证 Number 和 String 对象是否大于等于指定值
@DecimalMax(value) 验证注解的元素值小于等于 @DecimalMax 指定的 value 值
@DecimalMin(value) 验证注解的元素值大于等于 @DecimalMin 指定的 value 值
@Digits(integer,fraction) 验证字符串是否符合指定格式的数字,integer 指定整数精度,fraction 指定小数精度
@Size(min,max) 验证对象长度是否在给定的范围内
@Past 验证 Date 和 Calendar 对象是否在当前时间之前
@Future 验证 Date 和 Calendar 对象是否在当前时间之后
@Pattern 验证 String 对象是否符合正则表达式的规则
@NotBlank 检查字符串是不是 Null,被 Trim 的长度是否大于0,只对字符串,且会去掉前后空格
@URL 验证是否是合法的 url
@CreditCardNumber 验证是否是合法的信用卡号

2.2.3 Hibernate Validator 扩展注解

注解 功能
@Email 验证是否是合法的邮箱
@Length(min,max) 验证字符串的长度必须在指定范围内
@NotEmpty 检查元素是否为 Null 或 Empty
@Range(min,max,message) 验证属性值必须在合适的范围内

2.2.4 基本使用

2.2.4.1 pom.xml

  <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-validation</artifactId>
  </dependency>

注意:validation 启动依赖里 包含jakarta:el:3.0.4.jar 和hibernate-validator:6.1.7.Final.jar

2.2.4.2 实体类Stu.java

@Data
@EqualsAndHashCode(callSuper = false)
public class Stu implements Serializable {
    private static final long serialVersionUID = 1L;

    @TableId(value = "id",type = IdType.AUTO)
    private int id;
    @NotBlank(message = "姓名不能为空")
    @Length(max = 20, message = "用户名称的长度不能超过20")
    private String name;
    private int age;
    private String gender;
    @NotBlank(message = "性别不能为空")
    private String address;
    private Date birth;
}

2.2.4.3 控制层StuController.java

        使用@Valid注解。

        如果想进行分组校验可使用@Validated注解,这里不做演示。

@RestController
@RequestMapping("/stu")
public class StuController {
    @Resource
    private StuServiceI stuService;

    @PostMapping
    public Result add(@Valid @RequestBody Stu stu){
        stuService.save(stu);
        return Result.success("新增成功");
    }
}

2.2.4.4 全局异常处理ExceptionCatch.java

        数据合法校验的异常是MethodArgumentNotValidException,多个数据合法校验异常,只提示用户第一个。

@RestControllerAdvice
public class ExceptionCatch {

    private static ImmutableMap.Builder<Class<? extends Exception>,ExceptionResult> builder = ImmutableMap.builder();
    /**
     * 自定义异常处理
     * @param e
     * @return
     */
    @ExceptionHandler(CustomException.class)
    public ResponseResult handlerRuntimeException(CustomException e){
        ExceptionResult result = e.getExceptionResult();
        return ResponseResult.fail(result.retCode(),result.retMsg());
    }

    @ExceptionHandler(value= MethodArgumentNotValidException.class)
    public ResponseResult validateException(MethodArgumentNotValidException e){
        BindingResult bindingResult=e.getBindingResult();
        Map<String,Object> map=new HashMap<>();
        List<FieldError> fieldErrors = bindingResult.getFieldErrors();
        FieldError fieldError = fieldErrors.get(0);
        map.put("field",fieldError.getField());
        map.put("msg",fieldError.getDefaultMessage());
        return ResponseResult.fail("数据校验异常",map);

    }
    /**
     * 其他异常处理
     *          除运行时异常以外的异常
     * @param e
     * @return
     */
    @ExceptionHandler(Exception.class)
    public ResponseResult handlerException(Exception e){
        ExceptionResult result = builder.build().get(e.getClass());
        if(ObjectUtil.isNotNull(result)){
            return ResponseResult.fail(result.retCode(),result.retMsg());
        }
        return ResponseResult.fail(e.getMessage());
    }
}

2.2.4.5 测试

2.2.5 分组校验(多场景的复杂校验)和自定义校验

项目中一般使用分组校验或自定义校验,这里更换实体类和优化统一异常处理。

1. 分组校验(多场景的复杂校验)

        新增、修改等接口想要校验的入参不同,可以通过分组进行校验。

(1)引入相关依赖 pom.xml

<!-- 数据校验 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

(2)返回前端数据的相关类

/**
 * 返回数据
 *
 * @author wen
 */
public class R extends HashMap<String, Object> {
	private static final long serialVersionUID = 1L;
	
	public R() {
		put("code", 0);
		put("msg", "success");
	}
	
	public static R error() {
		return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");
	}
	
	public static R error(String msg) {
		return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
	}
	
	public static R error(int code, String msg) {
		R r = new R();
		r.put("code", code);
		r.put("msg", msg);
		return r;
	}

	public static R ok(String msg) {
		R r = new R();
		r.put("msg", msg);
		return r;
	}
	
	public static R ok(Map<String, Object> map) {
		R r = new R();
		r.putAll(map);
		return r;
	}
	
	public static R ok() {
		return new R();
	}

	@Override
	public R put(String key, Object value) {
		super.put(key, value);
		return this;
	}
}

(3) 统一处理的全局异常、异常消息枚举类

@Slf4j
@RestControllerAdvice(basePackages = "com.wen.product.controller")
public class ExceptionControllerAdvice {


    @ExceptionHandler(MethodArgumentNotValidException.class)
    public R handlerValidException(MethodArgumentNotValidException e){
        log.error("数据校验异常:{},异常类型:{}",e.getMessage(), e.getClass());
        BindingResult bindingResult = e.getBindingResult();

        Map<String, String> errorMap = new HashMap<>();
        bindingResult.getFieldErrors().forEach((fieldError)->{
            errorMap.put(fieldError.getField(),fieldError.getDefaultMessage());
        });
        return R.error(BizCodeEnum.VAILD_EXCEPTION.getCode(), BizCodeEnum.VAILD_EXCEPTION.getMsg()).put("data",errorMap);
    }

    @ExceptionHandler(Throwable.class)
    public R handlerException(Throwable throwable){
        return R.error(BizCodeEnum.UNKNOW_EXCEPTION.getCode(), BizCodeEnum.UNKNOW_EXCEPTION.getMsg());
    }
}
public enum BizCodeEnum {
    UNKNOW_EXCEPTION(10000,"系统未知异常"),
    VAILD_EXCEPTION(10001,"参数格式校验异常");
    private int code;
    private String msg;

    BizCodeEnum(int code, String msg){
        this.code = code;
        this.msg = msg;
    }

    public int getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
}

(4) 定义分组接口

        这边定义新增校验分组接口AddGroup、修改校验分组接口UpdateGroup、修改状态分组接口UpdateStatusGroup。

/**
 * 新增数据 Group
 *
 * @author wen
 */
public interface AddGroup {
}
/**
 * 更新数据 Group
 *
 * @author wen
 */

public interface UpdateGroup {

}
/**
 * @author wen
 * @createDate 2023/3/24 15:04
 * @description 修改状态 Group
 */
public interface UpdateStatusGroup {
}

(5)实体类 BrandEntity

观察JSR303校验注解的groups属性的值就是不同分组想要校验的实体变量。 

@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
	private static final long serialVersionUID = 1L;

	/**
	 * 品牌id
	 */
	@Null(message = "新增品牌id必须为空",groups = {AddGroup.class})
	@NotNull(message = "修改品牌id不能为空",groups = {UpdateGroup.class, UpdateStatusGroup.class})
	@TableId
	private Long brandId;
	/**
	 * 品牌名
	 */
	@NotBlank(message = "品牌名不能为空",groups = {AddGroup.class,UpdateGroup.class})
	private String name;
	/**
	 * 品牌logo地址
	 */
	@NotBlank(message = "品牌logo地址不能为空",groups = {AddGroup.class})
	@URL(message = "品牌logo地址必须是一个合法的URL",groups = {AddGroup.class,UpdateGroup.class})
	private String logo;
	/**
	 * 介绍
	 */
	private String descript;
	/**
	 * 显示状态[0-不显示;1-显示]
	 */
	@NotNull(message = "显示状态不能为空", groups = {AddGroup.class, UpdateStatusGroup.class})
	private Integer showStatus;
	/**
	 * 检索首字母
	 */
	@NotEmpty(message = "检索首字母不能为空",groups = {AddGroup.class})
	@Pattern(regexp = "^[a-zA-Z]$", message = "检索首字母必须是一个字母",groups = {AddGroup.class,UpdateGroup.class})
	private String firstLetter;
	/**
	 * 排序
	 */
	@NotNull(message = "排序不能为空",groups = {AddGroup.class})
	@Min(value = 0, message = "排序必须大于等于0的整数",groups = {AddGroup.class,UpdateGroup.class})
	private Integer sort;

}

(6)控制层使用@Validated注解开启分组校验

@RestController
@RequestMapping("product/brand")
public class BrandController {
    @Autowired
    private BrandService brandService;

    /**
     * 保存
     */
    @RequestMapping("/save")

    public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brand){
		brandService.save(brand);

        return R.ok();
    }

    /**
     * 修改
     */
    @RequestMapping("/update")

    public R update(@Validated(UpdateGroup.class) @RequestBody BrandEntity brand){
		brandService.updateDetail(brand);
        return R.ok();
    }
    /**
     * 修改状态
     */
    @RequestMapping("/update/status")

    public R updateStatus(@Validated(UpdateStatusGroup.class) @RequestBody BrandEntity brand){
        brandService.updateById(brand);
        //brandService.updateBrand(brand);
        return R.ok();
    }

(7)测试

 2. 自定义校验

(1)引入依赖 pom.xml

        在基础校验和分组校验的基础上添加validation-api依赖,该依赖中包含自定义校验所需的校验器等。

<!-- 数据校验 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<!--   自定义JSR303校验     -->
<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>

(2)和(3)同分组校验的(2)和(3)

(4)自定义校验注解(默认消息提示)、自定义校验器

 1)编写一个自定义的校验注解
        默认message定义在src/main/resources/ValidationMessages.properties文件中
 2)编写一个自定义的校验器
 3)关联自定义的校验注解和自定义的校验器,使用@Constraint注解

注意:一个自定义注解可以指定多个不同的校验器,适配不同的校验。

/**
 * @author wen
 * @createDate 2023/3/24 14:17
 * @description 自定义校验注解
 */
@Documented
@Constraint(
        validatedBy = {ListValueConstraintValidator.class}
) // 指定校验器
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ListValue {
    String message() default "{com.wen.common.validator.anno.ListValue.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    int[] vals() default {};
}

ValidationMessages.properties

com.wen.common.validator.anno.ListValue.message=必须提交指定的值
/**
 * @author WangWenwen
 * @createDate 2023/3/24 14:20
 * @description 自定义ListValue校验器
 * ConstraintValidator<A,T>:A-指定注解;T-指定什么类型的数据
 *
 */
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {

    private Set<Integer> set = new HashSet<>();

    @Override
    public void initialize(ListValue constraintAnnotation) {
        int[] vals = constraintAnnotation.vals();
        if(vals!=null && vals.length>0){
            for (int val : vals) {
                set.add(val);
            }
        }
    }

    /**
     *
     * @param integer 需要校验的值
     * @param constraintValidatorContext
     * @return
     */
    @Override
    public boolean isValid(Integer integer, ConstraintValidatorContext constraintValidatorContext) {
        return set.contains(integer);
    }
}

(5)实体类 BrandEntity

        实体类中的showStatus变量使用@ListValue注解。

@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
	private static final long serialVersionUID = 1L;

	/**
	 * 品牌id
	 */
	@Null(message = "新增品牌id必须为空",groups = {AddGroup.class})
	@NotNull(message = "修改品牌id不能为空",groups = {UpdateGroup.class, UpdateStatusGroup.class})
	@TableId
	private Long brandId;
	/**
	 * 品牌名
	 */
	@NotBlank(message = "品牌名不能为空",groups = {AddGroup.class,UpdateGroup.class})
	private String name;
	/**
	 * 品牌logo地址
	 */
	@NotBlank(message = "品牌logo地址不能为空",groups = {AddGroup.class})
	@URL(message = "品牌logo地址必须是一个合法的URL",groups = {AddGroup.class,UpdateGroup.class})
	private String logo;
	/**
	 * 介绍
	 */
	private String descript;
	/**
	 * 显示状态[0-不显示;1-显示]
	 */
	@NotNull(message = "显示状态不能为空", groups = {AddGroup.class, UpdateStatusGroup.class})
	@ListValue(vals = {0,1}, groups = {AddGroup.class})
	private Integer showStatus;
	/**
	 * 检索首字母
	 */
	@NotEmpty(message = "检索首字母不能为空",groups = {AddGroup.class})
	@Pattern(regexp = "^[a-zA-Z]$", message = "检索首字母必须是一个字母",groups = {AddGroup.class,UpdateGroup.class})
	private String firstLetter;
	/**
	 * 排序
	 */
	@NotNull(message = "排序不能为空",groups = {AddGroup.class})
	@Min(value = 0, message = "排序必须大于等于0的整数",groups = {AddGroup.class,UpdateGroup.class})
	private Integer sort;

}

(6)控制层在分组的基础上

@ListValue指定的分组是新增。

@RestController
@RequestMapping("product/brand")
public class BrandController {
    @Autowired
    private BrandService brandService;

    /**
     * 保存
     */
    @RequestMapping("/save")

    public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brand){
		brandService.save(brand);

        return R.ok();
    }
}

(7)测试 

猜你喜欢

转载自blog.csdn.net/weixin_48568302/article/details/124997741