Hibernate Validator

在日常开发中,Hibernate Validator经常用来验证bean的字段,基于注解,方便快捷高效。

1、Bean Validation 中内置的 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)    被注释的元素必须符合指定的正则表达式

2、Hibernate Validator 附加的 constraint

@Email    被注释的元素必须是电子邮箱地址
@Length(min=, max=)    被注释的字符串的大小必须在指定的范围内
@NotEmpty    被注释的字符串的必须非空
@Range(min=, max=)    被注释的元素必须在合适的范围内
@NotBlank    被注释的字符串的必须非空
@URL(protocol=,host=,    port=, regexp=, flags=)    被注释的字符串必须是一个有效的url
@CreditCardNumber    被注释的字符串必须通过Luhn校验算法,银行卡,信用卡等号码一般都用Luhn计算合法性
@ScriptAssert(lang=, script=, alias=)    要有Java Scripting API 即JSR 223 ("Scripting for the JavaTM Platform")的实现
@SafeHtml(whitelistType=, additionalTags=)    classpath中要有jsoup包

主要区分下@NotNull @NotEmpty @NotBlank 3个注解的区别:

@NotNull           任何对象的value不能为null,通常用在基本类型上
@NotEmpty       用在集合类上面 集合对象的元素不为0,即集合不为空,也可以用于字符串不为null
@NotBlank        用在String上面 只能用于字符串不为null,并且字符串trim()以后length要大于0

使用

在实体类的属性中添加注解。
之后在统一异常处理类中,进一步解析异常,返回前台信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 数据校验
*/
@ExceptionHandler(BindException.class)
@ResponseBody
public RetViewModel methodArgumentNotValidHandler(BindException e) {
logger.error(Constants.BAD_REQUEST, e);
BindingResult bindingResult = e.getBindingResult();
Map<String, Object> map = getErrors(bindingResult);
RetViewModel responseResult = new RetViewModel(HttpStatus.BAD_REQUEST.value(),map.values().toString(),null);
return responseResult;
}
private Map<String, Object> getErrors(BindingResult result) {
Map<String, Object> map = new HashMap<>();
List<FieldError> list = result.getFieldErrors();
for (FieldError error : list) {
map.put(error.getField(), error.getDefaultMessage());
}
return map;
}

控制器中,直接在对应bean上增加注解@Valid

具体分析如下:

1、简单的demo 配置校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
public class ValidatorConfig {
@Bean
public LocalValidatorFactoryBean localValidatorFactoryBean(){
LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean();
localValidatorFactoryBean.setProviderClass(org.hibernate.validator.HibernateValidator.class);
localValidatorFactoryBean.setValidationMessageSource(reloadableResourceBundleMessageSource());
return localValidatorFactoryBean;
}
public ReloadableResourceBundleMessageSource reloadableResourceBundleMessageSource(){
ReloadableResourceBundleMessageSource r = new ReloadableResourceBundleMessageSource();
r.setBasename("classpath:validationMessages.properties");//资源文件
//r.setFileEncodings();
r.setDefaultEncoding("UTF-8");
r.setCacheSeconds(120);//资源内容缓存时间
return r;
}
}

资源文件内容如:

notnull=该字段不能为空

验证的model里面对字段进行验证

@NotBlank(message = "{notnull}")
private String name;

使用,加入注解即可@Valid

1
2
3
4
5
6
7
public void demo(@Valid Demo demo, BindingResult result){
if(result.hasErrors()){
for (ObjectError error : result.getAllErrors()) {
System.out.println(error.getDefaultMessage());
}
}
}

通常不在控制器方法中写BindingResult,统一放到类去处理,如上面的统一异常处理类。

2、hibernate的校验模式

普通模式(默认是这个模式):会校验完所有的属性,然后返回所有的验证失败信息;快速失败返回模式:只要有一个验证失败,则返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Bean
public Validator validator(){
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.addProperty( "hibernate.validator.fail_fast", "true" )
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

/*ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.failFast( true )
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();*/

return validator;
}

3、hibernate的两种校验

请求参数校验和GET参数校验

请求参数校验,在控制器方法中加注解 @Valid,然后后面加BindindResult,有多个参数对应加多个注解和BindindResult
GET参数校验(@RequestParam参数校验)

如果是参数较少的情况下:

public void demo(@RequestParam int grade,@RequestParam int classroom) {}

使用@Valid注解,对RequestParam对应的参数进行注解,是无效的,需要使用@Validated注解来使得验证生效。

需要使用MethodValidationPostProcessor 的Bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
//MethodValidationPostProcessor默认是普通模式,配置的validator不会生效,通过配置Validator使用validator的快速方式
MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor();
postProcessor.setValidator(validator());
return postProcessor;
}

@Bean
public Validator validator(){
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.addProperty( "hibernate.validator.fail_fast", "true" )
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

return validator;
}

