springboot中使用自定义参数验证器

此文为本人对自定义参数验证器学习的总结,不涉及分组验证等其他知识。若有任何错误和不足之处,望指出。

自定义参数验证器一般只在引入的验证器不能完成任务时,才需要自定义参数验证器,比如多个字段需要联合验证时。

1. 新建springboot web项目

idea中选择Spring Initializr工具

springboot 整合kettle判断入参 springboot 可选参数_字段

添加web支持:

springboot 整合kettle判断入参 springboot 可选参数_User_02

从spring-boot-starter-web的依赖中可以看到,已经包含了验证器的依赖:

springboot 整合kettle判断入参 springboot 可选参数_User_03

所以不需要再另外添加验证器的依赖。

2. 设定需求:

有一个登录接口,会传入User对象的json字符串,现在要求,如果phone字段不为null,则要求age字段的值大于18.此时,不能直接在age字段上直接添加@Min注解,设置最小值是19, 因为对这个字段的最小值的要求,是有前提的。这时,就需要自定义验证器。

public class User {

    private String phone;

    @NotBlank
    private String name;

    @NotNull(message = "age不能为null")
    private Integer age;

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

3. 实现验证器

验证器注解:

@Target({ ElementType.TYPE})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = CheckAgeImpl.class)
public @interface CheckAge {
    String message() default "当phone不为空的时候,age字段必须大于18";

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

    Class<? extends Payload>[] payload() default {};
}
  • @Target({ ElementType.TYPE}),这个注解只能在类上使用,虽然要校验的是age字段,但是这个注解不能放在age字段上,因为如果放在age字段上,验证器到时候只能接收到age字段的值,但这个需求是有前提的,所以需要接收整个User对象,所以这个注解,要放在User类上,所以这里的Target,要设置为ElementType.TYPE
  • @Retention(RUNTIME):这个注解要在运行时起作用。
  • @Constraint(validatedBy = CheckAgeImpl.class): 这个注解设置了验证器功能的实现类。
  • messagegroupspayload这个3个属性必须的,即便不用,也不能省略。

验证器的实现:

public class CheckAgeImpl implements ConstraintValidator<CheckAge, User> {
    @Override
    public void initialize(CheckAge constraintAnnotation) {

    }

    @Override
    public boolean isValid(User value, ConstraintValidatorContext context) {
        if(!StringUtils.isEmpty(value.getPhone())){
            if(value.getAge() == null){
                return false;
            }
            if(value.getAge() <= 18){
                return false;
            }
        }
        return true;
    }
}

自定义验证器必须实现ConstraintValidator<A extends Annotation, T>接口。其中第一个泛型参数A是该验证器是为哪个注解实现验证功能,

可以看到这里要求我们的注解必须实现了Annotation接口,但是我们定义注解的时候,并没有实现Annotation接口,实际上编译器干了这个事,它会自动地让我们定义的注解实现Annotation接口。使用javap反编译即可看到:

javap -c CheckAge.class:

public interface com.woslx.springbootvaild.CheckAge extends java.lang.annotation.Annotation {
  public abstract java.lang.String message();

  public abstract java.lang.Class<?>[] groups();

  public abstract java.lang.Class<? extends javax.validation.Payload>[] payload();
}

从这里也说明,注解,其实本质上是接口,这也就是为什么注解的属性,是以方法的形式出现。

第二个参数T是被校验的对象的类型。这里虽然是验证age字段,但是因为有phone字段不为null的前提,实际上,这是对User对象的校验,而不是单纯对age字段的校验。

这个接口中的isValid方法,返回true表示验证通过,false表示验证未通过。

4. 使用验证器

把这个验证器注解放到User类上:

@CheckAge
public class User {

    private String phone;

    @NotBlank
    private String name;

    @NotNull(message = "age不能为null")
    private Integer age;

    ...省略getter/setter方法

Controller类:

@RestController
public class LoginController {

    @RequestMapping(value = "/login", consumes = "application/json")
    public String login(@Valid @RequestBody User user, BindingResult bindingResult){

        if(bindingResult.hasErrors()){
            List<ObjectError> allErrors = bindingResult.getAllErrors();
            StringBuilder sb = new StringBuilder();
            sb.append("error:");
            for(ObjectError error: allErrors){
                sb.append("\n").append(error.getDefaultMessage());
            }
            return sb.toString();
        }

        return "success";
    }
}

5. 测试

测试1:不设置phone字段:

public class TestLogin {
    public static final String url = "http://127.0.0.1:8080/";

    @Test
    public void test2() throws Exception{
        String urlTemp = url+"login";

        User user = new User();
        user.setName("Tom");
        user.setAge(10);
        System.out.println(JSON.toJSONString(user));
        OkHttpClient client = new OkHttpClient();
        MediaType mediaType = MediaType.parse("application/json");
        RequestBody body = RequestBody.create(mediaType, JSON.toJSONString(user));
        Request request = new Request.Builder()
                .url(urlTemp)
                .post(body)
                .addHeader("Content-Type", "application/json")
                .addHeader("Cache-Control", "no-cache")
                .build();

        Response response = client.newCall(request).execute();
        String respMessage = response.body().string();
        System.out.println(respMessage);
    }
}

输出:

{“age”:10,“name”:“Tom”}
success

测试2: 设置phone字段,但是不设置age字段:

User user = new User();
user.setPhone("13609876543");
user.setName("Tom");

{“name”:“Tom”,“phone”:“13609876543”}
error:
age不能为null
当phone不为空的时候,age字段必须大于18

测试3: 设置phone字段,但是age字段小于18

User user = new User();
user.setPhone("13609876543");
user.setName("Tom");
user.setAge(10);

输出:

{“age”:10,“name”:“Tom”,“phone”:“13609876543”}
error:
当phone不为空的时候,age字段必须大于18

  1. 源码地址

源码已经放在github上: https://github.com/hysgit/springbootvaild