Hibernate Validator服务端数据校验
1、Hibernate Validator介绍
- Hibernate Validator是Hibernate提供的一个开源的数据校验框架,能够使用注解方式非常方便的实现服务端数据校验,免去了自己在service层编写数据校验的麻烦。
- Hibernate Validator是 Bean Validation 的参考实现,Hibernate Validator 提供了JSR 303规范中所有内置 constraint(约束) 的实现,除此之外还有一些附加的 constraint。在日常开发中,Hibernate Validator经常用来验证bean的字段,基于注解,方便快捷高效。
官网:Hibernate Validator官网
2、使用背景
在前后端分离项目中,一般前端都会进行数据校验,但是前端的数据校验很容易被绕过,所以也要在后端使用数据校验保证数据的合法性、安全性。
3、添加依赖
-
SpringBoot2.3以前,SpringBoot的web启动器中集成了Hibernate-Validator框架的相关依赖:
-
springboot-2.3 开始,校验包被独立成了一个 starter 组件,需要引入如下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
4、常用注解
Constraint | 详细信息 |
---|---|
@Valid | 被注释的元素是一个对象,需要检查此对象的所有字段值 |
@Null | 被注释的元素必须为 null |
@NotNull | 被注释的元素必须不为 null |
@AssertTrue | 被注释的元素必须为 true |
@AssertFalse | 被注释的元素必须为 false |
@Min(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@Max(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@DecimalMin(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@DecimalMax(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@Size(max, min) | 被注释的元素的大小必须在指定的范围内 |
@Digits (integer, fraction) | 被注释的元素必须是一个数字,其值必须在可接受的范围内 |
@Past | 被注释的元素必须是一个过去的日期 |
@Future | 被注释的元素必须是一个将来的日期 |
@Pattern(value) | 被注释的元素必须符合指定的正则表达式 |
被注释的元素必须是电子邮箱地址 | |
@Length | 被注释的字符串的大小必须在指定的范围内 |
@NotEmpty | 被注释的字符串的必须非空 |
@Range | 被注释的元素必须在合适的范围内 |
@NotBlank | 被注释的字符串的必须非空 |
@URL(protocol=,host=, port=,regexp=, flags=) | 被注释的字符串必须是一个有效的url |
@CreditCardNumber | 被注释的字符串必须通过Luhn校验算法,银行卡,信用卡等号码一般都用Luhn计算合法性 |
5、使用方式
- 接口方法参数列表中添加BindResult参数封装错误结果
- 全局异常处理,参数校验错误时不使用BindResult导致抛出异常,使用全局异常处理类统一处理
6、使用
6.1、使用@Validate注解+BindResult校验对象类参数,比如在新增及修改接口中使用
User实体类:
package com.ds.entity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.*;
import java.util.Date;
import java.util.List;
/**
* @version V1.0
* @package com.ds.entity
* @description: 用户实体类
* @author: ds
* @date: 2023/7/17
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(value = "用户对象")
public class User {
/**
* 主键id 用户id
*/
@ApiModelProperty(value = "用户ID")
private Integer userId;
/**
* 用户名
*/
@NotEmpty(message = "用户名不能为空")
@ApiModelProperty(value = "用户名")
private String username;
/**
* 密码
*/
@NotEmpty(message = "密码不能为空")
@Length(min=6,max = 20,message = "密码长度应在6到20之间")
@ApiModelProperty(value = "密码")
private String password;
/**
* 昵称
*/
@ApiModelProperty(value = "用户昵称")
private String nickname;
/**
* 真实姓名
*/
@ApiModelProperty(value = "真实姓名")
private String realname;
/**
* 头像
*/
@ApiModelProperty(value = "用户头像")
private String img;
/**
* 手机号
*/
@NotEmpty(message = "手机号不能为空")
@ApiModelProperty(value = "手机号")
private String mobile;
/**
* 邮箱地址
*/
@NotEmpty(message = "邮箱地址不能为空")
@Email(message = "邮箱地址格式不正确")
@ApiModelProperty(value = "邮箱地址")
private String email;
/**
* 性别 M(男) or F(女)
*/
@ApiModelProperty(value = "性别")
private Integer gender;
/**
* 年龄
*/
@NotNull(message = "年龄不能为空")
@Min(value = 12,message = "允许最小年龄为12岁")
@Max(value = 24,message = "允许最小年龄为24岁")
@ApiModelProperty(value = "年龄")
private Integer age;
/**
* 生日
*/
@ApiModelProperty(value = "生日")
private Date birth;
/**
* 创建时间
*/
@ApiModelProperty(value = "创建时间")
private Date createTime;
/**
* 更新时间
*/
@ApiModelProperty(value = "更新时间")
private Date updateTime;
/**
* 更新时间
*/
@NotEmpty(message = "联系人不允许为空")
@Size(min = 1,max = 3,message = "联系人长度只允许1到3之间")
private List<String> contacts;
}
controller类中修改接口参数列表,使用@Validate注解标记User,并使用BindResult参数接收参数校验失败内容,返回给前端错误信息。
@PutMapping
@ApiOperation(value = "修改用户接口")
public Result<String> updateUser(@RequestBody @Validated User user, BindingResult bindingResult) {
List<ObjectError> allErrors = bindingResult.getAllErrors();
if(!allErrors.isEmpty()){
String validFailMsg = allErrors.stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.toList()).toString();
return Result.build(validFailMsg, ResultCodeEnum.VALIDATE_FAILED);
}
return Result.success("");
}
6.2、使用统一异常处理方式,可以用于校验基本参数以及对象类参数
创建全局异常处理类GlobalExceptionAdvice ,接口方法参数不使用BindResult时,检验不同参数会抛出不同的异常,分别如下:
- 校验参数为json数据时,校验失败时抛出MethodArgumentNotValidException异常
- 校验参数为form data数据时,校验失败时抛出BindException异常
- 校验单个或多个参数,校验失败时抛出ConstraintViolationException异常
GlobalExceptionAdvice 类:
package com.ds.common.exception.advice;
import com.ds.common.enums.ResultCodeEnum;
import com.ds.common.result.Result;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @version V1.0
* @package com.ds.common.exception.advice
* @description: 全局异常处理类
* @author: ds
* @date: 2023/7/17
*/
@RestControllerAdvice
public class GlobalExceptionAdvice {
/**
* 有关后端接口数据校验,不同种类的参数校验失败后对应不同的异常说明:
* 1、使用 json 请求体调用接口,校验异常抛出 MethodArgumentNotValidException
* 2、使用 form data 方式调用接口,校验异常抛出 BindException
* 3、使用 单个参数 请求体调用接口,校验异常抛出ConstraintViolationException
*/
/**
* 处理 json 请求体调用接口校验失败抛出的异常
* 使用 json 请求体调用接口,校验异常抛出 MethodArgumentNotValidException
*
* @param e
* @return
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<List<String>> MethodArgumentNotValidException(MethodArgumentNotValidException e) {
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
List<String> validFailMsg = fieldErrors.stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.toList());
return Result.build(validFailMsg, ResultCodeEnum.VALIDATE_FAILED);
}
/**
* 使用form data方式调用接口,校验异常抛出 BindException
*
* @param e
* @return
*/
@ExceptionHandler(BindException.class)
public Result<List<String>> BindException(BindException e) {
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
List<String> validFailMsg = fieldErrors.stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.toList());
return Result.build(validFailMsg, ResultCodeEnum.VALIDATE_FAILED);
}
/**
* 基本请求参数调用接口,校验异常抛出 ConstraintViolationException
*
* @param e
* @return
*/
@ExceptionHandler(ConstraintViolationException.class)
@ResponseBody
public Result<String> handler(ConstraintViolationException e) {
StringBuffer errorMsg = new StringBuffer();
Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
violations.forEach(x -> errorMsg.append(x.getMessage()).append(";"));
return Result.build(errorMsg.toString(), ResultCodeEnum.VALIDATE_FAILED);
}
}
6.2.1、基本参数校验:controller类上一定要添加@Validate注解,否则不生效,校验失败后会抛出ConstraintViolationException异常
package com.ds.controller;
import com.ds.common.result.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.constraints.NotNull;
/**
* @version V1.0
* @package com.ds.controller
* @description: 用户控制器类
* @author: ds
* @date: 2023/7/17 11:14
*/
@Api(tags = "用户管理模块")
@RestController
@RequestMapping("/user")
@Validated
public class UserController {
@GetMapping
@ApiOperation(value = "根据用户ID查询用户信息")
public Result<String> selectUserById(@NotNull(message = "用户ID不能为空") Integer userId) {
return Result.success("用户张三");
}
}
结果:
6.2.2、json数据校验:之前使用BindResult返回检验失败时的代码(见下面代码中注释部分)就不用写了,校验失败后会抛出MethodArgumentNotValidException异常
/**
* 校验json数据,使用全局异常处理类处理
*
* @param user
* @return
*/
@PostMapping
@ApiOperation(value = "新增用户接口")
public Result<String> insertUser(@RequestBody @Validated User user) {
//if (bindingResult.hasErrors()) {
// String validFailMsg = bindingResult.getFieldErrors()
// .stream()
// .map(FieldError::getDefaultMessage)
// .collect(Collectors.joining("|"));
// return ResponseEntity.ok(validFailMsg);
//}
return Result.success("");
}
结果:
6.2.3、form data类型数据校验:校验失败后抛出BindException异常
/**
* 对form data参数进行校验
* 校验失败时会抛出BindException异常,全局异常处理类会进行处理并返回错误信息
*
* @return
*/
@PostMapping("/save")
@ApiOperation(value = "通过form data数据新增用户")
public Result<String> saveUser(@Validated User user) {
return Result.success("用户张三");
}
结果:
7、分组校验
分组校验的使用场景举例:比如用户登录时可以使用用户名和密码登录,也可以使用手机号和验证码进行登录。
使用步骤:
- 1、定义接口
- 2、在实体类校验注解上添加 groups 属性指定分组,如:@NotEmpty(message = “密码不能为空”,groups = {LoginUsernameValid.class})
- 3、controller接口的方法上 @Validated 注解添加分组类,如:(@Validated(LoginUsernameValid.class) LoginEntity loginEntity)
定义接口:
/**
* @version V1.0
* @package com.ds.validate.group
* @description: 分组校验之用户名密码登录校验
* @author: ds
* @date: 2023/7/22 17:18
*/
public interface LoginUsernameValid{
}
/**
* @version V1.0
* @package com.ds.validate.group
* @description: 分组校验之手机号验证码登录校验
* @author: ds
* @date: 2023/7/22 17:19
*/
public interface LoginMobileValid{
}
实体类LoginEntity:
package com.ds.entity;
import com.ds.validate.Mobile;
import com.ds.validate.group.LoginMobileValid;
import com.ds.validate.group.LoginUsernameValid;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotEmpty;
/**
* @version V1.0
* @package com.ds.entity
* @description: 登录实体类
* @author: ds
* @date: 2023/7/22 20:59
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(value = "登录实体类")
public class LoginEntity {
/**
* 用户名
*/
@ApiModelProperty(value = "用户名")
@NotEmpty(message = "用户名不能为空",groups = {LoginUsernameValid.class})
private String username;
/**
* 密码
*/
@ApiModelProperty(value = "密码")
@Length(min=6,max = 20,message = "密码长度应在6到20之间",groups = {LoginUsernameValid.class})
@NotEmpty(message = "密码不能为空",groups = {LoginUsernameValid.class})
private String password;
/**
* 手机号
*/
@ApiModelProperty(value = "手机号")
@Mobile(groups = {LoginMobileValid.class})
@NotEmpty(message = "手机号不能为空",groups = {LoginMobileValid.class})
private String mobile;
/**
* 短信验证码
*/
@ApiModelProperty(value = "短信验证码")
@NotEmpty(message = "短信验证码不能为空",groups = {LoginMobileValid.class})
private String smsCode;
}
LoginController接口类:
package com.ds.controller;
import com.ds.common.result.Result;
import com.ds.entity.LoginEntity;
import com.ds.validate.group.LoginMobileValid;
import com.ds.validate.group.LoginUsernameValid;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @version V1.0
* @package com.ds.controller
* @description: 登录controller类
* @author: ds
* @date: 2023/7/22 21:07
*/
@RestController
@RequestMapping("/login")
@Api(tags = "登录管理")
public class LoginController {
/**
* 登录方式一:使用用户名和密码登录
* @param loginEntity
* @return
*/
@ApiOperation(value = "用户名密码登录接口")
@GetMapping("/way1")
public Result<String> loginByUsernameAndPassword(@Validated(LoginUsernameValid.class) LoginEntity loginEntity) {
return Result.success("登录成功");
}
/**
* 登录方式二:使用手机号和验证码登录
* @param loginEntity
* @return
*/
@ApiOperation(value = "手机号验证码登录接口")
@GetMapping("/way2")
public Result<String> loginByMobile(@Validated(LoginMobileValid.class) LoginEntity loginEntity) {
return Result.success("登录成功");
}
}
结果:
8、自定义校验
8.1 编写自定义校验注解
package com.ds.validate;
import com.ds.common.constants.GlobalConstants;
import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.constraints.Pattern;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* @version V1.0
* @package com.ds.validate
* @description: 自定义手机号校验注解
* @author: ds
* @date: 2023/7/22 17:01
*/
@Documented
//指定由哪个类执行校验逻辑,否则报错javax.validation.UnexpectedTypeException: HV000030: No validator
// could be found for constraint 'com.ds.validate.Mobile' validating type 'java.lang.String'.
@Constraint(validatedBy = {MobileValidator.class})
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface Mobile {
String message() default GlobalConstants.MOBILE_PATTERN_FAILED;
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
/**
* @return an additional regular expression the annotated element must match. The default
* is any string ('.*')
*/
String regexp() default GlobalConstants.MOBILE_PATTERN;
/**
* @return used in combination with {@link #regexp()} in order to specify a regular
* expression option
*/
Pattern.Flag[] flags() default { };
/**
* Defines several {@code @Email} constraints on the same element.
*
* @see javax.validation.constraints.Email
*/
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
public @interface List {
javax.validation.constraints.Email[] value();
}
}
8.2 编写执行校验逻辑类
package com.ds.validate;
import org.apache.commons.lang3.StringUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
/**
* @version V1.0
* @package com.ds.validate
* @description: 手机号校验类
* @author: ds
* @date: 2023/7/22 17:01
*/
public class MobileValidator implements ConstraintValidator<Mobile,String> {
private String regexp;
@Override
public void initialize(Mobile constraintAnnotation) {
// ConstraintValidator.super.initialize(constraintAnnotation);
this.regexp = constraintAnnotation.regexp();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (StringUtils.isEmpty(value)) {
//为空,不做校验
return true;
}
return value.matches(regexp);
}
}
8.3 使用
/**
* 手机号
*/
@Mobile
@NotEmpty(message = "手机号不能为空")
@ApiModelProperty(value = "手机号")
private String mobile;
结果:
9、源码地址
https://gitee.com/lucky-eagle/spring-boot-demos/tree/master/spring-boot-validation-demo,希望大家觉得有用的话star一下、fork一下哈哈,祝大家天天开心!