validator参数验证

传统后端参数验证通过大量if…else来做校验

在这里插入图片描述

Java Community Process Programa校验标准和validator版本对应

jsr版本303349380
validator版本1.01.12.0

Jakarta Bean Validation specification
Hibernate Validator 7.0.1.Final

项目中使用validator验证

1. 基础验证

新建springboot工程

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

<2.3.X版本的springboot依赖包含了validation依赖,可以不用重复引入上面依赖

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

通过实体类上注解说明验证条件

public class Department {
    @Null(message="主键不可以有值")//必须空
    private Integer id;//主键
    @NotNull(message="不能为null")//不为空
    private Integer parentId;//父级id
    @NotBlank(message="不能为空")//不能空和空字符串
    private String name;//部门名称
    @NotNull(message="不能为null")
    @PastOrPresent//不能空和未来时间
    private LocalDateTime creatTime;//成立时间,不能大于现在时间
    ...getter...
    ...setter...
}

通过类上@Validated或参数里@Valid开启验证

@RestController
@Validated  //本对类中方法开启参数验证功能
public class DepartmentController {

    @PostMapping("/department")
    public ResultVo add(@RequestBody @Valid/*校验后面的参数*/ Department department){
        return "OK";
    }
}

在这里插入图片描述

规范代码

统一返回对象并添加常用方法
public class ResultVo {
	private boolean success;//后端是否处理成功
    private String code;//错误码
    private String msg;//给前端返回的信息
    private Object data;//给前端返回的值

    public static ResultVo success(){
        ResultVo resultVo = new ResultVo();
        resultVo.setSuccess(true);
        return  resultVo;
    }

    public static ResultVo success(Object data){
        ResultVo resultVo = new ResultVo();
        resultVo.setSuccess(true);
        resultVo.setData(data);
        return  resultVo;
    }

    public static ResultVo fail(String msg, String code, Object data){
        ResultVo resultVo = new ResultVo();
        resultVo.setSuccess(false);
        resultVo.setMsg(msg);
        resultVo.setCode(code);
        resultVo.setData(data);
        return  resultVo;
    }
    ...getter...
    ...setter...
}
msg和code会散落在不同controller,不好管理,一般使用枚举
public enum ErrorCode {

    PARAM_ERROR("1000","参数不正确");

    private String code;
    private String msg;

    ErrorCode(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }
    ...getter...
    ...setter...
}
public class ResultVo {
    private boolean success;//后端是否处理成功
    private String code;//错误码
    private String msg;//给前端返回的信息
    private Object data;//给前端返回的值

    public static ResultVo success(){
        ResultVo resultVo = new ResultVo();
        resultVo.setSuccess(true);
        return  resultVo;
    }

    public static ResultVo success(Object data){
        ResultVo resultVo = new ResultVo();
        resultVo.setSuccess(true);
        resultVo.setData(data);
        return  resultVo;
    }

    public static ResultVo fail(ErrorCode errorCode, Object data){
        ResultVo resultVo = new ResultVo();
        resultVo.setSuccess(false);
        resultVo.setMsg(errorCode.getMsg());
        resultVo.setCode(errorCode.getCode());
        resultVo.setData(data);
        return  resultVo;
    }

    public static ResultVo fail(ErrorCode errorCode){
        ResultVo resultVo = new ResultVo();
        resultVo.setSuccess(false);
        resultVo.setMsg(errorCode.getMsg());
        resultVo.setCode(errorCode.getCode());
        return  resultVo;
    }

    ...getter...
    ...setter...
}
@RestController
@Validated  //本对类中方法开启参数验证功能
public class DepartmentController {

    @PostMapping("/department")
    public ResultVo add(@RequestBody @Valid/*校验后面的参数*/ Department department){
        return ResultVo.success();
    }
}
异常时返回给了前端大量不必要信息,一般会添加统一异常处理
@RestController
@Validated  //本对类中方法开启参数验证功能
public class DepartmentController {
    ...省略...
    @ExceptionHandler
    public ResultVo exceptionHandle(MethodArgumentNotValidException e){
        return ResultVo.fail(ErrorCode.PARAM_ERROR);
    }
}

在这里插入图片描述

data为null也会返回,使用@JsonInclude解决
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResultVo {
    ...省略...
}

在这里插入图片描述

将具体异常信息返回给前端
@RestController
@Validated  //本对类中方法开启参数验证功能
public class DepartmentController {
	...省略...
    @ExceptionHandler
    public ResultVo exceptionHandle(MethodArgumentNotValidException e){
        Map<String, String> map = e.getBindingResult().getFieldErrors().stream()
                .collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
        return ResultVo.fail(ErrorCode.PARAM_ERROR,map);
    }
}

