SpringBoot 使用 Validation 校验

概述
在 Web 应用中,客户端提交数据之前都会进行数据的校验,比如用户注册时填写的邮箱地址是否符合规范、用户名长度的限制等等,不过这并不意味着服务端的代码可以免去数据验证的工作,用户也可能使用 HTTP 工具直接发送违法数据。为了保证数据的安全性,服务端的数据校验是必须的。

先理清概念:

JSR-303 是 JavaEE 6 中的一项子规范,又称作 Bean Validation,提供了针对 Java Bean 字段的一些校验注解,如@NotNull,@Min等。JSR-349 是其升级版本,添加了一些新特性。
Hibernate Validator 是对这个规范的实现(与 ORM 框架无关),并在它的基础上增加了一些新的校验注解。
Spring 本身也有一个校验接口Validator,位于 org.springframework.validation 包下,但是使用这个接口需要进行硬编码,也就是手动校验,没有提供注解进行简化。为了给开发者提供便捷,Spring 也全面支持 JSR-303、JSR-349 规范,对 Hibernate Validation 进行二次封装,在 SpringMVC 模块中添加了自动校验机制,可以利用注解对 Java Bean 的字段的值进行校验,并将校验信息封装进特定的类中。
下面将介绍如何在 Spring 应用中使用 JSR-303 校验规范。

校验注解

JSR-303 包含的注解

注解名称 说明
@Null 被注解元素必须为 null
@NonNull 被注解元素必须不为 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 且trim()后长度大于 0
@Email 被注解元素必须是电子邮箱地址
@Length(min=,max=) 被注解的字符串的长度必须在指定范围内
@NotEmpty 被注解元素(字符串、数组、集合等)必须非 null 且长度大于 0
@Range(min=,max=,message=) 被注解元素必须在合适的范围内
@URL 被注解元素必须是合法的 URL

添加依赖

    
        org.springframework.boot
        spring-boot-starter-parent
        2.2.2.RELEASE
    
    

        
        
            org.springframework.boot
            spring-boot-starter-web
        

        
        
            org.springframework.boot
            spring-boot-starter-validation
        

        
            org.projectlombok
            lombok
        
    

创建一个返回统一个是的类

@Data
@NoArgsConstructor
public class R {

    private int code = 200;
    private String msg = "成功";
    private Object data;

    public R(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }
    public static R success(){
        return new R();
    }
    public static R error(){
        return error(500, "出错拉");
    }
    public static R error(int code,String msg){
        return new R(code,msg);
    }
    public static R success(Object data){
        R r = new R();
        r.setData(data);
        return r;
    }
    public static R error(Object obj){
        R r = error();
        r.setData(obj);
        return r;
    }
}

编写一个 javaBean 使用 Validation 注解

@Data
public class UserInfo {

    private Integer id;
   
    @NotBlank(message = "用户名不能为空")
    private String name;
  
    @NotNull(message = "年龄不能为空")
    @Range(min = 1,max = 100,message = "年龄必须1-100岁之间")
    private Integer age;

    @NotBlank(message = "邮件不能为空")
    @Email(message = "邮件格式不正确")
    private String email;
}

编写一个 controller 测试

  • @Valid 和 BindingResult 是成对出现的
    /**
     * 临时方式输出力  每个方法都需要加 BindingResult
     * @param userInfo
     * @param result
     * @return
     */
    @PostMapping("/validationTest")
    public R validationTest(@RequestBody @Valid UserInfo userInfo, BindingResult result){

        if(result.hasErrors()){
            List allErrors = result.getAllErrors();
            String errors = "";
            for (int i = 0; i < allErrors.size(); i++) {
                if(i == allErrors.size()-1){
                    errors += allErrors.get(i).getDefaultMessage();
                }else {
                    errors += allErrors.get(i).getDefaultMessage()+"---";
                }
            }
            return R.error(errors);
        }
        return R.success("用户合法");
    }

当我们参数全部正确填写时请求没有问题

SpringBoot 使用 Validation 校验_第1张图片
image.png

一旦填写的参数不合法时就会返回校验的异常

