springboot接口请求参数校验

发布于:2025-08-14 ⋅ 阅读:(17) ⋅ 点赞:(0)

参数校验

参数校验可以防止无效或错误的数据进入系统。通过校验前端输入的参数,可以确保数据的完整性,避免因为缺少必要的信息而导致程序错误或异常。例如,对于密码字段,可以通过校验规则要求用户输入至少8个字符、包含字母和数字等,以增加密码的强度,提高系统的安全性。通过及时地反馈错误信息,用户可以更快地发现和纠正输入错误,提升用户体验。特别是在前后端接口联调时,前端传参错误很快能得到异常提示,就大大提升了联调效率。

传统的校验方法

@RestController
@RequestMapping("/api/users")
public class UserController {

    @PostMapping("/register")
    public ResponseEntity<String> register(@RequestBody Map<String, Object> request) {
        String username = (String) request.get("username");
        if (username == null || username.length() < 3 || username.length() > 20) {
            return ResponseEntity.badRequest().body("用户名不能为空,且长度必须在3到20之间");
        }

        String password = (String) request.get("password");
        if (password == null || password.length() < 8) {
            return ResponseEntity.badRequest().body("密码不能为空,且长度至少为8个字符");
        }

        Integer age = (Integer) request.get("age");
        if (age == null || age <= 0 || age > 120) {
            return ResponseEntity.badRequest().body("年龄必须是正整数,且不能超过120");
        }

        return ResponseEntity.ok("注册成功!");
    }
}

这种手写参数校验的方式,在简单场景下勉强能用,但如果业务变复杂,问题会越来越多。在 Spring Boot 中,可以使用Hibernate Validator来实现参数校验。它的核心思路是:把校验逻辑从业务代码里抽离出来,用注解的方式声明校验规则

Springboot 校验原理

Java API规范(JSR303)定义了Bean校验的标准validation-api,但没有提供实现。hibernate validation是对这个规范的实现,并增加了校验注解如@Email、@Length等。Spring Validation是对hibernate validation的二次封装,用于支持SpringMVC参数自动校验,如果SpringBoot版本小于2.3.x,spring-boot-starter-web会自动引入hibernate-validator依赖。如果SpringBoot版本大于2.3.x,则需要手动引入依赖:

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

校验注解

校验空值

  • @Null:验证对象是否为 null
  • @NotNull:验证对象是否不为 null,但可以为空比如空字符串或者空集合
  • @NotEmpty:验证对象不为 null,可以为空字符串,但是长度(数组、集合、字符串等)大于 0
  • @NotBlank:验证字符串不为 null,且去除两端空白字符后长度大于 0

校验大小

  • @Size(min=, max=):验证对象(数组、集合、字符串等)长度是否在给定的范围之内
  • @Min(value):验证数值(整数或浮点数)是否大于等于指定的最小值
  • @Max(value):验证数值是否小于等于指定的最大值
  • @DecimalMin(value):被注释的元素必须是一个数字,其值必须大于等于指定的最小值
  • @DecimalMax(value):被注释的元素必须是一个数字,其值必须小于等于指定的最大值
  • @Digits (integer, fraction):被注释的元素必须是一个数字,其值必须在可接受的范围内

校验布尔值

  • @AssertTrue:验证 Boolean 对象是否为 true
  • @AssertFalse:验证 Boolean 对象是否为 false

校验日期和时间

  • @Past:验证 Date 和 Calendar 对象是否在当前时间之前
  • @Future:验证 Date 和 Calendar 对象是否在当前时间之后
  • @PastOrPresent:验证日期是否是过去或现在的时间
  • @FutureOrPresent:验证日期是否是现在或将来的时间

正则表达式

  • @Pattern(regexp=, flags=):验证 String 对象是否符合正则表达式的规则