使用:在控制器上加上注解,方法参数直接使用校验规则

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@Validated
public class DemoController {
/**
* 直接在方法参数上使用
*/
@PostMapping("/student")
public String validate1(
@Size(min = 1,max = 10,message = "姓名长度必须为1到10")@RequestParam("name") String name,
@Min(value = 10,message = "年龄最小为10")@Max(value = 100,message = "年龄最大为100") @RequestParam("age") Integer age,
@Future @RequestParam("birth")@DateTimeFormat(pattern = "yyyy-MM-dd hh:mm:ss") Date birth){
return name;
}

统一处理验证不通过的信息

1
2
3
4
5
6
7
8
9
10
11
@ExceptionHandler(ConstraintViolationException.class)
@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
public String handle(ConstraintViolationException e) {
Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
for (ConstraintViolation<?> item : violations) {
item.getMessage();//验证不通过信息
}

return "bad request, " ;
}

4、实体对象校验

实体类中的属性增加对应的规则

方法中可以通过代码直接校验对象

1
2
3
4
Set<ConstraintViolation<Demo>> violationSet = validator.validate(demo);
for (ConstraintViolation<Demo> model : violationSet) {
System.out.println(model.getMessage());
}

5、对象联级校验

对象内部包含另一个对象作为属性,属性上加@Valid,可以验证作为属性的对象内部的验证:

@Valid
private Demo demo;

校验方式和上面一致。

6、分组校验

分组顺序校验时,按指定的分组先后顺序进行验证,前面的验证不通过,后面的分组就不行验证。

设置validator为普通验证模式(”hibernate.validator.fail_fast”, “false”)

两个组GroupA、GroupB:

public interface GroupA {}

public interface GroupB {
}

实体类:

1
2
3
4
5
6
7
8
@NotBlank
@Range(min = 1,max = Integer.MAX_VALUE,message = "必须大于0",groups = {GroupA.class})
private Integer userId;
@NotBlank
@Length(min = 4,max = 20,message = "必须在[4,20]",groups = {GroupB.class})
private String userName;
@NotBlank
@Range(min = 0,max = 100,message = "年龄必须在[0,100]",groups={Default.class})
GroupA验证字段userId;
GroupB验证字段userName、sex;
Default验证字段age(Default是Validator自带的默认分组)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void demo5(){
Student s = new Student();
//GroupA验证不通过
s.setUserId(-12);
//GroupA验证通过
//p.setUserId(12);
s.setUserName("a");
s.setAge(110);
s.setSex(5);
Set<ConstraintViolation<Student>> validate = validator.validate(s, GroupA.class, GroupB.class);
for (ConstraintViolation<Student> item : validate) {
System.out.println(item);
}
}

如果在参数中使用bean,加入注解即可:@Validated({GroupA.class, GroupB.class})

如果使用组验证顺序:

@GroupSequence({GroupA.class, GroupB.class, Default.class})
    public interface GroupOrder {
}

定义好,使用时,直接用GroupOrder

7、自定义验证器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 自定义一个注解用来校验
*/
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {ValidatorDemo.class})
public @interface ValidateDemo {

String message() default "Not Null";

String name();

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

Class<? extends Payload>[] payload() default {};
}

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ValidatorDemo implements ConstraintValidator<ValidateDemo, Student> {

private String name;

/**
* 用于初始化注解上的值到这个validator
*/
@Override
public void initialize(ValidateDemo constraintAnnotation) {
name =constraintAnnotation.name();
}

/**
* 具体的校验逻辑
*/
@Override
public boolean isValid(Student value, ConstraintValidatorContext context) {
return name ==null || name.equals(value.getName());
}
}

这样就可以直接使用@ValidateDemo在属性或者参数上校验,类似@NotBlank,@Max一样的使用方式。

参考:http://docs.jboss.org/hibernate/validator/4.2/reference/zh-CN/html_single/#validator-gettingstarted

以上是Hibernate Validator校验方式,一般还可以用拦截器实现,比如创建一个校验注解来实现(针对单一参数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@Target({ElementType.PARAMETER})//参数级别
@Retention(RetentionPolicy.RUNTIME) //注解保留到运行阶段
public @interface ParamsNotNull {

}

/**
* 拦截器,直接在参数上就可以校验参数
*/
public class CheckParamsInterceptor extends HandlerInterceptorAdapter {
private static Logger logger = LoggerFactory.getLogger(CheckParamsInterceptor.class);

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
logger.warn("UnSupport handler");
return true;
}
List<String> list = getParamsName((HandlerMethod) handler);
for (String s : list) {
String parameter = request.getParameter(s);
if (StringUtils.isEmpty(parameter)) {
//可以增加更多的规则,还可以吧规则交给注解去定义
JSONObject jsonObject = new JSONObject();
jsonObject.put("status", 202);
jsonObject.put("msg", "缺少必要参数");
response.setHeader("Content-type", "application/json;charset=UTF-8");
response.setHeader("Access-Control-Allow-Origin", "*");//跨域
response.getWriter().write(jsonObject.toJSONString());
return false;
}
}
return true;
}

/**
* 获取使用了该注解的参数名称
*/
private List getParamsName(HandlerMethod handlerMethod) {
Parameter[] parameters = handlerMethod.getMethod().getParameters();
List<String> list = new ArrayList<>();
for (Parameter parameter : parameters) {
if (parameter.isAnnotationPresent(ParamsNotNull.class)) {
list.add(parameter.getName());
}
}
return list;
}
}

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
CheckParamsInterceptor checkSourceInterceptor = new CheckParamsInterceptor();
//增加校验拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(checkSourceInterceptor).addPathPatterns("/**");
}
//跨域
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**");
}
}

这样就可以在控制器方法上对应参数加上注解即可。

还有一些spring mvc一定定义好的一些异常,通过统一处理异常的方式去校验参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//参数类型不匹配
//getPropertyName()获取数据类型不匹配参数名称
//getRequiredType()实际要求客户端传递的数据类型
@ExceptionHandler({TypeMismatchException.class})
@ResponseBody
public String requestTypeMismatch(TypeMismatchException ex){
ex.printStackTrace();
return outputJson(400, "参数类型不匹配,参数" + ex.getPropertyName() + "类型应该为" + ex.getRequiredType());
}
//缺少参数异常
//getParameterName() 缺少的参数名称
@ExceptionHandler({MissingServletRequestParameterException.class})
@ResponseBody
public String requestMissingServletRequest(MissingServletRequestParameterException ex){
ex.printStackTrace();
return outputJson(400, "缺少必要参数,参数名称为" + ex.getParameterName());
}

代码见:
https://github.com/huingsn/tech-point-record.git中的springboot-validation-demo