前言
之前也用过hibernate的校验注解,但是没有去做一个总结,这里参考一篇博客去做一个总结。简述JSR303/JSR-349,hibernate validation,spring validation之间的关系。JSR303是一项标准,JSR-349是其的升级版本,添加了一些新特性,他们规定一些校验规范即校验注解,如@Null,@NotNull,@Pattern,他们位于javax.validation.constraints包下,只提供规范不提供实现。而hibernate validation是对这个规范的实践(不要将hibernate和数据库orm框架联系在一起),他提供了相应的实现,并增加了一些其他校验注解,如@Email,@Length,@Range等等,他们位于org.hibernate.validator.constraints包下。而万能的spring为了给开发者提供便捷,对hibernate validation进行了二次封装,显示校验validated bean时,你可以使用spring validation或者hibernate validation,而spring validation另一个特性,便是其在springmvc模块中添加了自动校验,并将校验信息封装进了特定的类中。这无疑便捷了我们的web开发。本文主要介绍在springmvc中自动校验的机制。
引入依赖
因为我们构建的是spring boot项目,所以直接引入web的starter的依赖即可。1
2
3
4
5
6<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
如果查看其子依赖,可以发现如下的依赖:1
2
3
4
5
6
7
8<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
进行校验
校验的实体类
这里用了lombok的@Data注解,也是非常推荐大家使用的一个插件。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@Data
public class ValidateBO {
@NotBlank(message = "name不能为空")
private String name;
@Min(value = 18, message = "年龄不能小于18岁")
private Integer age;
@Email(message = "email格式错误")
private String email;
/**
* 自定义注解 不能包含空格字符串
*/
@CannotHaveBlank
private String blank;
/**
* 正则校验
*/
@Pattern(regexp = "^1(3|4|5|7|8)\\d{9}$", message = "手机号码格式错误")
private String phone;
}
可以看到这里用到了一些常见的注解。(自定义校验注解在下边会提到)
controller进行校验
在controller中进行这个字段的校验,可以看到每个需要校验的对象,都需要一个BindingResult去承接校验的结果,并且也要对要校验的类去加上@Validated注解。1
2
3
4
5
6
7
8
9
10
11
12@GetMapping(value = "/validate")
public String validate(ValidateBO validateBO, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
StringBuilder stringBuilder = new StringBuilder();
for (String s : bindingResult.getFieldErrors().stream()
.map(FieldError::getDefaultMessage).collect(Collectors.toList())) {
stringBuilder.append(s);
}
return stringBuilder.toString();
}
return "success";
}
启动项目,在url中输入对项目进行debug。
可以看到,校验在碰到第一个字段不符合要求之后,并不是直接返回错误,而是会对所有的要校验字段去校验。当然这个也是可以配置的,下边会提到fast-fail的配置。最后返回的结果:
这里是打印出了这个对象中所有的错误。
常见的校验注解
JSR提供的校验注解:1
2
3
4
5
6
7
8
9
10
11
12
13@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(regex=,flag=) 被注释的元素必须符合指定的正则表达式
Hibernate Validator提供的校验注解:1
2
3
4
5@NotBlank(message =) 验证字符串非null,且长度必须大于0
@Email 被注释的元素必须是电子邮箱地址
@Length(min=,max=) 被注释的字符串的大小必须在指定的范围内
@NotEmpty 被注释的字符串的必须非空
@Range(min=,max=,message=) 被注释的元素必须在合适的范围内
分组校验
场景
如果同一个类,在不同的使用场景下有不同的校验规则,那么可以使用分组校验。未成年人是不能喝酒的,而在其他场景下我们不做特殊的限制,这个需求如何体现同一个实体,不同的校验规则呢?
校验对象
1 | @Data |
这就定义了只有在在adult组内才会进行最小值18的校验。
进行验证
1 | /** |
自定义注解
实现一个注解
这里去实现一个字符串中不能含有blank空格。主要分为两步:
先去定义这个注解,其中validatedBy指定的是真正去做校验的实体类。而其中的groups和payload可以直接用默认。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24@Target({METHOD, FIELD, ANNOTATION_TYPE, ElementType.CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
// 这个注解是引入真正的去做验证的类
@Constraint(validatedBy = {CannotHaveBlankValidator.class})
public @interface CannotHaveBlank {
// 默认错误信息
String message() default "不能包含空格";
// 分组
Class<?>[] groups() default {};
//负载
Class<? extends Payload>[] payload() default {};
//指定多个时使用
@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
@interface List {
CannotHaveBlank[] value();
}
}第二步是去实现真正去做校验的实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public class CannotHaveBlankValidator implements ConstraintValidator<CannotHaveBlank, String> {
@Override
public void initialize(CannotHaveBlank cannotHaveBlank) {
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
//null时不进行校验
if (value != null && value.contains(" ")) {
//获取默认提示信息
String defaultConstraintMessageTemplate = context.getDefaultConstraintMessageTemplate();
System.out.println("default message :" + defaultConstraintMessageTemplate);
//禁用默认提示信息
context.disableDefaultConstraintViolation();
//设置提示语
context.buildConstraintViolationWithTemplate("can not contains blank")
.addConstraintViolation();
return false;
}
return true;
}
}
这里去实现类去实现了ConstraintValidator接口,这个接口中包含一个初始化事件方法和一个判断是否合法的方法:1
2
3
4
5
6
7
8
9package javax.validation;
import java.lang.annotation.Annotation;
public interface ConstraintValidator<A extends Annotation, T> {
void initialize(A var1);
boolean isValid(T var1, ConstraintValidatorContext var2);
}
其中的A泛型参数是上一步定义的注解类,泛型T是要去校验的字段类型。
ConstraintValidatorContext 这个参数上下文包含了认证中所有的信息,我们可以利用这个上下文实现获取默认错误提示信息,禁用错误提示信息,改写错误提示信息等操作。
自定义注解进行校验
还是用第一个controller去验证这个自定义注解。因为要校验的对象中加入了自定义注解的blank字段。启动项目,输入http://localhost:7001/validate?blank=19 209(这里加了空格),可以看到返回值是:
说明自定义注解起到了作用。
@Valid 和 @Validated的区别
https://blog.csdn.net/qq_27680317/article/details/79970590
这篇讲的很清晰了。
aop
很显然,如果我们每个controller中的方法都去写BindingResult就显得很麻烦了,其实我们就是要对参数进行校验并且输出到log中,这就很自然的想到了aop。
注解标识
定义一个注解去标识使用了hibernate validate注解1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21package com.zhanglijun.springbootdemo.domain.anno;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.groups.Default;
/**
* 用来表示开启hibernate校验的注解
* @author 夸克
* @create 2018/8/19 22:28
*/
@Target({ElementType.METHOD,ElementType.TYPE,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EnableValidate {
Class<?> [] groups() default { Default.class };//校验分组信息
}
定义切面
1 | package com.zhanglijun.springbootdemo.aspect; |
这个切面说的也很清除,对多处使用这个的地方都去做了一个校验。
github
上述代码都在我的github,可以在review一下代码。
validated注解相关
引用
参考的博客: