持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情
参数校验的前世今生
对请求参数进行检验, 这在日常开发中经常遇到, 如果用 if/else 去做判断, 这样的代码会很繁琐且可读性较差. 顒诞生了下面几种校验框架。
- JSR-303
JSR-303 是 Java 为 bean 数据合法性校验提供的标准框架, 是 Java EE 中的一项子规范, 叫做 BeanValidation. JSR303 通过在 Bean 的属性上标注 @NotNull 等标准的注解来指定校验规则, 并通过这些标准的验证接口对 Bean 的属性值进行验证.
JSR-303 规定了一些检验规范, 规定了校验注解有哪些, 都位于 javax.validation.constraints 包下, 但是只提供规范 不提供具体实现.
- Hibernate-Validator
Hibernate-Validator 是 JSR-303 的具体实现. 不但提供了 JSR-303 规范中所有内置 constraint 的实现, 还进行了额外的扩展, 位于 org.hibernate,validator.constraints 包.
Spring-boot-starter-web 依赖中就默认引入了 hibernate-vlidator 依赖.
- Spring Validator
Spring 为了给开发者提供便捷, 对 Hibernate-Validator 进行了二次封装, 封装了 LocalValidatorFactorBean 作为 validator 的实现, 这个类兼容了 Spring 的 Validation 体系和 Hibernate 的 Validation 体系, LocalValidatorFactorBean 已经成为了 Validator 的默认实现.
数据校验注解
SpringBoot 的 Web 组件内部集成了 hibernate-validator, 所以不需要引入额外的依赖.
如下是常用的注解 :
@NotEmpty用在集合类上面, 要求Collection, Map 和 Array 对象不能是 null 并且 size > 0@NotBlank用在String上面, 且只能用在 String 上面,String 不能是 null 且 str.trim() > 0@NotNull用在基本类型上,对象不能是 null
更多注解如下 :
这些注解都有一个 message属性, 用来表示当条件不满足时, 返回给前端的信息.
如何在项目中使用
参数校验模式
有两种校验模式:
普通模式(默认的模式): 会校验完所有需要校验的属性,然后返回所有的验证失败信息.快速失败模式: 只要有一个验证失败,就返回.
如果想要配置第二种模式,需要添加如下配置类:
@Configuration
public class ValidatorConf {
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
.failFast(true)
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
return validator;
}
}
单个参数的校验
当处理 GET 请求时或只传入少量参数的时候,我们可能不会建一个 bean 来接收这些参数,就可以直接在 controller 方法的参数中进行校验。
注意:这里一定要在方法所在的 controller 类上加入@Validated注解,不然没有任何效果。
@RestController
@Validated // 启用参数校验
public class PingController {
@GetMapping("/getUser")
public String getUserStr(@NotNull(message = "name 不能为空") String name,
@Max(value = 99, message = "不能大于99岁") Integer age) {
return "name: " + name + " ,age:" + age;
}
}
多个参数的校验
① 在实体类的属性上,进行数据校验规则的编写
public class Label implements Serializable{
@Id
private String id; //标签ID
@NotNull(message = "标签名称不能为空")
private String labelname; //标签名称
private String state; //状态 ( 0-无效 , 1-有效 )
}
② 编写数据校验的全局异常处理器
@Slf4j
@ControllerAdvice
public class BaseExceptionHandler {
// 处理所有接口数据验证异常
@ExceptionHandler(value = MethodArgumentNotValidException.class)
@ResponseBody
public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
e.printStackTrace();
String message = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
return new Result(false, StatusCode.ERROR, message);
}
@ExceptionHandler(value = Exception.class)
public Result error(Exception e){
e.printStackTrace();
return new Result(false, StatusCode.ERROR,e.getMessage());
}
}
③ 控制器
在控制器的方法的入参上使用 @Validated 注解进行入参参数的校验.
@RestController
@RequestMapping(value = "label")
public class LabelController {
@PostMapping
public Result add(@Validated @RequestBody Label label) {
labelService.add(label);
return new Result(true, StatusCode.OK, "新增成功");
}
}
参数校验模式
有两种校验模式:
普通模式(默认的模式): 会校验完所有的属性,然后返回所有的验证失败信息.快速失败模式: 只要有一个验证失败,就返回.
如果想要配置第二种模式,需要添加如下配置类:
@Configuration
public class ValidatorConf {
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
.failFast(true)
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
return validator;
}
}
@Validated 分组校验
分组校验
① 定义分组接口 :
public interface IGroupA {
}
public interface IGroupB {
}
② 在需要校验的 bean 的属性上设置分组 :
public class StudentBean implements Serializable{
@NotBlank(message = "用户名不能为空")
private String name;
//设置分组,只在分组为IGroupB的情况下进行验证
@Min(value = 18, message = "年龄不能小于18岁", groups = {IGroupB.class})
private Integer age;
}
③ 测试代码 :
没设置分组的校验, 对所有分组都有效. 设置了分组的校验, 只对该分组有效.
@RestController
public class CheckController {
// 只设置分组 a 的校验
@PostMapping("stu")
public String addStu(@Validated({IGroupA.class}) @RequestBody StudentBean studentBean){
return "add student success";
}
// 设置分组 a 和 b 的校验
@PostMapping("stu2")
public String addStu(@Validated({IGroupA.class, IGroupB.class}) @RequestBody StudentBean studentBean){
return "add student success";
}
}
分组校验顺序问题
默认情况下不同级别的约束验证是无序的, 但是在一些情况下, 顺序验证却是很重要。
一个组可以定义为其他组的序列, 使用它进行验证的时候必须符合该序列规定的顺序。在使用组序列验证的时候, 如果序列前边的组验证失败, 则后面的组将不再给予验证。
① 定义组序列:
@GroupSequence({Default.class, IGroupA.class, IGroupB.class})
public interface GroupC {
}
② 需要校验的 Bean, 分别定义 IGroupA 对 age 进行校验, IGroupB 对 className 进行校验 :
public class StudentBean implements Serializable{
@NotBlank(message = "用户名不能为空")
private String name;
@Min(value = 18, message = "年龄不能小于18岁", groups = IGroupA.class)
private Integer age;
@MyConstraint(groups = IGroupB.class)
private String className;
}
③ 测试代码 :
@RestController
public class CheckController {
@PostMapping("stu")
public String addStu(@Validated({GroupC.class}) @RequestBody StudentBean studentBean){
return "add student success";
}
}
测试发现, 如果 age 出错, 那么对组序列在 GroupA 后的 GroupB 不会进行校验, 即例子中的 className 不进行校验.
@Validated 嵌套校验
一个待验证的 pojo 类, 其中还包含了待验证的对象, 需要在待验证对象上注解@Valid, 才能验证待验证对象中的成员属性, 注意:这里不能使用 @Validated.
① 需要约束校验的 bean:
public class Teacher {
@NotEmpty(message = "老师姓名不能为空")
private String teacherName;
@Min(value = 1, message = "学科类型从1开始计算")
private int type;
}
public class StudentBean implements Serializable{
@NotBlank(message = "用户名不能为空")
private String name;
@Min(value = 18, message = "年龄不能小于18岁")
private Integer age;
@Valid //该注解使得 teachers 中的每一个 teacher 对象都会进行校验, 即让 Teacher 类上的校验注解生效.
@NotNull(message = "任课老师不能为空")
@Size(min = 1, message = "至少有一个老师")
private List<TeacherBean> teachers;
}
参数校验顺序问题
Validation 参数校验时、如果这个 bean 有多个字段需要校验, 每次校验时, 都是随机校验的, 并没有按照一个固定的顺序.
现在来使用 分组校验, 来指定校验顺序.
① 定义一个接口, 指定参数校验的顺序 :
import javax.validation.GroupSequence;
import javax.validation.groups.Default;
/**
* 参数校验顺序
*/
@GroupSequence({VerifySeq.N0.class, VerifySeq.N1.class, VerifySeq.N2.class,VerifySeq.N3.class,
VerifySeq.N4.class,VerifySeq.N5.class,VerifySeq.N6.class, VerifySeq.N7.class,
VerifySeq.N8.class, VerifySeq.N9.class, Default.class})
public interface VerifySeq {
interface N0 { //分组
}
interface N1 {
}
interface N2 {
}
interface N3 {
}
interface N4 {
}
interface N5 {
}
interface N6 {
}
interface N7{
}
interface N8 {
}
interface N9 {
}
}
具体顺序由 @GroupSequence 注解中的顺序来决定, 排在前面的先校验.
② 在待校验的 实体类上, 指定属性的顺序 :
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import com.cebbank.poin.i18n.Msg;
import com.fasterxml.jackson.annotation.JsonFormat;
/**
* 客户高管
* @author lihao
*
*/
public class CustomerExecutives {
private String id; //主键
@NotBlank(message = Msg.CertificateType_CAN_NOT_NULL, groups=VerifySeq.N2.class)
private String certificateType; //证件类型
@NotBlank(message = Msg.CertificateNo_CAN_NOT_NULL, groups=VerifySeq.N3.class)
private String certificateNo; //证件号码
@NotBlank(message = Msg.ExecutivesName_CAN_NOT_NULL, groups=VerifySeq.N0.class)
private String executivesName; //高管姓名
@NotNull(message = Msg.Job_CAN_NOT_NULL, groups=VerifySeq.N1.class)
private Integer job; //担任职务
@NotNull(message = Msg.Sex_CAN_NOT_NULL, groups=VerifySeq.N4.class)
private Integer sex; //性别
@JsonFormat(pattern="yyyy-MM-dd",timezone="GMT+8")
private Date birthday; //出生日期
@NotNull(message = Msg.Education_CAN_NOT_NULL, groups=VerifySeq.N5.class)
private Integer education; //学历
private String resume; //工作简历
private String telphone; //联系电话
@JsonFormat(pattern="yyyy-MM-dd",timezone="GMT+8")
private Date jobDate; //担任该职务的时间
private Integer workYear; //相关行业从业年限
private String shareholding; //持股情况
@NotBlank(message = Msg.Valid_CAN_NOT_NULL, groups=VerifySeq.N6.class)
private String valid; //是否有效
private String registPerson; //登记人
private String registUnit; //登记单位
@JsonFormat(pattern="yyyy-MM-dd",timezone="GMT+8")
private Date registDate; //登记日期
@JsonFormat(pattern="yyyy-MM-dd",timezone="GMT+8")
private Date updateDate; //更新日期
}
③ 在控制层 :
@PostMapping("/add")
public Result add(@Validated(VerifySeq.class) @RequestBody CustomerExecutives customerExecutives) {
customerExecutivesService.add(customerExecutives);
return Result.Success(MessageUtils.get(Msg.INSERT_SUCCESS), null);
}
自定义参数校验注解
目标:自定义一个手机号格式的校验注解.
① 自定义校验注解类
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE,ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class) //定义当前注解使用哪个参数校验器进行校验
@Repeatable(Phone.List.class)
public @interface Phone {
String message() default "手机号码格式错误";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE,
ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@interface List {
Phone[] value();
}
}
注意:
- 注解的 message, groups, payload 属性都需要定义在参数校验注解中, 不能缺省.
- @Repeatable 是 JDK1.8 中的元注解, 表示在同一个位置可以有重复相同的注解. 如果使用的 JDK 版本低于 1.8, 可以使用以下方式创建 @Phone 注解.
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE,ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface Phone {
String message() default "手机号码格式错误";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
② 定义 PhoneValidator 参数校验器.
public class PhoneValidator implements ConstraintValidator<Phone, Object> {
private static final String PHONE_REGEX = "^((13[0-9])|(14[5|7])|(15([0-3]|[5-9]))|(17[013678])|(18[0,5-9]))\d{8}$";
@Override
public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
//值不为空或者满足正则表达式时返回true
return Objects.isNull(value) || Pattern.compile(PHONE_REGEX).matcher(value.toString()).find();
}
}
③ 使用参数校验注解.
@Phone
private String phone;