Java 后端发工作中经常会对前端传递过来的参数做一些校验,在业务中还要抛出异常或者不断的返回异常时的校验信息,充满了 if-else 这种校验代码,在代码中相当冗长。

例如说,用户注册时,会校验手机格式的正确性,用户名的长度等等。虽说前端也可以做参数校验,但是为了保证我们 API 接口的可靠性,以保证最终数据入库的正确性,后端进行参数校验不可忽视。Hibernate Validator提供了一种统一方便的方式,让我们快速的实现参数校验。

Hibernate Validator使用注解,实现声明式校验。而在实现原理上,也是基于 Spring AOP 拦截,实现校验相关的操作。javax.validation.constraints包下,定义了一系列的约束注解。文末会贴上一些常用的注解。如果项目的框架是 SpringBoot 的话,在 spring-boot-starter-web 中已经包含了 Hibernate-validator 的依赖(版本 < 2.3)。

校验和累计 java java实体类校验_校验和累计 java

2.3以后的版本中 spring-boot-starter-web已经去除了这个依赖,需要手动引入 Hibernate-validator依赖。本文已将版本升级为2.3.3

org.springframework.bootspring-boot-starter-validation
 

        org.springframework.bootspring-boot-starter-validation



校验和累计 java java实体类校验_c++builder传递参数_02

完整代码已上传至 GitHub:https://github.com/yese777/validator

RequestBody参数校验

1、创建实体类

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.*;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User {
    /**
     * 用户ID
     */
    @NotNull(message = "用户id不能为空")
    private Long userId;

    /**
     * 用户名
     */
    @NotBlank(message = "用户名不能为空")
    @Length(max = 20, message = "用户名不能超过10个字符")
    @Pattern(regexp = "^[\\u4E00-\\u9FA5A-Za-z0-9\\*]*$", message = "用户名限制:最多10字符,包含文字、字母和数字")
    private String username;

    /**
     * 密码
     */
    @NotBlank(message = "密码不能为空")
    private String password;

    /**
     * 手机号
     */
    @NotBlank(message = "手机号不能为空")
    @Pattern(regexp = "^[1][3,4,5,6,7,8,9][0-9]{9}$", message = "手机号格式有误")
    private String mobile;

    /**
     * 性别
     */
    @NotNull(message = "性别不能为空")
    private Integer sex;

    /**
     * 年龄
     */
    @NotNull(message = "年龄不能为空")
    @Min(value = 1, message = "年龄最小为1岁")
    @Max(value = 120, message = "年龄最大为120岁")
    private Integer age;

    /**
     * 邮箱
     */
    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式错误")
    private String email;

}
 

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.*;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User {
    /**
     * 用户ID
     */
    @NotNull(message = "用户id不能为空")
    private Long userId;

    /**
     * 用户名
     */
    @NotBlank(message = "用户名不能为空")
    @Length(max = 20, message = "用户名不能超过10个字符")
    @Pattern(regexp = "^[\\u4E00-\\u9FA5A-Za-z0-9\\*]*$", message = "用户名限制:最多10字符,包含文字、字母和数字")
    private String username;

    /**
     * 密码
     */
    @NotBlank(message = "密码不能为空")
    private String password;

    /**
     * 手机号
     */
    @NotBlank(message = "手机号不能为空")
    @Pattern(regexp = "^[1][3,4,5,6,7,8,9][0-9]{9}$", message = "手机号格式有误")
    private String mobile;

    /**
     * 性别
     */
    @NotNull(message = "性别不能为空")
    private Integer sex;

    /**
     * 年龄
     */
    @NotNull(message = "年龄不能为空")
    @Min(value = 1, message = "年龄最小为1岁")
    @Max(value = 120, message = "年龄最大为120岁")
    private Integer age;

    /**
     * 邮箱
     */
    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式错误")
    private String email;

}

2、创建 controller

import com.yese.pojo.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
public class UserController {

    /**
     * controller方法的入参里面在实体类前面加上@Validated
     */
    @PostMapping("/user")
    public void add(@RequestBody @Validated User user) {
        log.info("user====={}", user);
    }

}
 

import com.yese.pojo.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
public class UserController {

    /**
     * controller方法的入参里面在实体类前面加上@Validated
     */
    @PostMapping("/user")
    public void add(@RequestBody @Validated User user) {
        log.info("user====={}", user);
    }

}