其他

  • @Length(min=, max=):验证字符串的大小是否在指定的范围内
  • @Range(min=, max=):验证数值是否在合适的范围内
  • @UniqueElements:校验集合中的值是否唯一,依赖于 equals 方法
  • @ScriptAssert:利用脚本进行校
  • @Email:被注释的元素必须是电子邮箱地址
  • @SafeHtml:被注释的元素必须 Html
  • URL: 被注释的元素必须是有效的 URL
  • @Negative:负数不包括0
  • @NegativeOrZero:负数或0
  • **@**CreditCardNumber:字符串是有效的信用卡数字,不校验信用卡本身的有效性
  • @Valid 和 @Validated

这两个注解是校验的入口,作用相似但用法上存在差异。

// 用于类/接口/枚举,方法以及参数
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})  
@Retention(RetentionPolicy.RUNTIME)
@Documented  
public @interface Validated {  
    // 校验时启动的分组  
    Class<?>[] value() default {};  
}

// 用于方法,字段,构造函数,参数,以及泛型类型  
@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE })  
@Retention(RUNTIME)  
@Documented
public @interface Valid {  
    // 未提供其他属性  
}
  1. 作用范围不同:@Validated 无法作用在于字段, @Valid 无法作用于类
  2. 注解中的属性不同:@Validated 中提供了指定校验分组的属性,而 @Valid 没有这个功能,因为 @Valid 不能进行分组校验
  3. @Valid 注解支持嵌套校验,@Validated 不支持嵌套校验
  4. @Validated(Spring’s JSR-303 规范,是标准 JSR-303 的一个变种),javax提供了@Valid(标准JSR-303规范),配合 BindingResult 可以直接提供参数验证结果。

校验

@RequestBody 参数校验

当方法入参为 @RequestBody 注解的 JavaBean,可在入参前使用 @Validated 或 @Valid 注解开启校验

@PostMapping("/addStudent")
public void addStudent(@RequestBody  @Valid  Student student) {
  //所有验证通过才会执行下面的代码
  System.out.println("添加学生成功");
}

@ResponseBody标注方法校验原理

在SpringMVC中,RequestResponseBodyMethodProcessor是用于解析@RequestBody标注的参数以及处理@ResponseBody标注方法的返回值的。显然,执行参数校验的逻辑肯定就在解析参数的方法resolveArgument()中:

public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
    @Override
    public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

        parameter = parameter.nestedIfOptional();
        //将请求数据封装到DTO对象中
        Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
        String name = Conventions.getVariableNameForParameter(parameter);

        if (binderFactory != null) {
            WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
            if (arg != null) {
                // 执行数据校验
                validateIfApplicable(binder, parameter);
                if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
                    throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
                }
            }
            if (mavContainer != null) {
                mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
            }
        }
        return adaptArgumentIfNecessary(arg, parameter);
    }
}

可以看到,resolveArgument()调用了validateIfApplicable()进行参数校验。

protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
    // 获取参数注解,比如@RequestBody、@Valid、@Validated
    Annotation[] annotations = parameter.getParameterAnnotations();
    for (Annotation ann : annotations) {
        // 先尝试获取@Validated注解
        Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
        //如果直接标注了@Validated,那么直接开启校验。
        //如果没有,那么判断参数前是否有Valid起头的注解。
        if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
            Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
            Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
            //执行校验
            binder.validate(validationHints);
            break;
        }
    }
}

看到这里,大家应该能明白为什么这种场景下@Validated、@Valid两个注解可以混用。接下来继续看WebDataBinder.validate()实现。

@Override
public void validate(Object target, Errors errors, Object... validationHints) {
    if (this.targetValidator != null) {
        processConstraintViolations(
            //此处调用Hibernate Validator执行真正的校验
            this.targetValidator.validate(target, asValidationGroups(validationHints)), errors);
    }
}

最终发现底层最终还是调用了Hibernate Validator进行真正的校验处理。

注意:如果是通过post ResponseBody会抛出MethodArgumentNotValidException异常,如果是get 请求,普通的student参数,则会抛出BindException 异常

简单参数校验

