Spirng Boot 项目中校验前端恳求参数,高效易维护的手段推荐运用@Valid@Validated注解,开发时应当尽量防止运用一大堆if else 对恳求参数一个个判别校验。

一、@Valid@Validated 比照

比照项 @Valid @Validated
提供方 JSR-303规范,简略理解便是Java EE中界说的一套Java Bean校验规范 Spring,能够理解成是对JSR-303规范规范的二次封装
包途径 javax.validation org.springframework.validation.annotation
标示方位 办法、目标特点、 结构办法、 参数 类、办法、参数
支撑分组 不支撑 支撑
支撑嵌套 支撑 不支撑

1. 关于Jar包和常用注解

(1)@Valid

jakarta.validation-api.jar 中,它是一套标示的JSR-303规范的完结,主要有以下注解:

Spring Boot 项目中参数校验和异常拦截

(2)@Validated

Spring Boot项目中主要是依赖 hibernate-validator.jar,除了提供了JSR-303的规范,还扩展了一些注解,如下图所示:

Spring Boot 项目中参数校验和异常拦截

所以在 Spring Boot 项目里面引入 spring-boot-starter-validation ,就会主动引入 hibernate-validator 的Jar包,hibernate-validator 官方文档。

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

二、@Valid@Validated 运用技巧

1. 前端运用JSON串方法提交参数

Content-Type : application/json,这种方法后端接口能够直接运用 Java Bean 目标来承受恳求入参。

1.1 单个特点校验

import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotEmpty;
import java.io.Serializable;
@Getter
@Setter
public class ParamBody implements Serializable {
    @NotEmpty(message = "hour不能空")
    private String hour;
}    

Spring Boot 项目中参数校验和异常拦截
咱们运用 @Validated 来校验参数值,@RequestBody 承受JSON串参数。

1.2 嵌套特点校验

import lombok.Getter;
import lombok.Setter;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.List;
@Getter
@Setter
public class ParamBody implements Serializable {
    @Valid
    @NotNull(message = "用户信息不能为空")
    private User user;
    @NotEmpty(message = "hour不能空")
    private String hour;
}
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;
import java.io.Serializable;
@Getter
@Setter
public class User implements Serializable {
    @NotEmpty(message = "用户称号不能为空")
    @Size(message = "用户称号不能超过 {max} 个字符", max = 10)
    private String username;
}

Spring Boot 项目中参数校验和异常拦截

此刻的嵌套意思便是校验 ParamBody时也要对User的一切特点进行校验,完结嵌套校验留意以下几点

  • 对要校验的嵌套特点有必要运用 @Valid 注解规范
  • 被嵌套的目标,也便是事例里的 User 目标内部特点校验时注解请运用 javax.validation.constraints 包下的注解,不要运用 org.hibernate.validator.constraints 包下的注解,有时候会导致校验失效!!!

Spring Boot 项目中参数校验和异常拦截

1.3 分组校验

在实际项目开发中,针对多个前端办法用一个实体类来承受恳求参数,但是这两个办法的恳求参数校验规矩不一样,此刻就应该用分组校验来完结多规矩校验逻辑。

  • 界说分组校验接口
public interface OneType {
}
public interface TwoType {
}

该接口中不需要界说任何办法,仅仅用来标记分组罢了。

  • 指定分组
import javax.validation.constraints.Pattern;
import java.io.Serializable;
@Getter
@Setter
public class ParamBody implements Serializable {
    @Pattern(regexp = "1", message = "类型值应该等于1", groups = OneType.class)
    @Pattern(regexp = "2", message = "类型值应该等于2", groups = TwoType.class)
    private String type;
}

Spring Boot 项目中参数校验和异常拦截

中心点便是:@Validated 中的分组有必要和你要校验的目标特点分组标示一致,Default.class 是框架自带的默许分组

1.4 复杂校验逻辑自界说注解

比如前端提交的某个参数有必要是枚举值中的一个,咱们尝试用自界说注解来完结该校验逻辑。

  • 界说校验注解
