概述
在构建基于Spring Boot的高效RESTful API时,参数的有效性和完整性是保障系统健壮性的重要一环。Java Bean Validation规范为此提供了一套强大的验证机制,通过一系列注解如@NotNull、@Size和@Pattern等,开发者可以方便地对请求参数进行细致严格的校验。
ConstraintViolationException
是Java Bean Validation框架在参数校验过程中发现约束条件被违反时抛出的运行时异常;本文将聚焦于ConstraintViolationException
这一特定异常,深入解析ConstraintViolationException
异常的原因和处理方式。
- 关于 全局异常统一处理 的原理和完整实现逻辑,请参考文章:《SpringBoot 全局异常统一处理(AOP):@RestControllerAdvice + @ExceptionHandler + @ResponseStatus》
- 本文仅详细解析 ConstraintViolationException 的异常处理;其他类型异常的原理和处理方法,请参阅本文所在专栏内的其他文章。
异常基础与应用场景
当我们在Controller层接收HTTP请求,并将带有Bean Validation注解的对象作为方法参数时,如果参数对象使用了来自 javax.validation 包下的注解(如@NotNull、@Size、@Pattern等)进行数据校验,在执行业务逻辑或持久化操作之前,Hibernate Validator(作为Java Bean Validation规范的实现库)会自动进行验证;如果对象的某个属性值不满足这些注解所定义的约束条件,会抛出javax.validation.ConstraintViolationException
异常。
捕获并处理ConstraintViolationException可以帮助开发者向客户端返回具体的错误信息,指出是哪些字段不符合验证规则,这对于API开发尤为重要。在Spring Boot应用中,可以创建一个全局异常处理器方法,用@ExceptionHandler注解来捕获并统一处理此类异常。
ConstraintViolationException 校验生效条件
在测试时发现,并非所有参数校验异常都会抛出 ConstraintViolationException;很多的情况下,会抛出 BindException。那么,什么情况下校验失败,会抛出 ConstraintViolationException 呢?
需要满足如下3个条件:
- Controller上添加 @Validated 注解;
- 需要校验的接口入参, 直接 映射到控制器方法参数;
- 参数前添加了校验规则注解(比如 @Pattern)。
这里说的 直接映射到控制器方法参数
,是指这个参数,就是后端实际接收的一个字段;它是相对于另一种情况来说的,也就是控制器方法的参数是一个复杂对象,对象内的字段才是实际接收的参数。
异常处理代码
核心代码
在Spring Boot应用中,我们可以利用@ExceptionHandler
注解来捕获并处理ConstraintViolationException
异常,如下所示的代码片段提供了一种通用的异常处理策略:
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 参数校验异常:直接参数校验。
* <p>
* 此校验适用的接口,需要满足如下条件:
* 1. 需要校验的参数“直接”作为接口方法的参数;
* 2. Controller上添加 @Validated 注解;
* 3. 参数前添加了校验规则注解(比如 @Pattern)。
* <p>
* 示例:删除用户接口
*/
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<String> handle(ConstraintViolationException e, HandlerMethod handlerMethod) {
logInfo(e, handlerMethod);
String userMessage = UserTipGenerator.getUserMessage(e);
String format = "ConstraintViolationException(约束违反异常)(错误数量:%s):%s";
String errorMessage = String.format(format, e.getConstraintViolations().size(), e.getMessage());
return Result.fail(userMessage, String.valueOf(HttpStatus.BAD_REQUEST.value()), errorMessage);
}
// 其他异常处理和打印异常信息...
}
相关代码:UserTipGenerator
public static String getUserMessage(ConstraintViolationException e) {
Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
if (CollectionUtils.isEmpty(violations)) {
return "";
}
return violations.stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining(";"));
}
测试案例
示意图
测试代码
package com.example.web.user.controller;
import com.example.web.model.vo.UserVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.constraints.Pattern;
@Validated
@RestController
@RequestMapping("users")
@Tag(name = "用户管理")
public class UserController {
@GetMapping("{id}")
@Operation(summary = "查询用户")
@Parameter(name = "id", description = "用户ID", example = "1234567890123456789")
public UserVO getUser(@PathVariable @Pattern(regexp = "^\\d{19}$", message = "用户ID,应为19位数字") String id) {
// 示例代码;实际业务逻辑中应该从数据库中获取数据
UserVO vo = new UserVO();
vo.setId(id);
vo.setName("张三");
vo.setMobilePhone("18612345678");
vo.setEmail("zhangsan@example.com");
return vo;
}
}
测试结果
不进行异常处理的接口响应
- 接口相应
- 控制台报错
异常统一处理后的接口响应
参考文章
请参考博文:路径参数@PathVariable,格式校验:@Validated(Controller上)+ @Pattern + ConstraintViolationException(异常处理)