传统后端参数验证通过大量if…else来做校验
Java Community Process Programa校验标准和validator版本对应
jsr版本 | 303 | 349 | 380 |
---|---|---|---|
validator版本 | 1.0 | 1.1 | 2.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使用区别
- 入参实体,校验注解写在参数实体类上,
@RequestBody @Valid Department department
可以只用@Valid
- 入参上直接接受参数并声明注解,
@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的倍数
模仿已实现的注解写法
- 自定义注解
@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 { };
}
- 验证的类实现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;
}
}
- 使用自定义注解
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();
}
- 自定义验证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 { };
}
- 参考官方文档,需要validator对象
- 可以参考上面直接编程方式创建
- validator在spring中已被管理,可以将验证类交给spring管理
@Component@Scope("prototype")
然后注入 - 工具类中注入后使用(推荐)
@Component
public class ValidatorUtils {
public static Validator validator;
@Autowired
public void setValidator(Validator validator) {
ValidatorUtils.validator = validator;
}
}
- 定义验证类
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;
}
}
- 自定义异常
public class ListValidException extends RuntimeException {
Map<Integer, Set<ConstraintViolation<Object>>> errors;
public ListValidException(Map<Integer, Set<ConstraintViolation<Object>>> errors) {
this.errors = errors;
}
...getter...
...setter...
}
- 捕获异常
@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);
}
}
- 使用自定义注解验证
@PostMapping
public ResultVo addList(@RequestBody @ValidList(groupings = {Employeer.Add.class, Default.class}) List<Employeer> employeers){
return ResultVo.success();
}
- 当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();
}
- 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必须以"中级"开头
- 否则,不做验证
该场景要求根据业务动态判断组
- 定义实体类
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;
}
}
- 添加注解即可
@GroupSequenceProvider(EmployeeGroupSequenceProvider.class)
public class Employeer {
...省略...
}