当方法入参为 @PathVariable、 @RequestParam 注解的简单参数时,需要在 Controller 加上 @Validated 注解开启校验。

@RequestMapping("/student")
@RestController
// 必须加上该注解
@Validated
public class StudentController {
    // 路径变量
    @GetMapping("{id}")
    public Reponse<Student> detail(@PathVariable("id") @Min(1L) Long StudentId) {
        // 参数StudentId校验通过,执行后续业务逻辑
        return Reponse.ok();
    }

    // 请求参数
    @GetMapping("getByName")
    public Result getByName(@RequestParam("Name") @Length(min = 1, max = 20) String  Name) {
        // 参数Name校验通过,执行后续业务逻辑
        return Result.ok();
    }
}

简单参数校验原理

上面提到的将参数一个个平铺到方法参数中,然后在每个参数前面声明约束注解的校验方式,就是方法级别的参数校验。实际上,这种方式可用于任何Spring Bean的方法上,比如Controller/Service等。其底层实现原理就是AOP,具体来说是通过MethodValidationPostProcessor动态注册AOP切面,然后使用MethodValidationInterceptor对切点方法织入增强。

public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor implements InitializingBean {
    @Override
    public void afterPropertiesSet() {
        //为所有`@Validated`标注的Bean创建切面
        Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
        //创建Advisor进行增强
        this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
    }

    //创建Advice,本质就是一个方法拦截器
    protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
        return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
    }
}

接着看一下MethodValidationInterceptor

public class MethodValidationInterceptor implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        //无需增强的方法,直接跳过
        if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
            return invocation.proceed();
        }
        //获取分组信息
        Class<?>[] groups = determineValidationGroups(invocation);
        ExecutableValidator execVal = this.validator.forExecutables();
        Method methodToValidate = invocation.getMethod();
        Set<ConstraintViolation<Object>> result;
        try {
            //方法入参校验,最终还是委托给Hibernate Validator来校验
            result = execVal.validateParameters(
                invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
        }
        catch (IllegalArgumentException ex) {
            
        }
        //有异常直接抛出
        if (!result.isEmpty()) {
            throw new ConstraintViolationException(result);
        }
        //真正的方法调用
        Object returnValue = invocation.proceed();
        //对返回值做校验,最终还是委托给Hibernate Validator来校验
        result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
        //有异常直接抛出
        if (!result.isEmpty()) {
            throw new ConstraintViolationException(result);
        }
        return returnValue;
    }
}

实际上,不管是requestBody参数校验还是方法级别的校验,最终都是调用Hibernate Validator执行校验,Spring Validation只是做了一层封装。

复杂参数校验

有时候,需要对多个字段进行复杂的逻辑校验,例如需要两个字段相互比较或执行自定义的校验逻辑。在这种情况下,可以使用自定义的校验器(Validator)来实现。

public class UserDto{
    @NotNull(message = "起始日期不能为空")
    private LocalDate startDate;
    
    @NotNull(message = "结束日期不能为空")
    private LocalDate endDate;
    
    @AssertTrue(message = "结束日期必须晚于起始日期")
    private boolean isEndDateAfterStartDate(){
        if (startDate == null || endDate == null) {
            returntrue;
        }
        return endDate.isAfter(startDate);
    }
}

在上述示例中,使用了 @AssertTrue注解来标记自定义的校验方法 isEndDateAfterStartDate()。该方法检查 endDate是否晚于 startDate,如果校验失败,将返回指定的错误提示信息。

分组校验

在实际项目中,可能多个方法需要使用同一个DTO类来接收参数,而不同方法的校验规则很可能是不一样的。这个时候,简单地在DTO类的字段上加约束注解无法解决这个问题。因此,spring-validation支持了分组校验的功能,专门用来解决这类问题。还是上面的例子,比如保存User的时候,UserId是可空的,但是更新User的时候,UserId的值必须>=10000000000000000L;其它字段的校验规则在两种情况下一样。这个时候使用分组校验的代码示例如下:

约束注解上声明适用的分组信息groups

