# Spring 参数校验实战
# 介绍
在业务代码中,接口参数校验是绕不过去的坎。我们必须对接口输入参数去做一些判断,确认输入参数与我们期望的一致,避免导致我们的代码出现不在我们预期之内的错误。最简单的参数校验即是在拿到参数以后直接简单的做if else判断,这样可以完成功能,但是写起来特别的麻烦,而且需要写的代码量非常多,可读性很差,非常不可取。实际运用中,我们可以使用hibernate-validator来优雅的做参数校验处理,Spring中spring-boot-starter-web已经自带了hibernate-validator,如果没有引入spring-boot-starter-web的话,也可以单独引入hibernate-validator。接下来就详细介绍下如何使用hibernate-validator来做参数校验。
# 实现
# 使用校验注解
在
org.hibernate.validator.constraints
和javax.validation.constraints
包中提供了很多的注解,这些注解都是可以用来对参数做限制,常见注解如下:
验证注解 | 验证的数据类型 | 说明 |
---|---|---|
@AssertFalse | Boolean,boolean | 验证注解的元素值是false |
@AssertTrue | Boolean,boolean | 验证注解的元素值是true |
@NotNull | 任意类型 | 验证注解的元素值不是null |
@Null | 任意类型 | 验证注解的元素值是null |
@Min(value=值) | BigDecimal,BigInteger, byte,short, int, long,等任何Number或CharSequence(存储的是数字)子类型 | 验证注解的元素值大于等于@Min指定的value值 |
@Max(value=值) | 和@Min要求一样 | 验证注解的元素值小于等于@Max指定的value值 |
@DecimalMin(value=值) | 和@Min要求一样 | 验证注解的元素值大于等于@DecimalMin指定的value值 |
@DecimalMax(value=值) | 和@Min要求一样 | 验证注解的元素值小于等于@DecimalMax指定的value值 |
@Digits(integer=整数位数, fraction=小数位数) | 和@Min要求一样 | 验证注解的元素值的整数位数和小数位数上限 |
@Size(min=下限, max=上限) | 字符串、Collection、Map、数组等 | 验证注解的元素值的在min和max(包含)指定区间之内,如字符长度、集合大小 |
@Past | java.util.Date,java.util.Calendar;Joda Time类库的日期类型 | 验证注解的元素值(日期类型)比当前时间早 |
@Future | 与@Past要求一样 | 验证注解的元素值(日期类型)比当前时间晚 |
@NotBlank | CharSequence子类型 | 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的首位空格 |
@Length(min=下限, max=上限) | CharSequence子类型 | 验证注解的元素值长度在min和max区间内 |
@NotEmpty | CharSequence子类型、Collection、Map、数组 | 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0) |
@Range(min=最小值, max=最大值) | BigDecimal,BigInteger,CharSequence, byte, short, int, long等原子类型和包装类型 | 验证注解的元素值在最小值和最大值之间 |
@Email(regexp=正则表达式,flag=标志的模式) | CharSequence子类型(如String) | 验证注解的元素值是Email,也可以通过regexp和flag指定自定义的email格式 |
@Pattern(regexp=正则表达式,flag=标志的模式) | String,任何CharSequence的子类型 | 验证注解的元素值与指定的正则表达式匹配 |
@Valid | 任何非原子类型 | 指定递归验证关联的对象如用户对象中有个地址对象属性,如果想在验证用户对象时一起验证地址对象的话,在地址对象上加@Valid注解即可级联验证 |
注解可以直接用来修饰POJO类中的字段或者参数上,如下:
- 作用于POJO类
public class Student implements Serializable {
private static final long serialVersionUID = -339516038496531943L;
@NotBlank(message = "不能为空")
private String sno;
@NotBlank(message = "不能为空")
private String name;
@NotBlank(message = "不能为空")
@Length(min = 1, max = 1, message = "长度必须为1")
private String sex;
}
- 作用于参数
@RequestMapping(value = "/student", method = RequestMethod.GET)
public Student queryStudentBySno(@NotBlank(message = "不为空") String sno) {
return this.studentService.queryStudentBySno(sno);
}
在添加完注解以后,还需要使用
@Validated
注解开启校验,@Validated
可以用在类上,开启当前类的参数校验,但不会开启POJO类对象内部的参数校验。也可以用在参数上,单独开启某个参数或对象内的校验,完整写法如下:
- 作用于类上
@Validated
@RestController
public class TestController {
@Autowired
private StudentService studentService;
@RequestMapping(value = "/student", method = RequestMethod.GET)
public Student queryStudentBySno(@NotBlank(message = "不为空") String sno) {
return this.studentService.queryStudentBySno(sno);
}
@PostMapping(value = "/student")
public Student addStudent(@Validated @RequestBody Student student) {
this.studentService.add(student);
return this.studentService.queryStudentBySno(student.getSno());
}
}
- 作用于参数上
@RequestMapping(value = "/student", method = RequestMethod.GET)
public Student queryStudentBySno(@Validated @NotBlank(message = "不为空") String sno) {
return this.studentService.queryStudentBySno(sno);
}
# 使用自定义注解
在包提供的注解无法满足我们需求的时候,我们可以自定义我们自己的注解来做参数校验,主要步骤如下:
- 定义自定义参数校验注解
/**
* 默认 message,groups与payload是必须的
*/
@Documented
@Target({ElementType.FIELD, ElementType.PARAMETER}) // 可作用于字段或参数上
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = SexValidator.class) // 指定用于参数校验的校验类
public @interface CheckSex {
// 验证失败的信息
String message() default "无效的性别";
// 验证的分组,可依据分组来判断是否做校验
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
- 实现参数校验类
// 需要继承ConstraintValidator接口,并指定注解与校验参数类型
public class SexValidator implements ConstraintValidator<CheckSex, String> {
private static final List<String> validSex = Arrays.asList("M", "F");
// 用于处理初始化操作,只会被加载执行一次
@Override
public void initialize(CheckSex constraintAnnotation) {
}
// 用于校验参数是否有效调用的方法
@Override
public boolean isValid(String o, ConstraintValidatorContext constraintValidatorContext) {
return validSex.contains(o);
}
}
接下来就可以直接使用该注解了
# 使用Group解决同一对象在不同情况下验证要求不同的问题
在日常业务中,我们经常会碰到同一个对象,在不同的方法中参数校验要求不同的情况,比如某个字段在创建的时候是必须的,但是在更新接口中是非必须的,这个时候我们可以通过注解中的groups字段去解决这个问题,如下:
- 创建指定的group
// 创建分组
public interface Create extends Default {
}
// 更新分组
public interface Update extends Default {
}
- 设置不同条件针对不同的分组,只有在创建的时候才要求必填name和sex字段
public class Student implements Serializable {
private static final long serialVersionUID = -339516038496531943L;
@NotBlank(message = "不能为空")
private String sno;
@NotBlank(message = "不能为空", groups = {Create.class})
private String name;
@NotBlank(message = "不能为空", groups = {Create.class})
@Length(min = 1, max = 1, message = "长度必须为1")
@CheckSex(groups = {Create.class})
private String sex;
}
- 在验证的地方选择分组
@PostMapping(value = "/student")
public Student addStudent(@Validated(Create.class) @RequestBody Student student) {
this.studentService.add(student);
return this.studentService.queryStudentBySno(student.getSno());
}
@PutMapping(value = "/student")
public Student updateStudent(@Validated(Update.class) @RequestBody Student student) {
this.studentService.update(student);
return this.studentService.queryStudentBySno(student.getSno());
}
这样只有在创建的时候才会去验证@NotBlank
,@CheckSex
这两个注解,可以很好的应对不同的情况。
# 配合全局异常处理优化返回结果
在参数验证错误的时候,会抛出一些异常,并且返回结果,但是这个返回结果针对于业务来说并不是那么的友好,在业务处理中,我们一般都会规范的我们返回结果,统一样式返回。我们可以配合全局异常管理,全局捕捉参数异常,然后统一返回,详细可以参照上面的文章统一异常处理
# 总结
参数校验是一个很重要的部分,通过注解的方式可以让我们的参数校验开发变得简单且美观,可读性强、可维护性强。