JSR 303 - Bean Validation的使用(数据校验)

简介:

在我们开发的过程中,对于数据的校验是一项很重要的工作。尤其是现在前后端分离的情况下,我们无法保证调用者传过来的数据就是一定符合规则的,因此为了程序的健壮性,我们要尽可能的做出些防范措施。

JSR 303 是java为Bean数据合法性校验提供的标准框架,JSR-303 是JAVA EE 6 中的一项子规范,叫做Bean Validation,官方参考实现是hibernate Validator。

JSR303 用于对JavaBean 中的字段的值进行验证,使得验证逻辑从业务代码中脱离出来,常用于表单数据的验证。这里要注意,因为JSR303是面向参数是Bean进行的校验,因此直接校验单个基础类型的参数是不生效的,但Spring 在此基础上进行了扩展,添加了MethodValidationPostProcessor拦截器,可实现对单个参数的校验(后面有介绍)

使用

1. 引入依赖

JSR依赖 hibernate-validator包,在 spring-boot-starter-web 包中已经为我们引入了该包,可以直接使用

2. 使用方式

校验Bean:

  • ①使用 @Valid + BindingResult
  • ②使用 @Valid + 全局异常捕捉处理
  • ③使用ValidationUtils手动进行校验

校验单个参数:

  • 使用@Validated + MethodValidationPostProcessor 拦截器

总的来说,本质上就两种方式:
一种是校验,然后将校验结果绑定到一个对象中
另一种是校验,校验不通过的直接抛出异常,结合全局异常进行处理,校验通过的直接进入业务处理逻辑就行

下面分别介绍这几种方式:

首先看一下常见的校验规则:

JSR提供的校验注解:         
@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提供的校验注解:  
@NotBlank(message =)   		验证字符串非null,且长度必须大于0    
@Email  					被注释的元素必须是电子邮箱地址    
@Length(min=,max=)  		被注释的字符串的大小必须在指定的范围内    
@NotEmpty   				被注释的字符串的必须非空    
@Range(min=,max=,message=)  被注释的元素必须在合适的范围内
2.1 使用 @Valid + BindingResult 的方式
①.首先创建我们的数据Bean
import lombok.Getter;
import lombok.Setter;
import org.hibernate.validator.constraints.Range;

import javax.validation.constraints.*;

@Getter
@Setter
public class TestBean {

    @NotEmpty(message = "用户名不能为空")
    private String username;

    @NotNull
    private String password;

    @Min(18)
    private Integer age;

    @Pattern(regexp = "^1(3|4|5|7|8)\\d{9}$", message = "手机号码格式错误")
    @NotBlank(message = "手机号码不能为空")
    private String phone;

    @Email
    private String email;

    @Range(min = 1, max = 3, message = "类型只能取1~3")
    private Integer type;
}

可以看到,直接在需要校验的字段上添加相应的校验规则就行,还可以添加校验出错时提示信息

②.在Controller中使用校验
// 注意,需要校验的bean对象和其绑定结果的BindingResult对象必须紧挨着成对出现
@PostMapping("/add")
public void add(@RequestBody @Valid TestBean testBean, BindingResult bindingResult) {
    System.err.println(testBean);
    if (bindingResult.hasErrors()) {
        List<FieldError> fieldErrors = bindingResult.getFieldErrors();
        for (FieldError fieldError : fieldErrors) {
            System.err.println(fieldError.getField() + ":" + fieldError.getDefaultMessage());
        }
    }
    
    // 业务代码
    // ……
}

直接在需要校验的参数前加@Valid注解即可,如果有字段校验不通过,BindingResult中就会有相应的错误信息,相应处理就行了。
在这里插入图片描述

2.2 使用 @Valid + 全局异常捕捉处理

简单的说这种方式跟上面一种是一样的,只不过去掉Controller中的BindingResult,然后添加一个全局异常,捕捉校验不通过抛出的异常

①.Controller中
@PostMapping("/add")
public void add(@RequestBody @Valid TestBean testBean) {
    System.err.println(testBean);
    
    // 业务代码
    // ……
}

Controller中在需要校验的参数前加@Valid注解即可

