springboot2 - validation

业务需求:客户端提交的表单,后台需要有统一的校验拦截机制。

Maven 依赖

除了 hibernate-validator,springboot 本身自带这些依赖。


<dependencys>
    <dependency>
        <groupId>jakarta.validation</groupId>
        <artifactId>jakarta.validation-api</artifactId>
        <version>2.0.2</version>
    </dependency>
    <dependency>
        <groupId>javax.validation</groupId>
        <artifactId>validation-api</artifactId>
        <version>2.0.1.Final</version>
    </dependency>
    <dependency>
        <groupId>org.hibernate.validator</groupId>
        <artifactId>hibernate-validator</artifactId>
        <version>6.2.5.Final</version>
        <scope>compile</scope>
    </dependency>
</dependencys>

springboot-2.3 以上,需要手动引入依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

@Validated 和 @Valid 区别

  • javax 提供了@Valid(标准JSR-303规范),而 @Validated 是由 spring 提供的变种,多了个分组的功能。

  • @Validated 的可用位置:类、方法、参数(ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER)

  • @Valid 可用位置更多,多了这些:构造函数、java8新特性支持(ElementType.CONSTRUCTOR, ElementType.TYPE_USE)。

常用方式

我们一般使用下面这种方式进行数据校验。


@Controller
@RequestMapping("debug")
public class DebugCtrl {

    // 参数是一个对象,需要在参数上加 @Validated 或者 @Valid
    @ResponseBody
    @RequestMapping("valid")
    public Result valid(@Validated AppInfo info) {
        System.out.println(info);
        return Result.failed();
    }
}

验证单个字段,需要在类上增加 @Validated,这种情况使用 @Valid 是无效的。

用起来挺别扭的,只是验证是否为空,不如 @RequestParam,我一般选择自己编码增强这一逻辑。


// 需要在整个类上增加 @Validated
@Validated
@Controller
@RequestMapping("debug")
public class DebugCtrl {

    // 验证一个字段
    @ResponseBody
    @RequestMapping("valid")
    public Result valid(@NotEmpty String a) {
        return Result.failed();
    }
}

分组的使用

在业务的不同阶段,对象验证规则不一样,这时候,就需要用到分组功能。

场景:对于自增 ID,新增的时候,ID 不能有值,修改的时候,ID 不允许为空。


public class AppInfo implements Serializable {

    // version 字段最大长度是 10,指定新增、修改时有效
    @Size(groups = {Insert.class, Update.class}, max = 10)
    private String version;

    // 省略其它代码 …………
}

class Test {

    // 使用工具包校验,验证 groups 中带有 Insert.class 的字段
    public static void main(String[] args) throws BindException {
        AppInfo appInfo = new AppInfo();
        appInfo.setVersion("1adasdadadadasd32123");
        ValidationUtils.validate(appInfo, Insert.class);
    }

    // 使用 @Validated 校验,验证 groups 中带有 Insert.class 的字段
    @ResponseBody
    @RequestMapping("valid")
    public Result valid(@Validated(Insert.class) AppInfo info) {
        System.out.println(info);
        return Result.failed();
    }
}

个人并不推荐使用 @Validated 的分组功能:

  1. 容易被滥用:如果一个对象有很多套校验规则,使用分组的方式实现,代码就会变得十分混乱;
  2. 容易产生歧义:有的字段,指定了分组,有的不指定,对于不熟悉的人,无法预知程序的结果。

个人比较推荐:通用部分,在对象中注明,特殊的字段,在 Controller 层进行单独强调。

public class AppInfo implements Serializable {

    // @NotEmpty 没有分组配置,本意可能是希望任何场景都生效,
    // 但实际与之相反,@Validated 指定了分组,则会跳过 @NotEmpty 的校验。 
    @NotEmpty
    @Size(groups = {Insert.class, Update.class}, max = 10)
    private String version;
}

工具包

有些情况下需要手动进行校验,比如:Excel 导入过程中,需要批量校验数据,这种情况,手动校验更有利于组织代码。

可以参考下面这段代码,校验失败会抛出 BindException 异常,这与 spring 默认行为是一致的,你只要写一个统一的异常处理切面即可。


import cn.seaboot.commons.core.CommonUtils;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.Set;

/**
 * Java 对象校验工具
 *
 * @author Mr.css
 * @version 2023-09-21 11:12
 */
public class ValidationUtils {
    private static final Validator validator;

    static {
        validator = Validation.buildDefaultValidatorFactory().getValidator();
    }

    /**
     * javax.validation 注解校验,分组校验
     * <p>
     * 如果校验失败,则抛出异常,spring 环境下可以写一个统一的异常捕获切面
     *
     * @param obj    校验实体
     * @param groups 校验分组
     * @throws ConstraintViolationException 抛出验证失败异常
     */
    public static void validate(Object obj, Class<?>... groups) {
        Set<ConstraintViolation<Object>> set = validator.validate(obj, groups);
        if (CommonUtils.isNotEmpty(set)) {
            throw new ConstraintViolationException(set);
        }
    }
}

需要处理的异常

出现校验不通过,一般会抛出下面两种异常:

  • org.springframework.validation.BindException:spring 环境下会抛出的异常
  • javax.validation.ConstraintViolationException:javax 原生验证会触发的异常

对于异常的处理,下面提供代码样例,根据自身项目需求进行调整。

class Test {
    /**
     * Handler BindException (from: hibernate-validator)
     */
    public ModelAndView handlerBindException(HttpServletRequest request, HttpServletResponse response, BindException ex) {
        BindingResult result = ex.getBindingResult();
        if (result.hasErrors()) {
            List<FieldError> errors = result.getFieldErrors();
            Map<String, Object> object = new HashMap<>(errors.size());
            for (FieldError error : errors) {
                object.put(error.getField(), error.getDefaultMessage());
            }
            return new Result(HttpStatus.INTERNAL_SERVER_ERROR.value(), object).toView();
        } else {
            return Result.INTERNAL_SERVER_ERROR.toView();
        }
    }

    /**
     * Handler ConstraintViolationException (from: javax.validation)
     */
    public ModelAndView handlerConstraintViolationException(HttpServletRequest request, HttpServletResponse response, ConstraintViolationException ex) throws IOException {
        Set<ConstraintViolation<?>> set = ex.getConstraintViolations();
        Map<String, Object> object = new HashMap<>(set.size());
        for (ConstraintViolation<?> violation : set) {
            object.put(violation.getPropertyPath().toString(), violation.getMessage());
        }
        return new Result(HttpStatus.INTERNAL_SERVER_ERROR.value(), object).toView();
    }
}

相关源码

  • org.springframework.validation.beanvalidation.MethodValidationPostProcessor
  • org.springframework.validation.beanvalidation.MethodValidationInterceptor

posted on 2024-05-21 09:48  疯狂的妞妞  阅读(46)  评论(0编辑  收藏  举报

导航