SpringBoot 使用 Validation 校验_第2张图片
image.png

上面这种方案适合临时方案在特殊的时候使用,因为每一个方法都需要加上一个 @Valid 注解 和 BindingResult ,类多的话想想都够烦的,下面我们使用 spring 统一异常处理机制,来编写一个全局异常处理

编写统一异常处理类

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * valid 异常处理
     * @param e
     * @return
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public R handlerValidException(MethodArgumentNotValidException e){
        // 获取单个错误
       // String errorMessage = e.getBindingResult().getFieldError().getDefaultMessage();
        // 获取所有错误信息
        List allErrors = e.getBindingResult().getAllErrors();
        String errors = "";
        for (int i = 0; i < allErrors.size(); i++) {
            if(i == allErrors.size()-1){
                errors += allErrors.get(i).getDefaultMessage();
            }else {
                errors += allErrors.get(i).getDefaultMessage()+" | ";
            }
        }
        log.info("data errors = {}",errors);
        return R.error(errors);
    }
}

在编写一 controller 方法

    /**
     * 使用全局异常处理
     * @param userInfo
     * @return
     */
    @PostMapping("/validErrorsTest")
    public R validErrorsTest(@RequestBody @Valid UserInfo userInfo){
        return R.success(userInfo);
    }

测试一下,效果是一样的,但是代码简洁了许多

SpringBoot 使用 Validation 校验_第3张图片
image.png

自定义校验

SpringBoot 使用 Validation 校验_第4张图片
image.png
  • 在使用 @Email 注解校验邮箱的时候发现没有点也是合法的,但是我们需要必须要有点怎么实现呢?我们可以自定义一个注解校验

编写一个邮箱校验注解

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 指定真正的校验类
@Constraint(validatedBy = RequiredEmailFormatValidator.class)
public @interface RequiredEmailFormat {
    String message() default "邮件格式不正确";
    //分组
    Class[] groups() default {};
    //负载
    Class[] payload() default {};
    //指定多个时使用,从而支持重复注解
    @Target({ElementType.FIELD,ElementType.METHOD, ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @interface List {
        RequiredEmailFormat[] value();
    }
}
  • @Constraint 注解指定真正的校验类

编写校验类,需要实现 ConstraintValidator 接口

/**
 * 校验邮箱是否合法
 */
public class RequiredEmailFormatValidator implements ConstraintValidator {
    /**
     * 初始化事件方法
     * @param constraintAnnotation
     */
    @Override
    public void initialize(RequiredEmailFormat constraintAnnotation) {
    }
    /**
     * 判断是否合法
     * @param s
     * @param constraintValidatorContext
     * @return
     */
    @Override
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        if(StringUtils.isEmpty(s)){
            return false;
        }
        // 邮件正则
        String checkReg = "^([a-z0-9A-Z]+[-|_|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$";
        Pattern regex = Pattern.compile(checkReg);
        // 不匹配
        if(!regex.matcher(s).matches()){

            // 禁用默认提示信息
            //constraintValidatorContext.disableDefaultConstraintViolation();
            // 设置提示语
            //constraintValidatorContext.buildConstraintViolationWithTemplate("email error").addConstraintViolation();
            return false;
        }
        return true;
    }
}

第一个泛型参数是表明校验的注解类型,第二个泛型参数是需要被校验的类型。

  • initialize:初始化事件方法
  • isValid:判断是否合法的方法
    ConstraintValidatorContext这个上下文包含了校验中所有的信息,我们可以利用这个对象进行获取默认错误提示信息,禁用错误提示信息,改写错误提示信息等操作

我们在原来的邮件字段上加上我们自定义的注解

    @NotBlank(message = "邮件不能为空")
    //@Email(message = "邮件格式不正确")
    @RequiredEmailFormat(message = "邮件格式输入不正确")
    private String email;

测试

SpringBoot 使用 Validation 校验_第5张图片
image.png
  • 这时邮箱校验就会使用我们自定义的校验,如果输入的邮箱中没有点的话校验也是不通过的

有时候我们后端需要的是一个数字,前台确传递了一个字符串,这时 jackson 会抛出类型转换异常,如果把这些信息返回个前端不是很友好,自己看着也不舒服,最好能够具体一点,比如那个字段,需要什么类型,输入的值什么等......

SpringBoot 使用 Validation 校验_第6张图片
image.png

在统一异常处理类中添加一个统一的异常处理方法来通一处理

