跳至主要內容

JDK validation 功能(spring)

Steven小于 1 分钟javaspring

对于 JDK validation,Spring 做了封装 spring-boot-starter-validation,引用于如 spring-boot-starter-web 项目中。

Validation with Spring Boot - the Complete Guide

校验捕获

在 web 项目中,通过接收 BindingResult 参数来处理请求中的参数校验异常。

处理类
package org.example.handler;

import org.example.entity.R;
import org.example.entity.UserParam;
import org.example.entity.UserVo;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;
import javax.validation.groups.Default;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * 整理 {@link Valid} 和 {@link Validated} 的用法
 */
@RestController
@RequestMapping("/user")
@Validated // 💡在类上注解,开启方法参数校验(即:校验伴随校验注解的参数)
public class UserHandler {
    /**
     * 使用 {@link Valid} 触发校验,将全部校验失败结果存入 {@link BindingResult}
     */
    @GetMapping("/getUser")
    public R getUser(@RequestBody @Valid UserParam param, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return handleValidatedErrors(bindingResult);
        }
        UserVo userVo = UserVo.builder()
                .id(param.getId())
                .username(param.getUsername())
                .build();
        return R.SUCCESS(userVo);
    }

    /**
     * {@link Validated} 优点一:在方法参数中使用,可以指定校验 “分组” 功能
     */
    @PostMapping("/insertUser")
    public R insertUser(@RequestBody @Validated(value = {UserParam.Insert.class, Default.class}) UserParam param,
                        BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return handleValidatedErrors(bindingResult);
        }
        return R.SUCCESS("ok");
    }

    /**
     * {@link Validated} 优点二:在直接对方法参数开启校验功能
     * (对校验失败的抛出 {@link javax.validation.ConstraintViolationException} 异常;
     * 对参数转换错误的抛出 {@link org.springframework.web.method.annotation.MethodArgumentTypeMismatchException} 异常;
     * 参数缺失异常 {@link org.springframework.web.bind.MissingServletRequestParameterException})
     */
    @GetMapping("/reflect")
    public R reflect(@RequestParam("id") @NotNull @Positive Integer num) {
        return R.SUCCESS(num);
    }

    private static R handleValidatedErrors(BindingResult bindingResult) {
        List<Map<String, Object>> errors = bindingResult.getAllErrors()
                .stream().map(e -> {
                    Map<String, Object> map = new HashMap<>();
                    map.put("ObjectName", e.getObjectName());
                    map.put("DefaultMessage", e.getDefaultMessage());
                    // map.put("Arguments", e.getArguments()); // 全部入参
                    if (e instanceof FieldError) {
                        map.put("Field", ((FieldError) e).getField());
                        map.put("RejectedValue", ((FieldError) e).getRejectedValue());
                    }
                    return map;
                }).collect(Collectors.toList());
        return R.ERROR(errors);
    }
}

全局拦截校验异常

在 web 项目中,通过 @ControllerAdvice@ExceptionHandler({BindException.class}) 注解可以拦截校验异常。

但对于不同请求、不同参数接受方式,拦截异常不同:

  • @NotNull/Positive 这些注解直接修饰参数时,抛出 ConstraintViolationException 异常。 (💡 这些注解直接修饰的话,需要在类上添加 @Validated 注解,校验才会工作)
  • @Valid/@Validated 修饰 @RequestBody 参数时,抛出 MethodArgumentNotValidException 异常。
  • @Valid/@Validated 修饰非 @RequestBody 参数时,抛出 BindException 异常。
处理类
package org.example.handler;

import org.example.entity.R;
import org.example.entity.TaskParam;
import org.example.entity.TaskVo;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 整理校验异常
 */