package com.example.test.valid.ex.constraints;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Target({METHOD, FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = {ColourConstraintValidator.class})
public @interface ContainColour {
    String message() default "有必要指定色彩类型";
    // 一切答应的色彩值
    String[] colourArray() default {};
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
  • 界说校验器 ConstraintValidator
public class ColourConstraintValidator implements ConstraintValidator<ContainColour, String> {
    private String[] colourArrays;
    // 完结自己的校验逻辑
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        List<String> colourNameList = new ArrayList<>();
        colourNameList.add("YELLOW");
        colourNameList.add("RED");
        colourNameList.add("BLUE");
        if (colourNameList.contains(value)) {
            return true;
        }
        return false;
    }
    // 初始化参数
    @Override
    public void initialize(ContainColour constraintAnnotation) {
        colourArrays = constraintAnnotation.colourArray();
    }
}

1.5 分组按照指定次序校验

  • 默许状况下,参数校验是没有特定的次序的。但在某些状况下,控制参数校验次序是很有用的
  • 比如自驾游,首先要判别气候条件是否答应驾车出行,然后要检查轿车各项指标是否安全
  • 利用 @GroupSequence 便能完结这样的事务场景校验,只要有一个条件校验失利,后边的条件都不会被校验
// 气候分组
public interface WeatherChecks {
}
// 司机分组
public interface DriverChecks {
}

(1)界说分组次序

import javax.validation.GroupSequence;
import javax.validation.groups.Default;
@GroupSequence({Default.class, WeatherChecks.class, DriverChecks.class})
public interface TravelChecks {
}

(2)界说参数承受目标

import javax.validation.constraints.Pattern;
import java.io.Serializable;
@Getter
@Setter
public class TravelBody implements Serializable {
    @Pattern(regexp = "^老司机$", message = "有必要是老司机", groups = DriverChecks.class)
    private String driver;
    @Pattern(regexp = "^(晴天)$", message = "有必要是晴天", groups = WeatherChecks.class)
    private String weather;
}

Spring Boot 项目中参数校验和异常拦截

(3)测试成果

Spring Boot 项目中参数校验和异常拦截

如果咱们没有指定次序的话,默许状况下可能会先校验 TravelBodydriver 参数,但当咱们运用了 @GroupSequence 之后,他是按照咱们指定的分组次序进行校验的

1.6 动态增加分组校验

@GroupSequence静态地重新界说了组校验次序,Hibernate Validator 还提供了一个 SPI,用于依据目标特点动态的增加分组校验次序。即运用 @GroupSequenceProvider 注解。

Spring Boot 项目中参数校验和异常拦截

假设咱们有如图所示的表单页面,只要当前面的复选框勾选之后才对后边的输入框填写的值进行校验,此刻你该怎么动态校验呢?

(1)界说参数目标

Spring Boot 项目中参数校验和异常拦截

@Getter
@Setter
@GroupSequenceProvider(value = OvertimeGroupSequenceProvider.class)
public class OvertimeBody implements Serializable {
    @NotNull
    private Integer upDaySwitch;
    @NotNull
    @Range(min = 1, max = 2, message = "上半天加班小时填写不正确", groups = UpDayGroup.class)
    private Integer upDayHour;
    @NotNull
    private Integer downDaySwitch;
    @NotNull
    @Range(min = 1, max = 3, message = "下半天加班小时填写不正确", groups = DownDayGroup.class)
    private Integer downDayHour;
}

(2)界说校验器

import com.example.test.valid.ex.bean.OvertimeBody;
import org.hibernate.validator.spi.group.DefaultGroupSequenceProvider;
import java.util.ArrayList;
import java.util.List;
public class OvertimeGroupSequenceProvider implements DefaultGroupSequenceProvider<OvertimeBody> {
    @Override
    public List<Class<?>> getValidationGroups(OvertimeBody overtimeBody) {
        List<Class<?>> defaultGroupSequence = new ArrayList<>();
        defaultGroupSequence.add(OvertimeBody.class);
        // 这儿必定要做判空处理
        if (overtimeBody == null) {
            return defaultGroupSequence;
        }
        Integer upDaySwitch = overtimeBody.getUpDaySwitch();
        Integer downDaySwitch = overtimeBody.getDownDaySwitch();
        if (upDaySwitch == 1) {
            defaultGroupSequence.add(UpDayGroup.class);
        }
        if (downDaySwitch == 1) {
            defaultGroupSequence.add(DownDayGroup.class);
        }
        return defaultGroupSequence;
    }
}

(3)接口恳求校验

Spring Boot 项目中参数校验和异常拦截

  • 不做任何校验:

Spring Boot 项目中参数校验和异常拦截

  • 只校验其间一个开关

Spring Boot 项目中参数校验和异常拦截

1.7 List集合特点校验

@Getter
@Setter
public class ParamBody implements Serializable {
    @Valid
    private List<OvertimeBody> overtimeBodyList;
    @Valid
    private List<TimeInterval> breakTimes;
}
@Getter
@Setter
public class TimeInterval implements Serializable {
    @NotNull(message = "开端时刻不能为空")
    private String startTime;
    @NotNull(message = "结束时刻不能为空")
    private String endTime;
}

(1)运用 @GroupSequenceProvider 方法来校验

(2)参数界说时运用 ArrayList

Spring Boot 项目中参数校验和异常拦截

@Getter
@Setter
public class TimeInterval implements Serializable {
    @NotEmpty(message = "开端时刻不能为空")
    private String startTime;
    @NotEmpty(message = "结束时刻不能为空")
    private String endTime;
}

2. 前端运用表单方法提交参数

Content-Type : application/x-www-form-urlencoded; charset=UTF-8,比如表单提交数据如下:

Spring Boot 项目中参数校验和异常拦截

(1)接口界说

@PostMapping("/save-json6")
public String saveJsonBody6(HttpServletRequest request) {
    String data = request.getParameter("data");
    OvertimeBody body = JSON.parseObject(data, OvertimeBody.class);
    ValidatorUtil.validate(body);
    return "success";
}

(2)编程式校验工具类

BizException是界说的事务反常,ApiErrorCodeEnum 是事务错误码。

public class ValidatorUtil {
    private final static Logger log = LoggerFactory.getLogger(ValidatorUtil.class);
    private final static Validator validator;
    static {
        validator = Validation.buildDefaultValidatorFactory().getValidator();
    }
    public static void validate(Object object, Class<?>... groups) throws BizException {
        Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
        if (!constraintViolations.isEmpty()) {
            StringBuilder msg = new StringBuilder();
            Iterator<ConstraintViolation<Object>> iterator = constraintViolations.iterator();
            while (iterator.hasNext()) {
                ConstraintViolation<Object> constraint = iterator.next();
                msg.append(constraint.getMessage()).append(',');
            }
            String errorMsg = msg.substring(0, msg.toString().lastIndexOf(','));
            ApiErrorCodeEnum.PARAMETER.setMessage(errorMsg);
            throw new BizException(ApiErrorCodeEnum.PARAMETER);
        }
    }
}

三、Spirng Boot 中一致阻拦参数检验反常

(1)界说错误码枚举类 ApiErrorCodeEnum

public enum ApiErrorCodeEnum implements Serializable {
    SUCCESS("1", "成功"),
    PARAMETER("2", "参数反常"),
    UNKNOWN_ERROR("99", "不知道反常");
    private String code;
    private String message;
    ApiErrorCodeEnum(String code, String message) {
        this.code = code;
        this.message = message;
    }
    public String getCode() {
        return code;
    }
    public void setCode(String code) {
        this.code = code;
    }
    public String getMessage() {
        return message;
    }
    public void setMessage(String message) {
        this.message = message;
    }
}

(2)界说事务反常 BizException

public class BizException extends RuntimeException {
    private ApiErrorCodeEnum codeEnum;
    public BizException() {
        super();
    }
    public BizException(ApiErrorCodeEnum codeEnum) {
        this.codeEnum = codeEnum;
    }
    public BizException(String message, ApiErrorCodeEnum codeEnum) {
        super(message);
        this.codeEnum = codeEnum;
    }
    public BizException(String message, Throwable cause, ApiErrorCodeEnum codeEnum) {
        super(message, cause);
        this.codeEnum = codeEnum;
    }
    public BizException(Throwable cause, ApiErrorCodeEnum codeEnum) {
        super(cause);
        this.codeEnum = codeEnum;
    }
    public ApiErrorCodeEnum getCodeEnum() {
        return codeEnum;
    }
}

(3)恳求成果呼应封装

@Setter
@Getter
public class ResultBody<T> implements Serializable {
    private boolean status;
    // 呼应码
    private String code;
    // 呼应描绘信息
    private String message;
    // 呼应数据
    private T data;
    public ResultBody() {
    }
    private ResultBody(T data) {
        this.data = data;
    }
    private ResultBody(String code, String msg) {
        this.code = code;
        this.message = msg;
    }
    public static <T> ResultBody<T> success() {
        ResultBody<T> result = new ResultBody<>();
        result.setCode("1");
        result.setStatus(Boolean.TRUE);
        result.setMessage("成功");
        return result;
    }
    public static <T> ResultBody<T> error(String code, String message) {
        ResultBody<T> result = new ResultBody<>(code, message);
        result.setStatus(Boolean.FALSE);
        return result;
    }
}

1. 大局反常阻拦

为什么要做大局反常阻拦?

  • 比如有时候,我在浏览器输入了一个恳求地址URL,但这个URL的域名是对的,但是接口途径是错的,此刻就会出现 Spring Boot 默许的 whitelabel error page 页面

Spring Boot 项目中参数校验和异常拦截

  • Spring mvc 中界说的 Interceptor 阻拦器本身出现反常,此刻的逻辑还没到特定要的事务 Controller,此刻也需要对反常信息进行一致阻拦

1.1 大局反常阻拦完结

Spring Boot 项目中参数校验和异常拦截

@Controller
public class GlobalErrorController extends BasicErrorController {
    public GlobalErrorController(ErrorAttributes errorAttributes) {
        super(errorAttributes, new ErrorProperties());
    }
    @RequestMapping(consumes = MediaType.ALL_VALUE, produces = MediaType.ALL_VALUE)
    public ResponseEntity<Map<String, Object>> errorJson(HttpServletRequest request) {
        HttpStatus status = getStatus(request);
        if (status == HttpStatus.NO_CONTENT) {
            return new ResponseEntity<>(status);
        }
        Map<String, Object> resutBody = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
        resutBody.put("status", false);
        resutBody.put("code", ApiErrorCodeEnum.UNKNOWN_ERROR.getCode());
        resutBody.put("message", ApiErrorCodeEnum.UNKNOWN_ERROR.getMessage());
        return new ResponseEntity<>(resutBody, status);
    }
}

阻拦成果:

Spring Boot 项目中参数校验和异常拦截

2. 事务反常阻拦

运用 @ControllerAdvice + @ExceptionHandler 主键来完结

/**
 * 事务错误信息一致阻拦
 */
@ControllerAdvice(basePackages = {"com.example.test.valid.ex.controller"})
public class ApiExceptionHandler extends ResponseEntityExceptionHandler {
    private static final Logger log = LoggerFactory.getLogger(ApiExceptionHandler.class);
    @Override
    protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {
        return new ResponseEntity<>(handlerException(ex), HttpStatus.OK);
    }
    @ExceptionHandler(Exception.class)
    public ResultBody handlerException(Throwable e) {
        if (BizException.class.equals(e.getClass())) {
            // 事务反常处理
            ApiErrorCodeEnum apiError = ((BizException) e).getCodeEnum();
            ResultBody<Object> error = ResultBody.error(apiError.getCode(), apiError.getMessage());
            return error;
        } else if (e.getClass() == MethodArgumentNotValidException.class) {
            // 参数校验反常处理
            MethodArgumentNotValidException validException = (MethodArgumentNotValidException) e;
            BindingResult bindingResult = validException.getBindingResult();
            String defaultMessage = bindingResult.getAllErrors().get(0).getDefaultMessage();
            ApiErrorCodeEnum.PARAMETER.setMessage(defaultMessage);
            ResultBody<Object> error = ResultBody.error(ApiErrorCodeEnum.PARAMETER.getCode(), ApiErrorCodeEnum.PARAMETER.getMessage());
            log.error("restful api 参数校验反常,resultBody : {}", JSON.toJSONString(error), e);
            return error;
        } else if (e.getClass() == MissingServletRequestParameterException.class) {
            // @RequestParam 注解中 required = true 的状况阻拦
            return ResultBody.error(ApiErrorCodeEnum.PARAMETER.getCode(), ApiErrorCodeEnum.PARAMETER.getMessage());
        } else {
            log.error("不知道反常", e);
            return ResultBody.error(ApiErrorCodeEnum.UNKNOWN_ERROR.getCode(), ApiErrorCodeEnum.UNKNOWN_ERROR.getMessage());
        }
    }
}

Spring Boot 项目中参数校验和异常拦截
唯一需要留意的是这儿的包扫描途径必定是你 controller 包所在的途径

四、写在最终

  • 本文事例运用的是 Spirng Boot 2.7.6 版本