②.编写全局异常处理类
import com.example.demo.web.contract.BaseResponse;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Path;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @author zzs
 * @date 2019/11/25 23:35
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ConstraintViolationException.class)
    public Map<String, List<String>> defaultExceptionHandler(ConstraintViolationException ex) {
        Map<String, List<String>> validationErrors = Maps.newHashMap();

        Set<ConstraintViolation<?>> constraintViolations = ex.getConstraintViolations();
        for (ConstraintViolation<?> constraintViolation : constraintViolations) {
            for (Path.Node node : constraintViolation.getPropertyPath()) {
                String fieldName = node.getName();
                List<String> lst = validationErrors.get(fieldName);
                if (lst == null) {
                    lst = Lists.newArrayList();
                }
                lst.add(constraintViolation.getMessage());
                validationErrors.put(node.getName(), lst);
            }
        }
        return validationErrors;
    }
}

通过全局异常处理器捕捉校验出现的异常,包装成统一的样式返回给前端即可。

2.3 使用ValidationUtils手动校验

这种方式其实是第一种的优化版,即去掉Controller中的@Valid注解和BindingResult,然后通过工具类去手动校验,返回统一的格式

①.编写校验工具类
import javax.validation.*;
import java.util.*;

/**
 * 校验工具类
 */
public class ValidationUtils {
	
	/**
	 * @param t 			需要校验的类型
	 * @param skipFields	不需要校验的字段
	 */
    public static <T> Map<String, List<String>> validator(T t, HashSet<String> skipFields) {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();
        Set<ConstraintViolation<T>> constraintViolations = validator.validate(t);
        if (constraintViolations != null && constraintViolations.size() > 0) {
            Map<String, List<String>> mapErr = new HashMap<>();
            for (ConstraintViolation<T> constraintViolation : constraintViolations) {
                for (Path.Node node : constraintViolation.getPropertyPath()) {
                    String fieldName = node.getName();
                    //跳过校验的属性,校验错误不加入错误列表
                    if (skipFields == null || !skipFields.contains(fieldName)) {
                        List<String> lst = mapErr.get(fieldName);
                        if (lst == null) {
                            lst = new ArrayList<>();
                        }
                        lst.add(constraintViolation.getMessage());
                        mapErr.put(node.getName(), lst);
                    }
                }
            }
            return mapErr;
        }
        return null;
    }

    public static <T> Map<String, List<String>> validator(T t) {
        return validator(t, null);
    }
}
②.在Controller中使用
@PostMapping("/edit")
public void edit(@RequestBody TestBean testBean) {
    System.err.println(testBean);
    Map<String, List<String>> validator = ValidationUtils.validator(testBean);
    if (validator != null) {
		// 校验不通过处理……
    }
}
2.4 校验单个参数

如果参数只有很少几个字段或者就一个参数,那么定义一个Bean来封装显然有点麻烦了。然而JSR和Hibernate validator的校验只能对Object的属性进行校验,不能对单个的参数进行校验,但Spring 在此基础上进行了扩展,提供了@Validated注解,再配合MethodValidationPostProcessor拦截器,可以实现对方法参数的校验。

(@Validated是Spring Validation验证框架对参数的验证机制提供的注解,可以看作是标准JSR-303的一个变种)

使用方式如下:
首先要添加此配置:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;

/**
 * @author zzs
 * @date 2019/12/8 21:39
 */
@Configuration
public class ValidateConfig {

    /**
     * JSR和Hibernate validator的校验只能对Object的属性进行校验,不能对单个的参数进行校验,
     * 但Spring 在此基础上进行了扩展,添加了MethodValidationPostProcessor拦截器,可以实现对方法参数的校验。
     *
     * 使用时需要在类上面添加@Validated,参数上直接添加校验规则就行了
     * 类上加了@Validated注解后,校验不通过的直接返回异常或者走全局异常处理了,BindingResult就不生效了
     *
     * @return
     */
    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        return new MethodValidationPostProcessor();
    }
}

然后直接在Controller类上加@Validated注解,并且在需要校验的参数前加相应的规则,最后在配合上边的全局异常处理即可

import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.constraints.Pattern;

/**
 * @author zzs
 * @date 2019/12/8 21:39
 */
@Validated
@RestController
public class ValidController {

    @PostMapping("/add")
    public void add(@RequestBody @Pattern(regexp = "^1(3|4|5|7|8)\\d{9}$", message = "手机号码格式错误") String telephone) {

    }
}

这里需要注意一下,类上加了@Validated注解后,校验不通过的直接返回异常或者走全局异常处理了,BindingResult就不生效了

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值