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 的分组功能:
- 容易被滥用:如果一个对象有很多套校验规则,使用分组的方式实现,代码就会变得十分混乱;
- 容易产生歧义:有的字段,指定了分组,有的不指定,对于不熟悉的人,无法预知程序的结果。
个人比较推荐:通用部分,在对象中注明,特殊的字段,在 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