前言:当我们在进行web 项目的开发时,对于前端传入的参数,都需要进行一些非空必填等的验证,然后在进行业务逻辑的处理,如果写一堆的if 判断很不优雅,那么有没有好的方式来帮忙处理,本文通过hibernate-validator 及jakarta.validation 结合的方式来完成请求参数的验证;
整合开始:
1 : 引入验证框架所需要的jar 包:
<!--validation 核心jar 内部引入了hibernate-validator 及jakarta.validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- web 请求依赖jar -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--lombok 插件便于生成get set 方法 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 阿里json 格式化工具-->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.19</version>
</dependency>
<!--处理jdbc报错依赖jar -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
2 controller 使用:
import com.example.springvalidate.dto.UserReqDto;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.*;
@Validated
@RestController
@RequestMapping("user/")
public class UserController {
/**
* Get 请求参数验证
* @param id
* 设置id 非必须, 以便 AbstractNamedValueMethodArgumentResolver 的 resolveArgument 方法对参数解析不报错
* 在使用 NotNull进行非空校验
* @return
*/
@GetMapping("getName")
public Object getName(@RequestParam(value = "id",required = false) @NotNull(message = "id 不能为空") @Min(value = 1) Integer id ){
return "1";
}
/**
* post
* @param reqDto
* @return
*/
@PostMapping("getName1")
public String getName1(@RequestBody @Valid UserReqDto reqDto){
return "1";
}
/**
* put
* @param reqDto
* @return
*/
@PutMapping("getName2")
public String getName2(@RequestBody @Valid UserReqDto reqDto){
return "1";
}
/**
* delete
* @param id
* @return
*/
@DeleteMapping("del/{id}")
public String del(@PathVariable("id") @Min(message = "id 不能小于1",value = 1) Integer id ){
return "1";
}
/** 分组
* group1
*/
@PostMapping("group1")
public String group1(@RequestBody @Validated(UserReqDto.ModifyAge.class) UserReqDto reqDto){
return "1";
}
/** 分组
* group2
*/
@PostMapping("group2")
public String group2(@RequestBody @Validated({UserReqDto.ModifyEmail.class}) UserReqDto reqDto){
return "1";
}
}
UserReqDto 请求参数接收类:
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.groups.Default;
public class UserReqDto {
//特殊用于修改名字 标记使用 灵活放置位置
/**
* 分组验证+ 基本验证
* 此处会验证ModifyAge 的分组 及验证 userId不能为空
*/
public interface ModifyAge extends Default {
}
public interface ModifyEmail {
}
@NotNull(message = "id dto 不能为空")
private Integer userId;
//自定义一个正则
@NotBlank(groups = {UserReqDto.ModifyAge.class},
message = "失败,请填写name"
)
private String name;
@NotBlank(groups = {UserReqDto.ModifyEmail.class},
message = "失败,请填写email"
)
private String email;
}
3 简单解释:
项目中我们使用了 spring 中的 @Validated 和 javax.validation 中的 @Valid ,那么它们之间的联系是什么;
3.1 @Validated 注解,可以用在类,方法,和参数级别:
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Validated {
Class<?>[] value() default {};
}
3.2 @Valid 注解,可以用在类,方法,和参数,类中的属性:
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Valid {
}
也就是说我们在使用@RequestBody 接收body 中的请求参数,参数类内部对于属性的校验可以使用 javax.validation 的方法:
这样我们就可以在controller 类中,对类使用@Validated 进行修饰,然后 对于get,delete 方法 ,通过url 获取参数的我们可以使用javax.validation 的验证方法完成验证,在对于post ,put 通过body 获取参数的可以对其使用 @Validated ,在参数接收类的内部使用javax.validation 的验证方法完成验证;
3.3 通过使用group 完成参数分组,解决不同场景的参数验证:
如在新增用户的时候可能不需要id ,但是在修改名字的时候需要使用id 来找到改用户,可以通过继承Default 使得没有分组的属性也进行校验(如本例中的ModifyAge 会验证userId 和name);
controller 中设置分组:
3.4 对于从url 中获取参数来说,我们会发现当使用@RequestParam 接收参数时,发起的http 请求会直接报错,那是因为spring 本身参数值获取逻辑抛出了异常:
AbstractNamedValueMethodArgumentResolver 类中的 resolveArgument 方法:
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
NamedValueInfo namedValueInfo = this.getNamedValueInfo(parameter);
MethodParameter nestedParameter = parameter.nestedIfOptional();
Object resolvedName = this.resolveEmbeddedValuesAndExpressions(namedValueInfo.name);
if (resolvedName == null) {
throw new IllegalArgumentException("Specified name must not resolve to null: [" + namedValueInfo.name + "]");
} else {
// 获取指定参数名称的值
Object arg = this.resolveName(resolvedName.toString(), nestedParameter, webRequest);
if (arg == null) {
// 如果对应参数的默认值不是空,那么就使用默认值
if (namedValueInfo.defaultValue != null) {
arg = this.resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
}
// 否则,查看这个参数的值是否是必须设置的,如果必须设置,但是没有对应的值,那么按错误处理
else if (namedValueInfo.required && !nestedParameter.isOptional()) {
this.handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
}
arg = this.handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
}
// 如果为空字符串,但是有设定的默认值则取默认值
else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
arg = this.resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
}
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, (Object)null, namedValueInfo.name);
try {
arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
} catch (ConversionNotSupportedException var11) {
throw new MethodArgumentConversionNotSupportedException(arg, var11.getRequiredType(), namedValueInfo.name, parameter, var11.getCause());
} catch (TypeMismatchException var12) {
throw new MethodArgumentTypeMismatchException(arg, var12.getRequiredType(), namedValueInfo.name, parameter, var12.getCause());
}
// 如果参数必填,且参数为空,则报错
if (arg == null && namedValueInfo.defaultValue == null && namedValueInfo.required && !nestedParameter.isOptional()) {
this.handleMissingValueAfterConversion(namedValueInfo.name, nestedParameter, webRequest);
}
}
// 获取参数
this.handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);
return arg;
}
}
当我们使用@RequestParam 接收参数时可以看到参数默认是必须存在的:
所以它在发现参数没有且没有默认值,并且是必填的,则在进行valide 验证矿建之前就抛出了异常;所以我们在使用@RequestParam 接收并进行 @NotNull 判断时 ,可以设置,required = false,通过spring 的参数获取,然后交由后面的验证框架进行 非空的验证;
这里我们可以看出,对于@RequestParam的使用而言,其值的解析会有这么几个分支处理:
- 如果这个参数对应的值存在,就直接返回。
- 如果这个参数对应的值不存在,那么先看这个参数是否设置了默认值,若有,则返回默认值。
- 如果默认值也没有,那么看这个参数是否是required(默认为true),以及该参数本身是否可选isOptional()。如果满足这俩条件,但是没有可选的值,就报错。
- 否则按照null处理。
4 经常使用的参数验证方法:
javax.validation.constraints包下提供的注解:
hibernate也扩展了很多注解,位于org.hibernate.validator.constraints包下:
5 增加异常的统一处理:
参考:SpringBoot工具篇–统一数据结构及返回(controller & exception)
6 参考:
6.1 spring-boot-starter-validation开启参数校验使用详解;
6.2 Spring常见问题解决 - @RequestParam和@PathVariable的区别以及400报错问题;
项目git 地址:https://codeup.aliyun.com/61cd21816112fe9819da8d9c/spring-validate.git