3、创建全局异常处理器

import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.validation.ConstraintViolationException;
import java.util.HashMap;
import java.util.Map;
import java.util.StringJoiner;

/**
 * 全局异常处理器
 */
@RestControllerAdvice
public class GlobalExceptionHandler {
    /**
     * post方式提交json数据,参数校验失败后,会抛出一个MethodArgumentNotValidException
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Map handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        // 获取所有的错误
        // List fieldErrors = e.getBindingResult().getFieldErrors();
        // 获取错误提示
        // System.out.println(fieldErrors.get(0).getDefaultMessage());
        // 获取错误字段
        // System.out.println(fieldErrors.get(0).getField());
        // 将所有的错误提示使用";"拼接起来并返回
        StringJoiner sj = new StringJoiner(";");
        e.getBindingResult().getFieldErrors().forEach(x -> sj.add(x.getDefaultMessage()));
        // 此处通常定义一个全局相应对象返回
        Map map = new HashMap<>();
        // 此处状态码可以通过枚举或者常量定义
        map.put("code", 1001);
        map.put("msg", sj.toString());return map;
    }
    /**
     * get方式提交参数,参数校验失败后,会抛出一个ConstraintViolationException
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public Map handleConstraintViolationException(ConstraintViolationException e) {
        StringJoiner sj = new StringJoiner(";");
        e.getConstraintViolations().forEach(x -> sj.add(x.getMessage()));
        Map map = new HashMap<>();
        map.put("code", 1001);
        map.put("msg", sj.toString());return map;
    }
}
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.validation.ConstraintViolationException;
import java.util.HashMap;
import java.util.Map;
import java.util.StringJoiner;

/**
 * 全局异常处理器
 */
@RestControllerAdvice
public class GlobalExceptionHandler {
    /**
     * post方式提交json数据,参数校验失败后,会抛出一个MethodArgumentNotValidException
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Map handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        // 获取所有的错误
        // List fieldErrors = e.getBindingResult().getFieldErrors();
        // 获取错误提示
        // System.out.println(fieldErrors.get(0).getDefaultMessage());
        // 获取错误字段
        // System.out.println(fieldErrors.get(0).getField());
        // 将所有的错误提示使用";"拼接起来并返回
        StringJoiner sj = new StringJoiner(";");
        e.getBindingResult().getFieldErrors().forEach(x -> sj.add(x.getDefaultMessage()));
        // 此处通常定义一个全局相应对象返回
        Map map = new HashMap<>();
        // 此处状态码可以通过枚举或者常量定义
        map.put("code", 1001);
        map.put("msg", sj.toString());return map;
    }
    /**
     * get方式提交参数,参数校验失败后,会抛出一个ConstraintViolationException
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public Map handleConstraintViolationException(ConstraintViolationException e) {
        StringJoiner sj = new StringJoiner(";");
        e.getConstraintViolations().forEach(x -> sj.add(x.getMessage()));
        Map map = new HashMap<>();
        map.put("code", 1001);
        map.put("msg", sj.toString());return map;
    }
}

4、测试



校验和累计 java java实体类校验_form-data给后端传递参数_03

效果符合预期!

RequestParam/PathVariable参数校验

/**
     * 入参校验时,Controller上面要加:@Validated
     * @RequestParam方式绑定参数
     */
    @GetMapping("/get1")
    public void get1(@RequestParam("id") @NotNull(message = "id不能为空") Integer id,
                     @RequestParam("name") @NotBlank(message = "name不能为空") @Length(max = 10, message = "用户名不能超过10个字符") String name) {
        log.info("id:{},name:{}", id, name);
    }

    /**
     * @PathVariable方式绑定参数
     */
    @GetMapping("/get2/{id}")
    public void get2(@PathVariable("id") @Min(value = 0, message = "id最小为0") Integer id) {
        log.info("id:{}", id);
    }
 

    /**
     * 入参校验时,Controller上面要加:@Validated
     * @RequestParam方式绑定参数
     */
    @GetMapping("/get1")
    public void get1(@RequestParam("id") @NotNull(message = "id不能为空") Integer id,
                     @RequestParam("name") @NotBlank(message = "name不能为空") @Length(max = 10, message = "用户名不能超过10个字符") String name) {
        log.info("id:{},name:{}", id, name);
    }

