环境
- jdk 1.8
- spring boot 2.1.3
引言
开发项目时,很令人头疼的问题就是参数的验证问题,通常的MVC架构下的三层:
- controller
- service
- dao
到底应该在哪一层进行参数的验证,这是个问题;到底应该怎样验证,是使用传统的if-else这样的面条代码来进行验证还是使用其他方式来进行验证这也是问题。所以,今天我们就来聊聊如何更简洁优雅的处理参数验证。
谈参数校验分层问题
通常来说,我们的参数校验是分层的。
也就是说,我们会分别在controller与service层进行参数的校验,那么这两层的参数校验有何不同,如何区分呢?
其实如果开发项目的经验足够的话,我们可以很明显的感觉到参数的校验简单地分为两种:
- 验证基本参数合法性
对于第一种,常见的比如验证参数是否为null啦,验证字符串是否是空串啦,验证数值是否在指定范围啦,验证字符串长度等等。这一类逻辑比较简单的验证我们会在Controller这一层来完成。 - 验证业务参数合法性
而验证业务参数合法性,我们通常会在Service层,这也很合理。因为通常Service层的验证逻辑比较复杂,比如,必须这个用户先做某一操作后才能执行下一个操作;先干了这些事才能干那些事等等…
参数基本合法性
因为Controller层的校验逻辑比较简单,所以尽量交给代码在自动的完成。我们使用 spring-boot 的 validation来做Controller层的参数校验。下面是maven依赖:
maven 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
网上有很多童鞋会直接引入hibernate-validator进行数据校验,其实spring-boot-starter-web已经为我们引入了hibernate-validator,所以我们无需手动引入这个依赖。
还有很多童鞋会直接使用hibernate-validator的注解来进行数据校验,但是我们如果看源代码,比如hibernate-validator的 @NotBlank 这个注解的源代码就会发现这样一句话
@deprecated use the standard {@link javax.validation.constraints.NotBlank} constraint instead
没错,hibernate-validator的注解已经过期了,因为现在java已经自带了数据校验注解,比如 @NotBlank ,@NotEmpty,@Null等等。这些注解都是JSR303的规范注解。没错,这个注解是定义,而校验的工作还是得交给数据校验的实现,如hibernate-validator。所以以后不管引入什么样的数据校验实现,我们只需要使用同一套的校验注解即可。
验证简单的参数类型
那么对于controller的验证,如果请求处理方法的参数是如Integer、String等等这类比较简单的类型,我们直接在方法的参数上加上对应的注解即可,看下面这个例子
@RestController
@RequestMapping("/system/depart")
public class SystemDepartController {
@Autowired
private SysDepartService sysDepartService;
// Controller 加上@Validated注解后,@NotBlank才会生效
@GetMapping("/{id}")
public Result<?> getById(@PathVariable("id") @NotBlank(message="部门id不能为空") String id){
SysDepart depart = sysDepartService.getById(id);
if (depart == null){
throw new NotFoundException(ExceptionEnum.DEPART_NOT_FOUNT);
}
return ResultUtil.ok(depart);
}
}
在上面的代码中 @NotBlank 中有一个 message 的字段,这个message的值就是当参数验证不通过时的报错信息,其他的校验注解的使用方式也基本一致。
验证复杂的参数类型
而如果我们要验证的是自定义的类,比如什么entity类、model类等等,那么这些校验注解就没法满足我们的需求了。此时我们就需要更加强大的校验功能来帮助我们实现更复杂的校验需求。
比如,我们有一个User的实体类,我们要在后端验证用户名和密码不为空那么我们就可以这样写:
public class User{
private String id;
@NotBlank(message = "用户名不能为空", groups = {Group.class})
private String username;
@NotBlank(message = "用户名不能为空", groups = {Group.class})
private String password;
...
//getter和setter就省略了
}
当然,也可以对一个字段进行多种校验,就像下面这样:
public class User{
private String id;
@Email(message="用户名必须是邮箱")
@NotBlank(message = "用户名不能为空", groups = MyGroup.class)
private String username;
@Pattern(message="密码必须符合规定格式", regexp = "正则表达式...")
@NotBlank(message = "用户名不能为空", groups = MyGroup.class)
private String password;
...
//getter和setter就省略了
}
大家注意到这次在校验注解中多了一个字段groups,他的值为MyGroup.class,这又是哪里来的呢?且慢,我们先看看这次的Controller层的代码是怎么写的:
@RequestMapping("/user")
@RestController
public class UserController{
@PostMapping
public String save(@Validated(MyGroup.class)User user){
...
}
}
我们看到在save这个请求处理方法上有一个User类型的参数,而且还在前面加了 @Validated 注解,这到底是干啥玩意儿的?
那么具体过程是这样的:
- 当客户端发来一个 /user 的POST类型的ajax请求,我们后端开始就开始找对应的方法save进行处理
- 在Spring MVC把JSON参数转换为User类型对象后,一看,唉?user变量前面居然有 @Validated注解,值为 MyGroup.class
- 然后他就开始在User类里找了,这么一找,发现User类的两个字段上都有 校验注解的groups字段的值是 MyGroup.class , 分别是username字段的@NotBlank与password字段的@NotBlank
- 最后,validater就会对这两个字段进行NotBlank校验。
至于那个 MyGroup.class ,那个就是个自定义的类,随便,你甚至可以把
java.lang.Object类设置为校验注解的groups字段的值,就像这样
@NotBlank(message="这个操作可不是一般人能做出来的" groups=Object.class)
多场景复杂类型验证
当然有时候,会有这样的需求:
执行保存操作的时候不需要验证 id这个字段
而在执行修改操作的时候却需要验证id这个字段是否不为空,
这种情况下,我们该如何进行参数的验证呢?
其实很简单,还是使用校验注解的groups字段,看名字就知道这个字段是个数组,也就是说,一个校验注解可以在添加多个group,那么这里的group我们就可以理解为业务场景。具体如何使用呢?
- 我先自定义了两个接口(普通类也可以),分别为SaveGroup和UpdateGroup,随之对应着两个校验场景:保存校验与更新校验。
- 然后写了一个这样的User类
public class User{
@NotBlank(message="id不能为空", groups = UpdateGroup.class)
private String id;
@Email(message="用户名必须是邮箱")
@NotBlank(message = "用户名不能为空", groups = {SaveGroup.class, UpdateGroup.class})
private String username;
@Pattern(message="密码必须符合规定格式", regexp = "正则表达式...")
@NotBlank(message = "用户名不能为空", groups = {SaveGroup.class, UpdateGroup.class})
private String password;
...
//getter和setter就省略了
}
- 接着实现了Controller层的代码
@RequestMapping("/user")
@RestController
public class UserController{
@PostMapping
public boolean save(@Validated(SaveGroup.class)User user){
...
}
@PostMapping
public boolean update(@Validated(UpdateGroup.class)User user){
...
}
}
这样,在执行save方法的时候,User类中所有带有SaveGroup.class的注解的将会被执行校验,而在执行update方法的时候,User类中所有加UpdateGroup.class的注解将会被执行校验,这样就成功的实现了在多场景下分别进行参数校验的效果。
验证参数业务合法性
待续…