全局异常统一处理之约束违反异常:ConstraintViolationException

概述

在构建基于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个条件:

  1. Controller上添加 @Validated 注解;
  2. 需要校验的接口入参, 直接 映射到控制器方法参数;
  3. 参数前添加了校验规则注解(比如 @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("[email protected]");
        return vo;
    }

}


测试结果

不进行异常处理的接口响应

  • 接口相应

在这里插入图片描述

  • 控制台报错

在这里插入图片描述

异常统一处理后的接口响应

在这里插入图片描述

参考文章

请参考博文:路径参数@PathVariable,格式校验:@Validated(Controller上)+ @Pattern + ConstraintViolationException(异常处理)