在这里插入图片描述

异常处理一般会单独抽出一个类作用于全局
@ControllerAdvice(basePackages = "com.validator.demo")//作用范围
@ResponseBody
public class CtrlAdvice {
    @ExceptionHandler
    public ResultVo exceptionHandle(MethodArgumentNotValidException e){
        Map<String, String> map = e.getBindingResult().getFieldErrors().stream()
                .collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
        return ResultVo.fail(ErrorCode.PARAM_ERROR,map);
    }
}

注意@Valid和@Validated使用区别

  1. 入参实体,校验注解写在参数实体类上,@RequestBody @Valid Department department可以只用@Valid
  2. 入参上直接接受参数并声明注解,@PathVariable("id") @Min(5) int id需要在类上标注注解@Validated
    在这里插入图片描述

2. 级联验证

2.1 一对一

在实体类中所包含的另一实体属性上添加@Valid

public class Employeer {
    @Null
    private Integer id;
    @NotEmpty
    private String name;
    @Valid//验证该对象
    private Department department;//关联部门
    ...getter...
    ...setter...
}
@RestController
@Validated
public class EmployeerController {
    @PostMapping("/employee")
    public ResultVo add(@RequestBody @Valid Employeer employeer){
        return ResultVo.success();
    }
}

在这里插入图片描述

2.2 一对多

public class Department {
    ...省略...
    @Valid
    private List<Employeer> employeers;
    ...getter...
    ...setter...
}

或者更直观

public class Department {
    ...省略...
    private List<@Valid Employeer> employeers;
    ...getter...
    ...setter...
}

在这里插入图片描述

3. service层做校验

多个controller依赖同一service层,或者controller会去调用第三方获取额外参数,这样有可能service层做校验

@Service
@Validated
public class DepartmentService {
    public void add(@Valid Department department){
        System.out.println("添加部门成功");
    }
}
@RestController
//@Validated  //本对类中方法开启参数验证功能
public class DepartmentController {
    @Autowired
    private DepartmentService departmentService;

    @PostMapping("/department")
    public ResultVo add(@RequestBody /*@Valid校验后面的参数*/ Department department){
        departmentService.add(department);
        return ResultVo.success();
    }
}

注意:异常与controller层不同,是ConstraintViolationException.class

@ControllerAdvice(basePackages = "com.validator.demo")
@ResponseBody
public class CtrlAdvice {
    ...省略...
    @ExceptionHandler
    public ResultVo exceptionHandle(ConstraintViolationException e){
        Map<Path, String> map = e.getConstraintViolations().stream()
                .collect(Collectors.toMap(ConstraintViolation::getPropertyPath, ConstraintViolation::getMessage));
        return ResultVo.fail(ErrorCode.PARAM_ERROR,map);
    }
}

注意:实现接口类,注解必须加在接口上验证

@Valid 必须放在接口,否则报错

public interface IEmployeeService {
    void add(@Valid Employeer employeer);
}
@Service
@Validated
public class EmployeeService implements IEmployeeService {
    @Override
    public void add(Employeer employeer) {
        System.out.println("实现员工业务");
    }
}
@RestController
public class EmployeerController {

    @Autowired
    private EmployeeService employeeService;
    @PostMapping("/employee")
    public ResultVo add(@RequestBody Employeer employeer){
        employeeService.add(employeer);
        return ResultVo.success();
    }
}

注解还能作用于接口参数上,返回值上

public interface IEmployeeService {
    @NotNull Employeer get(@Valid @NotBlank String employeerId);
}

4.分组验证

业务场景:实体类id验证方式新增和更新不同

public class Employeer {

    public interface Add{}//用于分组标记
    public interface Update{}//用于分组标记

    @Null(groups = {Add.class})
    @NotNull(groups = {Update.class})
    private Integer id;
    //没标注分组的属于默认组
    @NotEmpty
    private String name;
    @Valid
    private Department department;
    ...getter...
    ...setter...
}

@Validated指定分组

@RestController
@Validated
@RequestMapping("/employee")
public class EmployeerController {

    @PostMapping
    public ResultVo add(@RequestBody @Validated({Employeer.Add.class, Default.class}) Employeer employeer){
        return ResultVo.success();
    }

    @PutMapping
    public ResultVo update(@RequestBody @Validated({Employeer.Update.class, Default.class}) Employeer employeer){
        return ResultVo.success();
    }

