写一个接口,大概的几个步骤:
- 参数校验
- 编写Service、Dao(SQL)
- Result统一结果封装返回值
- 如果是分布式,还可能涉及网关配置、服务引用等
参数校验归根结底就两种:手动校验、自动校验
实际生活中常用的两种:
封装ValidationUtils
使用Spring 自动的 Validation
对于上面两种方式,Spring都提供了解决方案。 除了Spring 的 Validation,Spring还提供了Assert
Assert:
- hasText 存在字符串 不存在字符串则报错
-
notNull 不为空 为空则报错
-
isTrue 是否为真 假则报错
当不满足条件时会爆出illegalArgumentException,所以使用Assert时全局异常处理非法参数异常即可。
如果对检验的自由度偏高的话,可以自己封装一个ValidationUtils
封装ValidationUtils
封装校验工具类两种思路:
- 检验并返回结果,调用者自行处理
- 检验失败直接抛异常
方式一:检验并返回结果,调用者自行处理
1.方法只返回true/false 2.调用者根据返回值自行处理,抛异常或者用Result封装
/ 检验方法只返回true/false @param id @return / public static final Boolean isNotId(Long id){ if (id == null){ return true; } return id < 0; } / 调用者根据返回值自行处理 / public void method(){ Long id = -1L; ValidationUtilsV1.isNotId(id); }
其实这种方式跟不封装区别不大,只是把判断逻辑放到检验工具类中而已
方式二:检验失败直接抛异常
一般结合@RestControllerAdvice进行全局异常处理:
public class ValidationUtilsV3 {
public static final String NULL_FORMAT = "%s不能为空";
public static final String LESS_THAN_ZERO = "%s不能小于0";
public static final void isNotNull(Object o,String filedName){
if (o == null){
throw new ValidationException(String.format(NULL_FORMAT,filedName));
}
}
public static final void isLessThan0(Integer num,String filedName){
isNotNull(num,filedName);
if (num < 0){
throw new ValidationException(String.format(LESS_THAN_ZERO,filedName));
}
}
}
@Slf4j @RestControllerAdvice public class GlobalExceptionHandler {
/**
* ValidationException 异常处理
*/
public <T> T handleValidationExcepion(ValidationException e){
// 打印精准的错误日志,方便后端排查
log.warn("校验参数异常:{}",e.getMessage(),e);
return (T) "校验参数异常";
}
}
Spring Validation
Spring基于注解的检验逻辑常用的有:
- @Validated
- @NotNull
- @NotBlank
- @NotEmpty
- @Positive
- @Length
- @Max
- @Min
JSR303的@Valid和Spring的@Validated有什么关系呢?
Spring的@Validated是基于@Valid扩展的,所以相对来说Spring的@Validated功能更强大,但在嵌套校验中@Valid却能够支持。
SpringBoot2.3.x之前可以直接使用@Validated及@Valid,SpringBoot2.3.x以后需要额外引入依赖:
org.hibernate hibernate-validator 6.0.1.FinalGET散装参数校验:校验不满足条件抛出ConstraintViolationException违反约束异常。
1.如果get的参数不多,可以使用散装参数校验,需要在校验方法的类上加@Validated注解
2.因为当检验不满足条件时会爆出异常,所以我们可以在全局异常处理捕获该异常,并返回给前台友好的提示信息。在Spring2.3.X以前,如果没有捕获异常会返回Spring的默认异常JSON。 服务端则抛出ConstraintViolationException
Get DTO参数校验:校验不满足条件时抛出BindException绑定异常
1.使用Get Dto参数校验时,在Dto类中用检验参数检验之外,还需要在检验的Dto前加上@Validated注解。
2.检验不满足条件时,此时抛出的异常为BindException,如果后端全局异常处理器为处理该异常,SpringBoot将会自行处理成异常Json信息给前端。所以我们可以在全局异常处理器中进行处理。
Post参数校验:MethodArgumentNoValidException方法参数不校验异常
1.如果前端传来Json字符串时,需要使用@RequestBody转换一下,然后在需要校验的参数前面加上@Validated注解。
2.检验不满足条件时,此时抛出的异常为MethodArgumentNoValidException
其他场景校验
- 嵌套校验
- 分组校验
- List校验
- 嵌套校验 @Validated不支持嵌套校验,@Valid支持
@Data public class User {
@NotNull(message = "id不能为空")
private Long id;
@NotNull(message = "age不能为空")
@Max(value = 30,message = "程序员年龄最大不能超过30岁")
@Min(value = 16,message = "程序员年龄最小不能低于16岁")
private Integer age;
@NotNull(message = "部门不能为空")
@Valid
private Department department;
} class Department{
@NotNull(message = "id不能为空")
Long id;
}
1.需要在嵌套校验的类对象前面加上JSR303的@Valid
2.嵌套校验不满足条件时会抛出 IllegalStateException
分组校验
1.首先在设置好检验参数的的分组,在检验注解的group注解附上分组的值(值通常为接口的类对象),其次在校验时指定检验的分组,即指定@Validated(Add.class)等。
注意: interface Add这些接口只是用来作为标记,可以抽取出来复用。
这些接口都可以基础Default
继承Default后,除非显示指定,否则只要加了@NotNull等注解,就默认是此分组,但显示指定Group后,就会按照指定的分组进行校验。
不建议使用Default,理解混乱
List校验
@Validated不支持此种校验,可以借鉴@Valid嵌套校验的方式,可看做特殊的嵌套校验。 可以将List在包一层,在内部创建List的对象。实现List 接口
实际开发中可以专门创建一个包存放Spring Validated 的类
Spring validatorUtils封装: 工具类一般定义成final,构造器私有化 public final class SpringValidatorUtils {
private SpringValidatorUtils(){}
public static final Validator VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator();
public static final <T> void valid(T param,Class<?> ... groupClass){
Set<ConstraintViolation<T>> validateSet = VALIDATOR.validate(param, groupClass);
if (CollectionUtils.isEmpty(validateSet)){
return;
}
String errorMsg = validateSet.stream().map(ConstraintViolation::getMessage).collect(Collectors.joining("&"));
throw new ValidationException(errorMsg);
}
}
代码其实很简单,做的事情其实和@Validated一样。@Validated通过注解的方式让Spring使用Validator去帮我们校验,异常由Spring抛出(多种异常),而工具类则是我们从Spring那里借来的Validator自己校验,抛出的异常可以为自己自定义的异常。
更灵活的Spring ValidatorUtils封装
调用者自己决定抛出异常还是返回错误信息。 public final class SpringValidatorAndReturnUtils {
private SpringValidatorAndReturnUtils(){}
public static final Validator VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator();
/**
* 校验基于注解校验的对象
* @param param 被校验对象
* @param isThrow 是否抛出异常
* @param <T>
*/
public static final <T> String validReq(T param,Boolean isThrow){
if (param == null){
return exceptionHandle("检验对象不能为空",isThrow);
}
Set<ConstraintViolation<T>> validateSet = VALIDATOR.validate(param);
ConstraintViolation<T> first = Iterables.getFirst(validateSet, null);
if (CollectionUtils.isEmpty(validateSet)){
return "";
}
String errorMsg = validateSet.stream().map(ConstraintViolation::getMessage).collect(Collectors.joining("&"));
return exceptionHandle(errorMsg,isThrow);
}
private static String exceptionHandle(String message, Boolean isThrow) {
if (isThrow){
throw new ValidationException(message);
}
return message;
}
}
为什么Spring 的@Validated这么方便还要封装工具类呢?
如果想在Service层做校验,使用SpringValidatorUtils会方便些,Service有接口和实现类;当然Service也能用注解方式校验。
工具类,想校验什么就写什么,不用考虑杂七杂八的分组,而Spring Validation更多的是关心分组。