在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规范的完结,主要有以下注解:
(2)@Validated
在 Spring Boot
项目中主要是依赖 hibernate-validator.jar
,除了提供了JSR-303的规范,还扩展了一些注解,如下图所示:
所以在 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;
}
咱们运用 @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;
}
此刻的嵌套意思便是校验 ParamBody
时也要对User
的一切特点进行校验,完结嵌套校验留意以下几点:
- 对要校验的嵌套特点有必要运用
@Valid
注解规范 -
被嵌套的目标,也便是事例里的
User
目标内部特点校验时注解请运用javax.validation.constraints
包下的注解,不要运用org.hibernate.validator.constraints
包下的注解,有时候会导致校验失效!!!
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;
}
中心点便是:@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;
}
(3)测试成果
如果咱们没有指定次序的话,默许状况下可能会先校验 TravelBody
的 driver
参数,但当咱们运用了 @GroupSequence
之后,他是按照咱们指定的分组次序进行校验的。
1.6 动态增加分组校验
@GroupSequence
静态地重新界说了组校验次序,Hibernate Validator
还提供了一个 SPI
,用于依据目标特点动态的增加分组校验次序。即运用 @GroupSequenceProvider
注解。
假设咱们有如图所示的表单页面,只要当前面的复选框勾选之后才对后边的输入框填写的值进行校验,此刻你该怎么动态校验呢?
(1)界说参数目标
@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)接口恳求校验
- 不做任何校验:
- 只校验其间一个开关
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
@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
,比如表单提交数据如下:
(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 mvc
中界说的Interceptor
阻拦器本身出现反常,此刻的逻辑还没到特定要的事务Controller
,此刻也需要对反常信息进行一致阻拦
1.1 大局反常阻拦完结
@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);
}
}
阻拦成果:
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());
}
}
}
唯一需要留意的是这儿的包扫描途径必定是你 controller 包所在的途径
四、写在最终
- 本文事例运用的是
Spirng Boot 2.7.6
版本