SpringBoot-参数校验注解

436 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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;