@Data
public class UserDTO {

    @Min(value = 10000000000000000L, groups = Update.class)
    private Long userId;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 2, max = 10, groups = {Save.class, Update.class})
    private String userName;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})
    private String account;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})
    private String password;

    /**
     * 保存的时候校验分组
     */
    public interface Save {
    }

    /**
     * 更新的时候校验分组
     */
    public interface Update {
    }
}

@Validated注解上指定校验分组

@PostMapping("/save")
public Result saveUser(@RequestBody @Validated(UserDTO.Save.class) UserDTO userDTO) {
    // 校验通过,才会执行业务逻辑处理
    return Result.ok();
}

@PostMapping("/update")
public Result updateUser(@RequestBody @Validated(UserDTO.Update.class) UserDTO userDTO) {
    // 校验通过,才会执行业务逻辑处理
    return Result.ok();
}

嵌套(递归)校验

前面的示例中,DTO类里面的字段都是基本数据类型和String类型。但是实际场景中,有可能某个字段也是一个对象,这种情况先,可以使用嵌套校验。比如,上面保存User信息的时候同时还带有Job信息。需要注意的是,此时如果是 post json 请求,无法完成对嵌套类字段校验,必须在 DTO类的对应字段必须标记@Valid注解。但如果是 GET 请求,是正常校验 job 下的所有属性的

@Data
public class UserDTO {

    @Min(value = 10000000000000000L, groups = Update.class)
    private Long userId;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 2, max = 10, groups = {Save.class, Update.class})
    private String userName;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})
    private String account;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})
    private String password;

    @NotNull(groups = {Save.class, Update.class})
    @Valid
    private Job job;

    @Data
    public static class Job {

        @Min(value = 1, groups = Update.class)
        private Long jobId;

        @NotNull(groups = {Save.class, Update.class})
        @Length(min = 2, max = 10, groups = {Save.class, Update.class})
        private String jobName;

        @NotNull(groups = {Save.class, Update.class})
        @Length(min = 2, max = 10, groups = {Save.class, Update.class})
        private String position;
    }

    /**
     * 保存的时候校验分组
     */
    public interface Save {
    }

    /**
     * 更新的时候校验分组
     */
    public interface Update {
    }
}

集合校验

就像下面的写法,方法的参数为集合时,如何检验元素的约束呢?

/**
 * 集合类型参数元素.
 *
 * @param student the student
 * @return the rest
 */
@PostMapping("/batchadd")
public Rest<?> batchAddStudent(@Valid @RequestBody List<Student> student) {
    return RestBody.okData(student);
}

同样是在类上添加@Validated注解。注意一定要添加到方法所在的类上才行。这时候会抛出ConstraintViolationException异常

自定义校验

业务需求总是比框架提供的这些简单校验要复杂的多,可以自定义校验来满足需求。自定义Spring validation非常简单,如下两个案例

1、 假设自定义加密id(由数字或者a-f的字母组成,32-256长度)校验,主要分为两步:

自定义约束注解

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {EncryptIdValidator.class})
public @interface EncryptId {

    // 默认错误消息
    String message() default "加密id格式错误";

    // 分组
    Class<?>[] groups() default {};

    // 负载
    Class<? extends Payload>[] payload() default {};
}

实现ConstraintValidator接口自定义校验器

public class EncryptIdValidator implements ConstraintValidator<EncryptId, String> {

    private static final Pattern PATTERN = Pattern.compile("^[a-f\\d]{32,256}$");

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // 不为null才进行校验
        if (value != null) {
            Matcher matcher = PATTERN.matcher(value);
            return matcher.find();
        }
        return true;
    }
}

这样就可以使用@EncryptId进行参数校验了!

2、 校验集合中的指定属性是否存在重复

实现校验注解,功能如注释所示

@Target({ElementType.FIELD, ElementType.PARAMETER})  
@Retention(RetentionPolicy.RUNTIME)  
@Documented  
// 指定校验器
@Constraint(validatedBy = UniqueValidator.class)  
public @interface Unique {  