    @GetMapping
    public ResultVo add(@RequestBody @Min(10) Integer id){
        return ResultVo.success();
    }
}
validator提供的注解

在这里插入图片描述
在这里插入图片描述

5. 自定义注解验证

5.1 场景:业务有关

public class Job {
    private Integer id;
    @Size(min = 1)//仅在不为空才生效
    private String name;
    @Size(min = 1)
    private List<String> labels;
    ...getter...
    ...setter...
}

源码中与判空无关校验验注解,如@Size当值为null都是直接返回true
在这里插入图片描述
场景:A系统创建职位的id为3倍数,添加标签必须3的倍数

模仿已实现的注解写法

  1. 自定义注解
  2. @Constraint(validatedBy = {MultipleOfThreeInterger.class,MultipleOfThreeList.class })指定验证的类
@Target({FIELD})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {MultipleOfThreeInterger.class,MultipleOfThreeList.class })
public @interface MultipleOfThree {
    String message() default "必须是3的倍数";

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

    Class<? extends Payload>[] payload() default { };
}
  1. 验证的类实现ConstraintValidator接口
public class MultipleOfThreeInterger implements ConstraintValidator<MultipleOfThree,Integer> {
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        if (value == null){
            return true;
        }
        return value % 3 == 0;
    }
}
public class MultipleOfThreeList  implements ConstraintValidator<MultipleOfThree, List> {
    @Override
    public boolean isValid(List value, ConstraintValidatorContext context) {
        if (value.isEmpty()){
            return true;
        }
        return value.size() % 3 == 0;
    }
}
  1. 使用自定义注解
public class Job {
    @MultipleOfThree
    private Integer id;
    @Size(min = 1)//仅在不为空才生效
    private String name;
    @Size(min = 1,max = 10,Message = "{min}")//支持el表达式
    @MultipleOfThree
    private List<String> labels;
    ...getter...
    ...setter...
}

注意: 支持EL表达式

@Size(min = 1,Message = "{min}")//支持EL表达式
@RestController
@RequestMapping("/job")
@Validated
public class JobController {
    @PostMapping
    public ResultVo add(@RequestBody @Valid Job job){
        return ResultVo.success()
    }
}

在这里插入图片描述
在这里插入图片描述

5.2 场景:原注解不满足

场景:验证list里@Validated({Employeer.Add.class, Default.class})的逻辑,提供的注解不满足需求,也不支持List<@Validated({Employeer.Add.class, Default.class}) Employeer这种写法,需要自定义注解

@PostMapping
    public ResultVo addList(@RequestBody @Validated({Employeer.Add.class, Default.class}) List<Employeer> employeers){
        return ResultVo.success();
    }
  1. 自定义验证List注解
