Spring Validation 的使用

栏目: Hibernate · 发布时间: 6年前

内容简介:目前在新乐才以及餐学院的项目中,参数校验的工作都在前端完成,而后端接口只处理业务逻辑,但是这种方式不太合理,绕过页面直接进行http请求,会有系统异常以及脏数据的风险,所以推荐使用Bean Validation 基于Hibernate Validator是 Bean Validation 的参考实现。Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,目前已升级到举例Bean Validation 中的 constraint (约束,限制),Bean
  • zhangxs
  • 2019-4-14

使用背景

目前在新乐才以及餐学院的项目中,参数校验的工作都在前端完成,而后端接口只处理业务逻辑,但是这种方式不太合理,绕过页面直接进行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

约束 限制
@Email 被注释的元素必须是电子邮箱地址
@Length 被注释的字符串的大小必须在指定的范围内
@NotEmpty 被注释的字符串的必须非空
@Range 被注释的元素必须在合适的范围内

使用方法

Bean Validation 是JDK 1.6 +后内置的,包名为javax.validation.constraints

Hibernate Validator 则需要引入jar包,包名为org.hibernate.validator.constraints

POM.xml

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.1.Final</version> 
</dependency>
复制代码

实体类

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<DateFormatByPattern, String> {

    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
}
复制代码

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Machine Learning

Machine Learning

Kevin Murphy / The MIT Press / 2012-9-18 / USD 90.00

Today's Web-enabled deluge of electronic data calls for automated methods of data analysis. Machine learning provides these, developing methods that can automatically detect patterns in data and then ......一起来看看 《Machine Learning》 这本书的介绍吧!

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具