简单介绍
JSR303规范定义了Bean校验的标准validation-api,但没有实现。
hibernate validation则是对这个规范的实现。
Spring validation则是对hibernate validation的封装,方便在spring项目中使用
引入依赖
如果spring-boot版本小于2.3.x,spring-boot-starter-web会自动传入hibernate-validator依赖。如果spring-boot版本大于2.3.x,则需要手动引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.3.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.1.Final</version>
</dependency>
编写实体类
在实体类上加校验注解,phone字段目前只有@NotNull,后期自定义注解校验手机号码格式
@Data
public class User {
@NotNull
@Length(min = 2, max = 12)
private String name;
@NotNull
private String phone;
@Email
@NotNull
private String email;
}
编写controller
@RestController
@RequestMapping("user")
public class UserController {
@RequestMapping("/saveUser")
public User saveUser(@Validated @RequestBody User user) {
User userInfo = new User();
BeanUtils.copyProperties(user, userInfo);
return userInfo;
}
}
启动类和配置就不展示了,接着postman测试,来看结果
postman会返回400状态码和"Bad Request",是因为系统抛出了 MethodArgumentNotValidException 方法参数异常,查看控制台可以看到:
但我们通常需要返回友好的格式给前端。
所以我们编写一个
返回类 R
@Data
public class R {
private Boolean success;
private Integer code;
private String message;
private Map<String, Object> data = new HashMap<>();
private R() {}
private static R ok() {
R r = new R();
r.setCode(ResultCode.SUCCESS);
r.setMessage("成功");
r.setSuccess(true);
return r;
}
private static R error() {
R r = new R();
r.setSuccess(false);
r.setCode(ResultCode.ERROR);
r.setMessage("失败");
return r;
}
private R data(String key, Object value) {
this.data.put(key,value);
return this;
}
private R data(Map<String, Object> data) {
this.setData(data);
return this;
}
private R success(boolean success) {
this.setSuccess(success);
return this;
}
public R message(String message) {
this.setMessage(message);
return this;
}
public R code(Integer code) {
this.setCode(code);
return this;
}
}
interface ResultCode {
public static Integer SUCCESS = 20000;
public static Integer ERROR = 20001;
}
这个时候我们修改controller,并把@validated注解去掉,看看效果
@RestController
@RequestMapping("user")
public class UserController {
@RequestMapping("/saveUser")
public R saveUser(@Validated @RequestBody User user) {
User userInfo = new User();
BeanUtils.copyProperties(user, userInfo);
return R.ok().data("data", userInfo);
}
}
接着我们为了校验的时候也返回这个格式的数据,需要编写自定义异常,统一返回格式
@RestControllerAdvice
@Slf4j
public class ExceptionControllerAdvice {
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handleValidException(MethodArgumentNotValidException e) {
log.error("数据校验出现问题{},异常类型{}", e.getMessage(), e.getClass());
BindingResult bindingResult = e.getBindingResult();
HashMap<String, Object> errorMap = new HashMap<>();
bindingResult.getFieldErrors().forEach(item->{
System.out.println(item);
errorMap.put(item.getField(), item.getDefaultMessage());
});
return R.error().data(errorMap);
}
}
postman测试参数都不输入的情况下返回的数据:
再测试当 email 参数输入格式不对时返回的数据:
自此就完成了最基本的参数校验
接着来自定义校验注解来完成对手机号码的格式校验
自定义注解
编写一个注解
@Documented
@Constraint(
validatedBy = {PhoneNumberConstraintValidator.class}
)
// 注解可以放在xx上
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PhoneNumber {
// 注解的提示信息
String message() default "电话号码格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
编写一个类需要实现 ConstraintValidator 接口:
public class PhoneNumberConstraintValidator implements ConstraintValidator<PhoneNumber, String> {
private static final Pattern PATTERN = Pattern.compile("^[1]\\d{10}$");
@Override
public void initialize(PhoneNumber constraintAnnotation) {
}
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
if (value != null) {
Matcher matcher = PATTERN.matcher(value);
return matcher.find();
}
return true;
}
}
最后在实体类加上注解 @PhoneNumber
@NotNull
@PhoneNumber
private String phone;
最后postman测试,故意输错电话号码格式
分组校验
在实际应用中,多个方法使用的实体类可能是同一个,而不同方法的校验规则可能不一样,比如新增和修改,两个方法都使用 @RequestBody 接收实体参数,但是新增不需要传递id,而修改需要,这时就会产生冲突,可以设置分组校验来解决。
首先需要新增两个接口作为分组依据:saveUser updateUser
public interface SaveUser {
}
public interface UpdateUser {
}
然后修改实体类,加上分组
@Data
public class User {
@NotNull(groups = UpdateUser.class)
private String id;
@NotNull(groups = {UpdateUser.class, SaveUser.class})
@Length(min = 2, max = 12, groups = {UpdateUser.class, SaveUser.class})
private String name;
@NotNull(groups = {UpdateUser.class, SaveUser.class})
@PhoneNumber(groups = {UpdateUser.class, SaveUser.class})
private String phone;
@Email(groups = {UpdateUser.class, SaveUser.class})
@NotNull(groups = {UpdateUser.class, SaveUser.class})
private String email;
}
修改controller
@RestController
@RequestMapping("user")
public class UserController {
@RequestMapping("/saveUser")
public R saveUser(@Validated(SaveUser.class) @RequestBody User user) {
User userInfo = new User();
BeanUtils.copyProperties(user, userInfo);
return R.ok().data("data", userInfo).message("新增成功");
}
@RequestMapping("/updateUser")
public R updateUser(@Validated(UpdateUser.class) @RequestBody User user) {
User userInfo = new User();
BeanUtils.copyProperties(user, userInfo);
return R.ok().data("data", userInfo).message("修改成功");
}
}
最后在postman中测试
当调用修改方法,参数中加上id值则显示成功,而不加上id则失败
当调用新增方法时即时没加上id也不会报错,因为设置了分组校验