    /**
     * 处理统一的类型转换异常
     * @param e
     * @return
     */
    @ExceptionHandler(HttpMessageNotReadableException.class)
    public R httpMessageNotReadableException(HttpMessageNotReadableException e){
        if(e.getCause() instanceof InvalidFormatException){
            InvalidFormatException invalidFormatException = (InvalidFormatException)e.getCause();
            String errors = "";
            List path = invalidFormatException.getPath();
            for(JsonMappingException.Reference reference : path){
                errors += "参数名:"+reference.getFieldName()+
                        " 输入不合法,需要的是 "+invalidFormatException.getTargetType().getName() +
                        " 类型,"+"提交的值是:"+invalidFormatException.getValue().toString();
                log.info("参数名:{}",reference.getFieldName());
            }
            log.info("提交的参数值:{}",invalidFormatException.getValue().toString());
            log.info("需要的参数类型:{}",invalidFormatException.getTargetType().getName());
            return R.error(errors);
        }
        return R.error();
    }

这个时候我们再来测试

SpringBoot 使用 Validation 校验_第7张图片
image.png
  • 我们年龄依然写的是字符串,但是异常信息已经和明了了

group 分组校验

  • 在我们添加数据和修改数据的时候,添加的时候是不能指定 id 的,但是修改的时候又必须有 id ,这是就可以使用分组校验,每一个校验注解都有一个 groups 属性,接收的是一个接口类型数组,用来表示分组
    我们创建 addGroup 接口表示添加分组,updateGroup 接口表示更新分组,接口可以是空接口


    image.png

    然后在校验注解上引用

@Null(message = "新增不能指定id",groups = {AddGroup.class})
@NotBlank(message = "id不能为空",groups = {UpdateGroup.class})

这是 Controller 中就不能使用 @Valid 注解了,需要使用 @Validated 注解来指定校验分组
比如我们这里是一个新增的方法 id 必须是空的,这是我们就使用添加分组,在添加数据的时候校验 id 必须为空,注意,默认没有分组的校验注解,在分组校验 @Validated(value = {AddGroup.class} 下不会生效

   @RequestMapping("/save")
    public R save(@RequestBody @Validated(value = {AddGroup.class}) BrandEntity brand){
        brandService.save(brand);

        return R.ok();
    }
  • 测试添加


    SpringBoot 使用 Validation 校验_第8张图片
    image.png

Validator 国际化配置

yml 配置
spring:
  messages:
    basename: i18n/validations
    encoding: UTF-8
resources 目录下新建 i18n 文件夹,并创建两个文件,一个是 valications.properties 默认读取的中文国际化文件,另一个是 valications_en.properties 英文国际化文件
SpringBoot 使用 Validation 校验_第9张图片
image.png
  • validations.properties 内容
    userNameNotEmpty=用户名不能为空
  • validations_en.properties 内容
    userNameNotEmpty=username not empty !
i18n 配置类
@Configuration
public class I18nConfig {

    @Autowired
    private MessageSource messageSource;

    @Bean
    public Validator getValidator(){
        LocalValidatorFactoryBean validatorFactoryBean = new LocalValidatorFactoryBean();
        validatorFactoryBean.setValidationMessageSource(this.messageSource);
        return validatorFactoryBean;
    }
}
在实例中引用
    @NotBlank(message = "{userNameNotEmpty}")
    private String name;

测试

  • 默认走的是 validations.properties


    SpringBoot 使用 Validation 校验_第10张图片
    image.png

在请求头 headers 中加入 Accept-Language=en 就会切换到英文

SpringBoot 使用 Validation 校验_第11张图片
image.png

参考 https://www.cnblogs.com/zzzt20/p/12482979.html

你可能感兴趣的:(SpringBoot 使用 Validation 校验)