@Target({FIELD,PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {ValidListValidator.class})
public @interface ValidList {
    //要验证的分组
    Class<?>[] groupings() default { Default.class };

    String message() default "";

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

    Class<? extends Payload>[] payload() default { };
}
  1. 参考官方文档,需要validator对象
    在这里插入图片描述
  • 可以参考上面直接编程方式创建
  • validator在spring中已被管理,可以将验证类交给spring管理@Component@Scope("prototype")然后注入
  • 工具类中注入后使用(推荐)
@Component
public class ValidatorUtils {
    public static Validator validator;

    @Autowired
    public void setValidator(Validator validator) {
        ValidatorUtils.validator = validator;
    }
}
  1. 定义验证类
public class ValidListValidator  implements ConstraintValidator<ValidList, List> {

    Class<?>[] groupings;

    //初始化数据,获取注解里分组class数组
    @Override
    public void initialize(ValidList constraintAnnotation) {
        groupings = constraintAnnotation.groupings();
    }

    @Override
    public boolean isValid(List list, ConstraintValidatorContext context) {
        Map<Integer,Set<ConstraintViolation<Object>>> errors = new HashMap<>();
        for (int i = 0; i < list.size(); i++) {
            Object o = list.get(i);
            //校验list中的符合分组的对象
            Set<ConstraintViolation<Object>> validate = ValidatorUtils.validator.validate(o, groupings);
            errors.put(i,validate);
        }
        if (errors.size()>0){
        	//抛异常,从而将错误信息errors传递出去
            throw new ListValidException(errors);
        }
        //如果错误返回false,会按原逻辑取message给前端
        return true;
    }
}
  1. 自定义异常
public class ListValidException extends RuntimeException {
    Map<Integer, Set<ConstraintViolation<Object>>> errors;

    public ListValidException(Map<Integer, Set<ConstraintViolation<Object>>> errors) {
        this.errors = errors;
    }
    ...getter...
    ...setter...
}
  1. 捕获异常
@ControllerAdvice(basePackages = "com.validator.demo")
@ResponseBody
public class CtrlAdvice {

	...省略...
    //自定义异常会被包装成这个异常
    @ExceptionHandler
    public ResultVo exceptionHandle(ValidationException e){
        HashMap<Integer, Map<Path, String>> map = new HashMap<>();
        //强转可优化下
        ((ListValidException)e.getCause()).getErrors().forEach((integer, constraint)->{
            map.put(integer,constraint.stream()
                    .collect(Collectors.toMap(ConstraintViolation::getPropertyPath, ConstraintViolation::getMessage)));
        });
        return ResultVo.fail(ErrorCode.SYSTEM_ERROR,map);
    }
}
  1. 使用自定义注解验证
@PostMapping
public ResultVo addList(@RequestBody @ValidList(groupings = {Employeer.Add.class, Default.class}) List<Employeer> employeers){
    return ResultVo.success();
}
  1. 当list很大时,希望失败就不继续验证,避免浪费性能
public @interface ValidList {
    ..省略...
    boolean quickFail() default false;
}
public class ValidListValidator  implements ConstraintValidator<ValidList, List> {

    Class<?>[] groupings;
    boolean quickFail;

    //初始化数据
    @Override
    public void initialize(ValidList constraintAnnotation) {
        groupings = constraintAnnotation.groupings();
        quickFail = constraintAnnotation.quickFail();
    }

    @Override
    public boolean isValid(List list, ConstraintValidatorContext context) {
        Map<Integer,Set<ConstraintViolation<Object>>> errors = new HashMap<>();
        for (int i = 0; i < list.size(); i++) {
            Object o = list.get(i);
            //校验list中的符合分组的对象
            Set<ConstraintViolation<Object>> validate = ValidatorUtils.validator.validate(o, groupings);
            if (errors.size()>0){
                errors.put(i,validate);
                if (quickFail){
                    throw new ListValidException(errors);
                }
            }
        }
        if (errors.size()>0){
            throw new ListValidException(errors);
        }
        //如果错误返回false,会按原逻辑取message给前端
        return true;
    }
}
@PostMapping
    public ResultVo addList(@RequestBody @ValidList(groupings = {Employeer.Add.class, Default.class},quickFail = true) List<Employeer> employeers){
        return ResultVo.success();
    }
  1. Bean Validation默认会校验完所有字段,然后才抛出异常。可通过配置,一旦校验失败就立即返回
@Bean
public Validator validator() {
    ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
            .configure()
            .failFast(true)//快速失败配置
            .buildValidatorFactory();
    return validatorFactory.getValidator();
}

6. bean参数之间的逻辑校验

场景:

  • 员工的age在20-25之间,title必须以"初级"开头
  • 员工的age在25-30之间,title必须以"中级"开头
  • 否则,不做验证

该场景要求根据业务动态判断组

  1. 定义实体类
public class Employeer {
	...省略...
    public interface TitleJunior {}
    public interface TitleMiddle {}

    @NotNull
    private Integer age;

    @NotEmpty
    @Pattern(regexp = "^\u521d\u7ea7.*",groups = TitleJunior.class)//正则内不能写中文字符,用jdk工具查看ASCII码或者java代码输出查看下,匹配初级...
    @Pattern(regexp = "^\u4e2d\u7ea7.*",groups = TitleMiddle.class)//中级...
    private String title;
    }

在这里插入图片描述
2. 参照官方文档,实现DefaultGroupSequenceProvider
在这里插入图片描述

public class EmployeeGroupSequenceProvider implements DefaultGroupSequenceProvider<Employeer> {
    @Override
    public List<Class<?>> getValidationGroups(Employeer employeer) {
        List<Class<?>> defaultGroupSequence = new ArrayList<Class<?>>();
        defaultGroupSequence.add( Employeer.class ); //相当于添加了默认组
		//根据年龄判断加组
        if ( employeer != null && employeer.getAge() != null) {
            if (20<employeer.getAge() && employeer.getAge() <= 25){
                defaultGroupSequence.add(Employeer.TitleJunior.class);
            }else if (25<employeer.getAge() && employeer.getAge() <= 30){
                defaultGroupSequence.add(Employeer.TitleMiddle.class);
            }
        }
        return defaultGroupSequence;
    }
}
  1. 添加注解即可
@GroupSequenceProvider(EmployeeGroupSequenceProvider.class)
public class Employeer {
	...省略...
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值