    // 用于自定义验证信息
    String message() default "字段存在重复";  

    // 指定集合中的待校验字段
    String[] field();  

    // 指定分组
    Class<?>[] groups() default {};  
}

实现对应的校验器,主要校验逻辑在 isValid 方法:获取集合中指定字段,并组装为 set,比较 set 和集合的长度,以判断集合中指定字段是否存在重复。

// 实现ConstraintValidator<T, R>接口,T为注解的类型,R为注解的字段类型
public class UniqueValidator implements ConstraintValidator<Unique, Collection<?>> {  

    private Unique unique;  

    @Override  
    public void initialize(Unique constraintAnnotation) {  
        this.unique = constraintAnnotation;  
    }  

    @Override  
    public boolean isValid(Collection collection, ConstraintValidatorContext constraintValidatorContext) {
        // 集合为空直接校验通过
        if (collection == null || collection.size() == 0) {  
            return Boolean.TRUE;  
        }  
        // 从集合中获取filed中指定的待校验字段,看是否存在重复
        return Arrays.stream(unique.field())  
        .filter(fieldName -> fieldName != null && !"".equals(fieldName.trim()))  
        .allMatch(fieldName -> {
            // 收集集合collection中字段为fieldName的值,存入set并计算set的元素个数count
            int count = (int) collection.stream()  
                         .filter(Objects::nonNull)  
                         .map(item -> {  
                             Class<?> clazz = item.getClass();  
                             Field field;  
                             try {  
                                 field = clazz.getField(fieldName);  
                                 field.setAccessible(true);  
                                 return field.get(item);  
                             } catch (Exception e) {  
                                 return null;  
                             }  
                         })  
                         .collect(Collectors.collectingAndThen(Collectors.toSet(), Set::size)); 
            // set中元素个数count与集合长度比较,若不相等则说明collection中字段存在重复,校验不通过
            if (count != collection.size()) {  
                return false;  
            }  
            return true;  
        });  
    }  
}

枚举校验

在后台定义了枚举值来进行状态的流转,也是需要校验的,比如定义了颜色枚举:

public enum Colors {

    RED, YELLOW, BLUE

}

希望入参不能超出Colors的范围[“RED”, “YELLOW”, “BLUE”],这就需要实现ConstraintValidator<A extends Annotation, T>接口来定义一个颜色约束了,
其中泛型A为自定义的约束注解,泛型T为入参的类型,这里使用字符串,实现如下:

public class ColorConstraintValidator implements ConstraintValidator<Color, String> {
    private static final Set<String> COLOR_CONSTRAINTS = new HashSet<>();

    @Override
    public void initialize(Color constraintAnnotation) {
        Colors[] value = constraintAnnotation.value();
        List<String> list = Arrays.stream(value)
            .map(Enum::name)
            .collect(Collectors.toList());
        COLOR_CONSTRAINTS.addAll(list);

    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        return COLOR_CONSTRAINTS.contains(value);
    }
}

然后声明对应的约束注解Color,需要在元注解@Constraint中指明使用上面定义好的处理类ColorConstraintValidator进行校验。

