使用背景
目前在项目中,参数校验的工作都在前端完成,而后端接口只处理业务逻辑,但是这种方式不太合理,绕过页面直接进行http请求,会有系统异常以及脏数据的风险,所以推荐使用Bean Validation 基于 JSR 303 - Bean Validation参数校验框架在后端接口做参数校验,格式化校验,以及参数可选范围的校验,这样既能规避大部分因参数缺失而产生的系统异常,也能在接口联调阶段,提高联调效率,减少前后端同学在联调时排查问题的时间
Hibernate Validator 是 Bean Validation 的参考实现。Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,目前已升级到Bean Validation 2.0 / JSR - 380,除此之外还有一些附加的 constraint。该Hibernate不是ORM的Hibernate
举例Bean Validation 中的 constraint (约束,限制),Bean Validation 的注解在javax.validation.constraints下
约束 | 限制 |
---|---|
@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) | 被注释的元素必须符合指定的正则表达式 |
Hibernate Validator 附加的 constraint / Hibernate Validator是JSR - 303 的最好实现,目前规范已升级到 JSR
约束 | 限制 |
---|---|
被注释的元素必须是电子邮箱地址 | |
@Length | 被注释的字符串的大小必须在指定的范围内 |
@NotEmpty | 被注释的字符串的必须非空 |
@Range | 被注释的元素必须在合适的范围内 |
使用方法
Bean Validation 是JDK 1.6 +后内置的,包名为javax.validation.constraints
Hibernate Validator 则需要引入jar包,包名为org.hibernate.validator.constraints
POM.xml
org.hibernate
hibernate-validator
6.0.1.Final
复制代码
实体类
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import java.util.Date;
public class ValidationDemo {
private String id;
@Length(min = 2, max = 6, message = "用户名长度要求在{min}-{max}之间")
@NotNull(message = "用户名不可为空")
private String userName;
@Email(message = "邮箱格式错误")
private String email;
@Past(message = "出生日期错误")
private Date birthDay;
@Min(value = 18, message = "年龄错误")
@Max(value = 80, message = "年龄错误")
private Integer age;
@Range(min = 0, max = 1, message = "性别选择错误")
private Integer sex;
}
复制代码
关于@Valid和Validated的比较,根据实际需求需求选择
@Valid : 没有分组功能,可以用在方法、构造函数、方法参数和成员属性(field)上,如果一个待验证的pojo类,其中还包含了待验证的对象,需要在待验证对象上注解@valid,才能验证待验证对象中的成员属性
@Validated :提供分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制,用在类型、方法和方法参数上。但不能用于成员属性(field)。
Controller
-- @Valid 表示对该实体进行校验
-- BindingResult 则保存对参数的校验结果
@RequestMapping(value = "validation", method = RequestMethod.POST)
public JsonResult validation(@Valid @RequestBody ValidationDemo demo, BindingResult result) {
JsonResult jsonResult = new JsonResult();
if (result.hasErrors()) {
result.getAllErrors().forEach(err -> {
jsonResult.setCode(ApiConstants.JsonResult.FAIL);
jsonResult.setMsg(err.getDefaultMessage());
});
}
return jsonResult;
}
复制代码
RequestBody
{
"age": 19,
"birthDay": "2019-04-14T09:05:39.604Z",
"email": "string",
"id": "string",
"sex": 0,
"userName": "string"
}
复制代码
Response
{
"code": 1,
"msg": "邮箱格式错误",
"total": 0,
"totalpage": 0
}
复制代码
由此可见,参数的校验已经生效,因为email不符合@Email的校验规则,具体校验规则可以查看@Email的实现EmailValidator.java
userName 的错误message 里面有{min} - {max} ?
RequestBody
{
"age": 19,
"birthDay": "2019-04-14T09:05:39.604Z",
"email": "string",
"id": "string",
"sex": 0,
"userName": ""
}
复制代码
Response
{
"code": 1,
"msg": "用户名长度要求在2-6之间",
"total": 0,
"totalpage": 0
}
复制代码
Hibernate Validator 通过EL表达式获取到了在@length中定义的min以及max属性的值
在上面的Controller中,需要在在接口参数中,增加一个BindingResult来接收校验的结果,每一个BindingResult与@Valid是一一对应的,如果有多个@Valid,那么需要对个BindResult来保存校验结果
进阶使用,统一处理校验结果并返回前端
在 ResponseEntityExceptionHandler (Line 162) 中,如果验证出现异常的时候是抛出了MethodArgumentNotValidException
MethodArgumentNotValidException 描述:
Exception to be thrown when validation on an argument annotated with {@code @Valid} fails.
当使用@Valid注解的参数验证失败是抛出异常
复制代码
所以在BaseController中对MethodArgumentNotValidException进行处理
Controller
-- 对接口进行简化,通过异常捕获的方式对校验结果返回给前端
@RequestMapping(value = "validation", method = RequestMethod.POST)
public JsonResult validation(@Valid @RequestBody ValidationDemo demo) {
return null;
}
复制代码
BaseController
if (e instanceof MethodArgumentNotValidException) {
res.setCode(ApiConstants.JsonResult.FAIL);
res.setMsg(JSONArray.toJSONString(((MethodArgumentNotValidException) e).getBindingResult().getAllErrors().stream().map(ObjectError::getDefaultMessage).collect(Collectors.toList())));
}
复制代码
Response
{
"code": 1,
"msg": "[\"年龄错误\",\"邮箱格式错误\"]",
"total": 0,
"totalpage": 0
}
复制代码
分组校验
在实际使用中,有可能我们针对一个属性,有多个校验规则,这时候就要使用到分组校验了
改造实体
public class ValidationDemo {
private String id;
@Length(min = 2, max = 6, message = "用户名长度要求在{min}-{max}之间")
@NotNull(message = "用户名不可为空")
private String userName;
// 表示分组为Adult时使用该校验规则
@Email(message = "邮箱格式错误")
@NotBlank(message = "邮箱不可为空", groups = {ValidationDemo.Adult.class})
private String email;
@Past(message = "出生日期错误")
private Date birthDay;
@Min(value = 18, message = "年龄错误")
@Max(value = 80, message = "年龄错误")
private Integer age;
@Range(min = 0, max = 1, message = "性别选择错误")
private Integer sex;
// 添加两个分组
public interface Adult {
}
public interface Minor {
}
}
复制代码
测试一下
// 这里将分组设置为Minor,目的是不校验邮箱字段
@RequestMapping(value = "validation", method = RequestMethod.POST)
public JsonResult validation(@Validated({ValidationDemo.Adult.class}) @RequestBody ValidationDemo demo) {
return null;
}
RequestBody:
{
"age": 0,
"birthDay": "2019-04-14T10:39:08.501Z",
"email": "",
"id": "string",
"sex": 0,
"userName": "string"
}
Response:
{
"code": 1,
"msg": "[\"邮箱不可为空\"]",
"total": 0,
"totalpage": 0
}
复制代码
如果是接口使用Minor分组呢?
RequestBody:
{
"age": 0,
"birthDay": "2019-04-14T10:39:08.501Z",
"email": "",
"id": "string",
"sex": 0,
"userName": "string"
}
Response:
{
"code": 0,
"data": [
{}
],
"extra": "string",
"msg": "string",
"result": {},
"total": 0,
"totalpage": 0
}
复制代码
并没有提示邮箱不可为空,由此可见,分组验证已经生效
自定义校验规则
例如新建一个自定义日期格式的校验
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
@Constraint(validatedBy = {DateFormatByPatternValidator.class})
public @interface DateFormatByPattern {
String pattern() default "yyyy-MM-dd HH:mm";
//默认错误消息
String message() default "日期格式错误";
//分组
Class>[] groups() default {};
//负载
Class extends Payload>[] payload() default {};
}
复制代码
同时新建一个对应的校验器
public class DateFormatByPatternValidator implements ConstraintValidator {
private DateFormatByPattern dateFormatByPattern;
@Override
public void initialize(DateFormatByPattern constraintAnnotation) {
dateFormatByPattern = constraintAnnotation;
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
//假如参数为空的话,返回true,如果要对参数值进行非空校验的话,通过@NotNull来校验,这样与日期格式校验解耦
if (StringUtils.isNotBlank(value)) {
String pattern = dateFormatByPattern.pattern();
SimpleDateFormat dateFormat = new SimpleDateFormat(pattern);
try {
dateFormat.parse(value);
} catch (ParseException e) {
return false;
}
}
return true;
}
}
复制代码
改造实体
//使用自定义规则校验前端参数
@DateFormatByPattern(pattern = "yyyy-MM-dd")
//因为同时用到了分组校验,所以在stringDate上添加@Valid,使校验生效
@Valid
private String stringDate;
复制代码
测试一下
RequestBody:
{
"age": 0,
"birthDay": "2019-04-15T08:23:21.683Z",
"email": "",
"id": "string",
"sex": 0,
"stringDate": "string",
"userName": "string"
}
Response:
{
"code": 1,
"msg": "[\"日期格式错误\",\"邮箱不可为空\",\"年龄错误\"]",
"total": 0,
"totalpage": 0
}
复制代码
由此可见,自定义校验已生效
参考文档
Hibernate Validation自定义注解校验
自定义校验器注解
SpringMVC集成Bean Validation 1.0(JSR-303)
通过Hibernate-Validation进行参数验证