    /**
     * @PathVariable方式绑定参数
     */
    @GetMapping("/get2/{id}")
    public void get2(@PathVariable("id") @Min(value = 0, message = "id最小为0") Integer id) {
        log.info("id:{}", id);
    }

@Valid 和 @Validated

绝大多数场景下,我们使用 @Validated 注解即可。

而在有嵌套校验的场景,我们使用 @Valid 注解添加到成员属性上。参考:

public class Dog {
    @NotBlank(message = "狗的名字不能为空")
    private String name;
}
 

public class Dog {
    @NotBlank(message = "狗的名字不能为空")
    private String name;
}
public class User {
 // 省略其他。。。

    // 用于嵌套校验
    @Valid
    private Dog dog;
}
 

public class User {
 // 省略其他。。。

    // 用于嵌套校验
    @Valid
    private Dog dog;
}

集中定义提示信息

在 resources 目录下新建ValidationMessages.properties文件,内容如下:

id.NotNull=id不能为空
username.NotBlank=用户名不能为空
username.length=用户名不能超过10个字符
username.invalid=用户名限制:最多10字符,包含文字、字母和数字
password.NotBlank=密码不能为空
phone.NotBlank=手机号不能为空
phone.invalid=手机号格式不合法
sex.NotNull=性别不能为空
age.NotNull=年龄不能为空
age.min=年龄最小为1岁
age.max=年龄最大为120岁
email.NotBlank=邮箱不能为空
email.invalid=邮箱格式不合法
dogName.NotBlank=狗的名字不能为空
 

id.NotNull=id不能为空
username.NotBlank=用户名不能为空
username.length=用户名不能超过10个字符
username.invalid=用户名限制:最多10字符,包含文字、字母和数字
password.NotBlank=密码不能为空
phone.NotBlank=手机号不能为空
phone.invalid=手机号格式不合法
sex.NotNull=性别不能为空
age.NotNull=年龄不能为空
age.min=年龄最小为1岁
age.max=年龄最大为120岁
email.NotBlank=邮箱不能为空
email.invalid=邮箱格式不合法
dogName.NotBlank=狗的名字不能为空

修改原本验证注解上的文本,使用占位符{key}的方式来获取ValidationMessages.properties中定义的文本内容。以后要定义其他提示信息,只需要在该 properties 中集中定义即可,并且可以复用。参考如下:



校验和累计 java java实体类校验_form-data给后端传递参数_04

分组校验

当同一个对象要复用时, 比如 User 在更新时候要校验 userId, 在新增的时候不需要校验 userId, 而其他一些参数两种情况都需要校验,那就用上groups的功能了。定义两个接口:

import javax.validation.groups.Default;

/**
 * 用于新增时的分组校验
 */
public interface Insert extends Default {
}
 

import javax.validation.groups.Default;

/**
 * 用于新增时的分组校验
 */
public interface Insert extends Default {
}
import javax.validation.groups.Default;

/**
 * 用于更新时的分组校验
 */
public interface Update extends Default {
}
 

import javax.validation.groups.Default;

/**
 * 用于更新时的分组校验
 */
public interface Update extends Default {
}

修改实体,如果其余参数都需要验证,也可以不加 groups。

public class User {
    /**
     * 用户ID
     */
    @NotNull(message = "{id.NotNull}", groups = {Update.class})
    private Long userId;

    /**
     * 用户名
     */
    @NotBlank(message = "{username.NotBlank}", groups = {Insert.class, Update.class})
    @Length(max = 10, message = "{username.length}")
    @Pattern(regexp = "^[\\u4E00-\\u9FA5A-Za-z0-9\\*]*$", message = "{username.invalid}")
    private String username;
}
 

public class User {
    /**
     * 用户ID
     */
    @NotNull(message = "{id.NotNull}", groups = {Update.class})
    private Long userId;

    /**
     * 用户名
     */
    @NotBlank(message = "{username.NotBlank}", groups = {Insert.class, Update.class})
    @Length(max = 10, message = "{username.length}")
    @Pattern(regexp = "^[\\u4E00-\\u9FA5A-Za-z0-9\\*]*$", message = "{username.invalid}")
    private String username;
}

groups 数组表示生效的范围。修改 controller 的方法

@PostMapping("/user")
    public void add(@RequestBody @Validated(Insert.class) User user) {
        log.info("user====={}", user);
    }
 