@Constraint(validatedBy = ColorConstraintValidator.class)
@Documented
@Target({ElementType.METHOD, ElementType.FIELD,
        ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR,
        ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Color {
    // 错误提示信息
    String message() default "颜色不符合规格";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    // 约束的类型
    Colors[] value();
}

然后来试一下,先对参数进行约束:

@Data
public class Param {
    @Color({Colors.BLUE,Colors.YELLOW})
   private String color;
}

接口跟上面几个一样,调用下面的接口将抛出BindException异常

编程式校验

上面的示例都是基于注解来实现自动校验的,在某些情况下,可能希望以编程方式调用验证。

1、配置validator

@Configuration  
public class ValidatorConfiguration {  
    @Bean  
    public Validator validator() {  
        ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)  
        .configure()  
        // 设置是否开启快速失败模式  
        //.failFast(true)  
        .buildValidatorFactory();  
        return validatorFactory.getValidator();  
    }  
}

2、获取 validator 并校验

@Autowired
private javax.validation.Validator validator;

// 编程式校验
@PostMapping("/saveWithCodingValidate")
public Result saveWithCodingValidate(@RequestBody UserDTO userDTO) {
    Set<ConstraintViolation<UserDTO>> validate = globalValidator.validate(userDTO, UserDTO.Save.class);
    // 如果校验通过,validate为空;否则,validate包含未校验通过项
    if (validate.isEmpty()) {
        // 校验通过,才会执行业务逻辑处理

    } else {
        for (ConstraintViolation<UserDTO> userDTOConstraintViolation : validate) {
            // 校验失败,做其它逻辑
            System.out.println(userDTOConstraintViolation);
        }
    }
    return Result.ok();
}

快速失败(Fail Fast)

Spring Validation默认会校验完所有字段,然后才抛出异常。可以通过一些简单的配置,开启Fali Fast模式,一旦校验失败就立即返回。

@Bean
public Validator validator() {
    ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
            .configure()
            // 快速失败模式
            .failFast(true)
            .buildValidatorFactory();
    return validatorFactory.getValidator();
}

Dubbo 接口校验

  1. 可在@DubboService注解中,设置validation参数为true开启生产者的字段验证
@DubboService(version = "1.0.0", validation="true")
public class DubboApiImpl implements DubboApi {
    
}
  1. 该方式返回的信息对使用者不友好,可通过 Dubbo 的 filter自定义校验逻辑和返回信息。需要注意的是,在 Dubbo 中有自己的 IOC 实现来控制容器,因此需提供 setter 方法,供 Dubbo 调用。
@Activate(  
    group = {"provider"},  
    value = {"customValidationFilter"},  
    order = 10000  
)  
@Slf4j  
public class CustomValidationFilter implements Filter {  

    private javax.validation.Validator validator;  

    // duubo会调用setter获取bean
    public void setValidator(javax.validation.Validator validator) {  
        this.validator = validator;  
    } 

    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {  
        if (this.validator != null && !invocation.getMethodName().startsWith("$")) {  
            // 补充字段校验,返回信息的组装以及异常处理
        }  
        return invoker.invoke(invocation);  
    }  
}

校验结果接收

BindingResult接收

这种方式需要在Controller层的每个接口方法参数中指定,Validator会将校验的信息自动封装到其中。这也是上面例子中一直用的方式。如下:

@PostMapping("/add")
public String add(@Valid @RequestBody ArticleDTO articleDTO, BindingResult bindingResult){}

这种方式的弊端很明显,每个接口方法参数都要声明,同时每个方法都要处理校验信息,显然不现实,舍弃。这种写法不会将异常处理的结果返回给全局异常处理

此种方式还有一个优化的方案:使用AOP,在Controller接口方法执行之前处理BindingResult的消息提示,不过这种方案仍然不推荐使用。

统一异常处理接收

如果校验失败,会抛出MethodArgumentNotValidException或者ConstraintViolationException异常。在实际项目开发中,通常会用统一异常处理来返回一个更友好的提示。比如系统要求无论发送什么异常,http的状态码必须返回200,由业务码去区分系统的异常情况。

@RestControllerAdvice
public class CommonExceptionHandler {

    @ExceptionHandler({MethodArgumentNotValidException.class})
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
        BindingResult bindingResult = ex.getBindingResult();
        StringBuilder sb = new StringBuilder("校验失败:");
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");
        }
        String msg = sb.toString();
       return Result.fail(BusinessCode.参数校验失败, msg);
    }

    @ExceptionHandler({ConstraintViolationException.class})
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public Result handleConstraintViolationException(ConstraintViolationException ex) {
        return Result.fail(BusinessCode.参数校验失败, ex.getMessage());
    }
}

网站公告

今日签到

点亮在社区的每一天
去签到