Hibernate
不仅仅为操作数据库提供了解决方案,还为数据校验提供了解决方案——Hibernate Validator
。本篇博客将介绍常用的Validator
注解的使用以及在Validator
不满足实际需求的情况下如何使用自定义Validator
来实现数据校验。
首先我们需要在项目的POM
文件中添加Hibernate Validator
的依赖才可以使用它的数据校验器进行数据校验。由于Spring Boot
已经将Hibernate Validator
集成到了spring-boot-starter-web
包里,所以这里不需要额外引用Hibernate Validator
依赖。常用的校验注解下表所示:
注解 | 说明 |
---|---|
@NotNull | 值不能为空 |
@Null | 值必须为空 |
@Pattern(regex=) | 字符串必须匹配正则表达式 |
@Size(min=, max=) | 集合元素数量必须在min和max之间 |
@CreditCardNumber(ignoreNonDigitCharaters=) | 字符串必须是信用卡号(美国标准信用卡) |
字符串必须是Email地址 | |
@Length(min=, max=) | 字符串长度必须在min和max之间 |
@NotBlank | 字符串必须有字符 |
@NotEmpty | 字符串不为null,集合必须有元素 |
@Range(min=, max=) | 数字必须在min和max之间 |
@SafeHtml | 字符串是安全的HTML |
@URL | 字符串是合法的URL |
@AssertFalse | 值必须是false |
@AssertTrue | 值必须是true |
@DecimalMax(value=, inclusive=) | 如果inclusive=true,那么值必须大于等于value,如果inclusive=false,那么值必须大于value |
@DecimalMin(value=, inclusive=) | 如果inclusive=true,那么值必须小于等于value,如果inclusive=false,那么值必须小于value |
@Digits(integer=, fraction=) | 数字格式检查,integer是指整数部分最大长度,fraction是指小数部分最大长度 |
@Future | 值必须是未来的日期 |
@Past | 值必须是过去的日期 |
@Max(value=) | 值必须小于等于value指定的值,不能注释在字符串类型属性上 |
@Min(value=) | 值必须大于等于value指定的值,不能注释在字符串类型属性上 |
主要区分下@NotNull
、@NotEmpty
、@NotBlank
3个注解的区别:
@NotNull
任何对象的value
不能为null
@NotEmpty
集合对象的元素不为0,即集合不为空,也可以用于字符串不为null
@NotBlank
只能用于字符串不为null
,并且字符串trim()
以后length
要大于0
其实以上的每个注解都有三个共同的属性,因为他们都遵循JSR 303
规范:
String message() default "{org.hibernate.validator.constraints.xxx.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
message
提供校验失败后的错误消息;
groups
分组验证
payload
承载元数据
为了测试注解的作用,我在User
类的属性上加了部分注解,如下所示:
@NotEmpty(message = "用户名不能为空")
private String username;
@NotEmpty(message = "密码不能为空")
private String password;
@Past(message = "生日必须是过去的日期")
private Date birthday;
这里我写一个创建用户的测试用例和Controller,并人为设置不符合要求的数据,测试用例代码如下:
@Test
public void create3() throws Exception {
Date date = new Date(LocalDateTime.now().plusYears(1).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
String content = "{\"username\":null,\"password\":null,\"birthday\":" + date.getTime() + "}";
mockMvc.perform(MockMvcRequestBuilders.post("/user3")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(content))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.id").value(3));
}
为了测试效果,我设置的时间不是过去的时间,而是未来一年的时间,LocalDateTime.now()
获取当前时间,plusYears(1)
加上一年,atZone(ZoneId.systemDefault())
设置当前时区为系统默认时区,最后再毫秒化。
Controller
方法为:
@PostMapping("/user3")
public User create3(@RequestBody @Valid User user, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
bindingResult.getAllErrors().forEach(error -> System.out.println(error.getDefaultMessage()));
}
user.setId(3);
return user;
}
这里对错误消息进行了循环打印,打印结果是:
用户名不能为空
生日必须是过去的日期
密码不能为空
@Valid
注解在数据封装之间会对数据的合法性进行校验,并将校验的错误结果存储在BindingResult
对象中。
以上的所有注解都是Java
给我们提供的,其实他们往往只能校验一些简单的值,在实际开发中,我们面临的校验可能会很复杂,所以校验逻辑往往需要我们自己来写,这时候就需要我们自定义校验注解了。接下来我以校验身份证号码的案例来说明如何实现自定义的校验注解。
一般来说,自定义校验注解的开发步骤有以下几步:
第一步: 编写校验注解,但是需要注意的是,自定义的校验注解也得和其他Java
提供的校验注解一样,必须拥有message
、groups
、payload
三个属性。
第二步: 编写自定义校验的逻辑实体类,这个类必须实现ConstraintValidator
这个接口,这样才可以被注解用来校验。
第三步: 编写具体的校验逻辑。
package com.lemon.security.web.validator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义身份证号码校验注解
*
* @author lemon
* @date 2018/3/31 下午7:43
*/
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = IdCardValidator.class)
public @interface IsIdCard {
String message();
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
对上面的代码进行如下解释:
message
、groups
、payload
三个属性是必须的,可以参考@NotNull等注解;
@Target
注解是指定当前自定义注解可以使用在哪些地方,这里仅仅让他可以使用在方法上和属性上;
@Retention
指定当前注解保留到运行时;
@Constraint
指定了当前注解使用哪个类来进行校验。
package com.lemon.security.web.validator;
import com.lemon.security.web.service.IdCardValidatorService;
import org.springframework.beans.factory.annotation.Autowired;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
/**
* 校验注解的校验逻辑
*
* @author lemon
* @date 2018/3/31 下午7:57
*/
public class IdCardValidator implements ConstraintValidator<IsIdCard, String> {
@Autowired
private IdCardValidatorService idCardValidatorService;
/**
* 校验前的初始化工作
*
* @param constraintAnnotation 自定义的校验注解
*/
@Override
public void initialize(IsIdCard constraintAnnotation) {
String message = constraintAnnotation.message();
System.out.println("用户自定义的message信息是:".concat(message));
}
/**
* 具体的校验逻辑方法
*
* @param value 需要校验的值,从前端传递过来
* @param context 校验器的校验环境
* @return 通过校验返回true,否则返回false
*/
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return idCardValidatorService.valid(value);
}
}
对以上代码进行如下解释:
IdCardValidator
实现了接口ConstraintValidator
,其后面的第一个泛型是指为哪个注解提供校验服务,第二个泛型是指需要校验的值的类型;
message
内容;第二个方法的第一个参数是需要被校验的值,第二个参数是校验的上下文环境;
Service
服务,并通过Spring
的DI
注入到这个校验类中,需要注意的是,这个校验类上并不需要添加Spring
的Component
等注解,Spring
可以自动将校验逻辑服务类实例对象注入到这个类中。在这里就注入了IdCardValidatorService
实现类对象。
接口:
package com.lemon.security.web.service;
/**
* @author lemon
* @date 2018/3/31 下午8:11
*/
public interface IdCardValidatorService {
/**
* 身份证号校验,支持18位、15位和港澳台的10位
*
* @param value 需要被校验的值
* @return 校验通过返回true,否则返回false
*/
boolean valid(String value);
}
实现类:
package com.lemon.security.web.service.impl;
import cn.hutool.core.util.IdcardUtil;
import com.lemon.security.web.service.IdCardValidatorService;
import org.springframework.stereotype.Service;
/**
* @author lemon
* @date 2018/3/31 下午8:14
*/
@Service
public class IdCardValidatorServiceImpl implements IdCardValidatorService {
@Override
public boolean valid(String value) {
return IdcardUtil.isValidCard(value);
}
}
这里的校验逻辑采用的是Hutool
提供的工具包进行校验的,具体可以参考它的文档。
这就完成了自定义校验注解的完整案例编写,接下来进行提供RESTful
风格的API
进行测试。在测试之前,请在原来的User
类上加上idCard
属性,并加上@IsIdCard
注解。
@IsIdCard(message = "身份证号码必须是大陆的18位或者15位,或者是港澳台的10位")
private String idCard;
测试方法:
@Test
public void create4() throws Exception {
Date date = new Date(LocalDateTime.now().plusYears(1).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
String content = "{\"username\":null,\"password\":null,\"birthday\":" + date.getTime() + ",\"idCard\":\"12345678\"}";
mockMvc.perform(MockMvcRequestBuilders.post("/user4")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(content))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.id").value(4));
}
Controller
方法:
@PostMapping("/user4")
public User create4(@RequestBody @Valid User user, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
bindingResult.getAllErrors().forEach(error -> System.out.println(error.getDefaultMessage()));
}
user.setId(4);
return user;
}
在上面的测试方法中,随便设置了一个不合法的身份证号码,显然是会校验失败的,最后的打印结果是:
用户自定义的`message`信息是:身份证号码必须是大陆的18位或者15位,或者是港澳台的10位
身份证号码必须是大陆的18位或者15位,或者是港澳台的10位
用户名不能为空
生日必须是过去的日期
密码不能为空
请认真思考上面的一个自定义校验注解的流程,可以轻松掌握在后期的开发中,使用注解来实现校验,而不是写许多重复的校验逻辑代码。
示例代码下载地址:
项目已经上传到码云,欢迎下载,内容所在文件夹为
chapter003
。