@RestController
@RequestMapping("/task")
@Validated
public class TaskHandler {
    /**
     * 对于 {@link RequestParam}/{@link PathVariable} 修饰的 URL 参数:
     * 在类上用 {@link Validated} 修饰且参数用 {@link javax.validation.Constraint} 修饰时,
     * 校验失败将抛出 {@link ConstraintViolationException} 异常
     */
    @GetMapping("/param")
    public R param(@RequestParam("num") @Positive @NotNull Integer num) {
        return R.SUCCESS(num);
    }
    @GetMapping("/param/{num}")
    public R paramPath(@PathVariable("num") @Positive @NotNull Integer num) {
        return R.SUCCESS(num);
    }

    /**
     * 对于默认的表单参数:
     * 参数用 {@link Valid}/{@link Validated} 修饰时、
     * 校验失败将抛出 {@link BindException} 异常
     */
    @GetMapping("/formValid")
    public R formValid(@Valid TaskParam param) {
        TaskVo taskVo = TaskVo.builder()
                .id(param.getId())
                .build();
        return R.SUCCESS(taskVo);
    }
    @GetMapping("/formValidated")
    public R formValidated(@Validated TaskParam param) {
        TaskVo taskVo = TaskVo.builder()
                .id(param.getId())
                .build();
        return R.SUCCESS(taskVo);
    }

    /**
     * 对于 {@link RequestBody} 修饰的 json 参数:
     * 参数用 {@link Valid}/{@link Validated} 修饰时、
     * 校验失败将抛出 {@link MethodArgumentNotValidException} 异常
     */
    @GetMapping("/jsonValid")
    public R jsonValid(@RequestBody @Valid TaskParam param) {
        TaskVo taskVo = TaskVo.builder()
                .id(param.getId())
                .build();
        return R.SUCCESS(taskVo);
    }
    @GetMapping("/jsonValidated")
    public R jsonValidated(@RequestBody @Validated TaskParam param) {
        TaskVo taskVo = TaskVo.builder()
                .id(param.getId())
                .build();
        return R.SUCCESS(taskVo);
    }

    /**
     * 通过 {@link ExceptionHandler} 拦截当前类的 {@link ConstraintViolationException} 异常
     * (如果考虑拦截全部 handler 异常,可以将 {@link ExceptionHandler} 定义在 {@link ControllerAdvice} 修饰的类中)
     */
    @ExceptionHandler({ConstraintViolationException.class}) // url 参数提交
    public R handleConstraintViolationException(ConstraintViolationException exception) {
        Set<ConstraintViolation<?>> constraintViolations = exception.getConstraintViolations();
        List<Map<String, Object>> collect = constraintViolations.stream().map(e -> {
            Map<String, Object> map = new HashMap<>();
            map.put("PropertyPath", e.getPropertyPath().toString());
            map.put("InvalidValue", e.getInvalidValue());
            map.put("Message", e.getMessage());
            return map;
        }).collect(Collectors.toList());
        return R.ERROR(collect);
    }
    @ExceptionHandler({BindException.class}) // 表单提交
    public R handleBindException(BindException exception) {
        BindingResult bindingResult = exception.getBindingResult();
        return getR(bindingResult);
    }
    @ExceptionHandler({MethodArgumentNotValidException.class}) // json 提交
    public R handleBindException(MethodArgumentNotValidException exception) {
        BindingResult bindingResult = exception.getBindingResult();
        return getR(bindingResult);
    }

    private static R getR(BindingResult bindingResult) {
        List<Map<String, Object>> errors = bindingResult.getAllErrors()
                .stream().map(e -> {
                    Map<String, Object> map = new HashMap<>();
                    map.put("ObjectName", e.getObjectName());
                    map.put("DefaultMessage", e.getDefaultMessage());
                    // map.put("Arguments", e.getArguments()); // 全部入参
                    if (e instanceof FieldError) {
                        map.put("Field", ((FieldError) e).getField());
                        map.put("RejectedValue", ((FieldError) e).getRejectedValue());
                    }
                    return map;
                }).collect(Collectors.toList());
        return R.ERROR(errors);
    }
}