    @PostMapping("/user")
    public void add(@RequestBody @Validated(Insert.class) User user) {
        log.info("user====={}", user);
    }

@Validated(Insert.class)表明该验证只验证 groups 中包含了 Insert.class 的参数。即,当前情况下,不会校验 userId。如果实体的其他参数没有显示的定义 groups,则两种情况都会校验。

新增时,未校验 id,效果如下,:

校验和累计 java java实体类校验_c++builder传递参数_05

若修改为@Validated(Update.class)

更新时,校验了 id,效果如下:

校验和累计 java java实体类校验_校验和累计 java_06

自定义校验注解

1. 自定义约束注解

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

@Documented
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UsernameValidator.class)
public @interface Username {

    // 默认错误消息
    String message() default "用户名不合法";

    // 分组
    Class>[] groups() default {};

    // 负载
    Class extends Payload>[] payload() default {};

}
 

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

@Documented
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UsernameValidator.class)
public @interface Username {

    // 默认错误消息
    String message() default "用户名不合法";

    // 分组
    Class>[] groups() default {};

    // 负载
    Class extends Payload>[] payload() default {};

}

2. 实现ConstraintValidator接口编写约束校验器

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

/**
 * 自定义用户名验证规则
 * 用户名需要以abc开头
 */
public class UsernameValidator implements ConstraintValidator {
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // 不为null才进行校验if (value != null) {return value.startsWith("abc");
        }return true;
    }
}
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

/**
 * 自定义用户名验证规则
 * 用户名需要以abc开头
 */
public class UsernameValidator implements ConstraintValidator {
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // 不为null才进行校验if (value != null) {return value.startsWith("abc");
        }return true;
    }
}

3. 使用

@NotBlank(message = "name不能为空")
    @Username(message = "name需要以abc开头")
    private String name;
 

    @NotBlank(message = "name不能为空")
    @Username(message = "name需要以abc开头")
    private String name;

附常用注解

javax.validation.constraints 包下

注解

验证的数据类型

说明

@Null

任意类型

验证注解的元素值必须是 null

@NotNull

任意类型

验证注解的元素值不是 null

@NotBlank

字符串

验证注解的元素值不为空(不为 null、去除首位空格后长度大于 0)

@NotEmpty

CharSequence 子类型、Collection、Map、数组

验证注解的元素值不为 null 且不为空(字符串长度不为 0、集合大小不为 0)

@Min(value = 值)

数值类型

验证注解的元素值大于等于 @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(包含)指定区间之内,如字符长度、集合大小

@Positive

判断正数

@PositiveOrZero

判断正数或 0 。

@Negative

判断负数。

@NegativeOrZero

判断负数或 0 。

@Past

日期类型

验证注解的元素值(日期类型)比当前时间早

@Future

与 @Past 要求一样

验证注解的元素值(日期类型)比当前时间晚

@AssertFalse

Boolean

验证注解的元素值是 false

@AssertTrue

Boolean

验证注解的元素值是 true

@Range(min = 最小值, max = 最大值)

BigDecimal,BigInteger,CharSequence, byte, short, int, long 等原子类型和包装类型

验证注解的元素值在最小值和最大值之间

@Email

字符串

验证注解的元素值是 Email,也可以通过 regexp 和 flag 指定自定义的 email 格式

@Pattern(regexp = 正则表达式, flag = 标志的模式)

字符串

验证注解的元素值与指定的正则表达式匹配

@Valid

任何非原子类型

指定递归验证关联的对象如用户对象中有个地址对象属性,如果想在验证用户对象时一起验证地址对象的话,在地址对象上加 @Valid 注解即可级联验证

org.hibernate.validator.constraints 包下,定义了一系列的约束 (constraint) 注解。如下:

@Range(min=, max=) :被注释的元素必须在合适的范围内。

@Length(min=, max=) :被注释的字符串的大小必须在指定的范围内。

@URL(protocol=,host=,port=,regexp=,flags=) :被注释的字符串必须是一个有效的 URL 。

@SafeHtml :判断提交的 HTML 是否安全。例如说,不能包含 javascript 脚本等等。


作者:yese777