springboot

SpringBoot 使用 Validation 校验

2021-01-08  本文已影响0人  吉他手_c156

概述
在 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

添加依赖

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.2.RELEASE</version>
    </parent>
    <dependencies>

        <!--  web 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--  springboot 整合 hibernate validator -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

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

@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 测试

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

        if(result.hasErrors()){
            List<ObjectError> 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("用户合法");
    }

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

image.png

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

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<ObjectError> 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);
    }

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

image.png

自定义校验

image.png

编写一个邮箱校验注解

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

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

/**
 * 校验邮箱是否合法
 */
public class RequiredEmailFormatValidator implements ConstraintValidator<RequiredEmailFormat,String> {
    /**
     * 初始化事件方法
     * @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;
    }
}

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

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

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

测试

image.png

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

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<JsonMappingException.Reference> 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();
    }

这个时候我们再来测试

image.png

Validator 国际化配置

yml 配置
spring:
  messages:
    basename: i18n/validations
    encoding: UTF-8
resources 目录下新建 i18n 文件夹,并创建两个文件,一个是 valications.properties 默认读取的中文国际化文件,另一个是 valications_en.properties 英文国际化文件
image.png
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;

测试

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

image.png

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

上一篇 下一篇

猜你喜欢

热点阅读