前言
在之前的悦享校园的开发中使用了SSM框架,由于当时并没有使用参数参数校验工具,方法的入参判断使用了大量的if else语句,代码十分臃肿,因此最近在重构代码时,将框架改为SpringBoot后,引入了Hibernate Validator校验工具对参数进行优雅校验(SSM同样可用)。本文将通过实例来演示如何使用该框架。
环境配置
JDK 1.8
Spring Boot 2.7.12
导入依赖
<!--SpringBoot 2.3开始,校验包单独组件,需要手动引入 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
基础校验
在正式开始之前,需要先对以下的内容作以了解,方便后续的学习。
校验注解
Hibernate Validator,提供了丰富的校验注解来供我们使用,这里列举一些比较常用的:
注解 | 解释 |
@Null | 验证对象是否为空 |
@NotNull | 验证对象是否为非空 |
@NotBlank | 用于String对象,验证字符串不为空且trim()后长度大于0 |
@Length | 验证对象的长度是否符合 |
@Size | 验证对象长度是否在给定的范围之内(用于Array,Collection,Map,String) |
@Min | 入参对象的值不能小于该值(用于Number和String) |
@Max | 入参对象的值不能大于该值(用于Number和String) |
@Digits | 验证number和string的构成是否合法 |
@Past | 验证date和calendar对象是否在当前时间之前 |
@Future | 验证date和calendar对象是否在当前时间之后 |
@Pattern | 验证是否符合正则表达式规则 |
验证邮箱 | |
@Positive | 验证输入的对象是否非负数 |
@Range | 验证输入的对象的值是否在指定范围内 |
参数绑定
@PathVariable
该注解用于接收具有Restful风格的参数,如/api/v1/1001,最终userId的值为1001。
如下代码中,使用name属性可以指定GetMapping中的id名称与之对应,从而可以自定义参数名称userId,而不是使用默认名称id
@GetMapping("/v1/{id}")
public void getMsg(@PathVariable(name = "id") Integer userId){}
@RequestParam
该注解用于接收查询参数,如/api/v1/product?user="123",则user的值为123。该注解也可用于接收form-data类型的数据。
当在参数前使用@RequireParam时,当请求该方法时,对应的参数必须存在,否则会引发异常,可使用@RequireParam(required = false)指明该参数非必须,该注解在入参为null时可提供默认值。
@GetMapping("/v1/product")
public void getMsg(@RequestParam String user){}
@RequestBody
该注解用于接收JSON格式的数据,如请求为{"name":123,"age":18},需要有对应的实体类作为映射。
@PostMapping("/v1/user")
public void getMsg(@RequestBody User user){}
@Validated
该注解可以作用于类、方法、参数上,用于开启参数校验。
@Valid
该注解可以作用于方法、参数、字段、构造器上,同样可以用于参数校验。
单参校验
将@Validated注解用于类上,可以不用在每个方法的参数上重复开启校验,使用前面提的基础校验即可对参数作约束。
校验方法
@RestController
@RequestMapping("/v1/hello")
@Slf4j
@Validated
public class HelloController {
/**
* GetMapping 请求
* @param name 用户名称
* @param uid 用户uid
* @return
*/
@GetMapping("/{id}")
public ResultDataVO getMsg(@RequestParam String name,
@Max(value = 10,message = "最大值不能超过10")
@PathVariable(name = "id") int uid) {
log.info("访问hello下的msg方法。");
String result = "Hello,"+name+" id "+uid;
return ResultDataVO.success(result);
}
}
请求参数 响应结果
由于我们没有给String参数赋值,因此默认为空字符串。但由于在uid上使用了@Max注解,因此当入参为100时便会出现异常,而返回的JSON格式为全局的响应处理,在后续的文章会提到。
对象校验
当我们在使用中,往往会遇到多个参数的情况,由于get请求对参数长度有限制,且参数会拼接在URL中, 因此对于此场景我们一般使用对象的方式来接收参数,而对于我们的参数也将传入也将用JSON格式。
由于使用JSON格式传参,将使用@RequestBody注解,除此之外还要使用@Validated或@Valid注解,关于两者的选择,只需要记住分组校验使用@Validated,否则二者均可。
即使在类上添加了@Validated注解,此处仍然需要以上两个注解之一,否则参数校验将无效。
校验方法
@RestController
@RequestMapping("/v1/hello")
@Slf4j
@Validated
public class HelloController {
/**
* 对象校验示例
* @param user
* @return
*/
@PostMapping("/msg")
public ResultDataVO(@RequestBody @Valid UserDTO user){
return ResultDataVO.success(user.toString());
}
}
UserDTO
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserDTO {
/**
* id
*/
@Min(value = 1,message = "id值不合法")
@Positive
private Integer id;
/**
* 昵称
*/
@NotBlank(message = "用户名不能为空")
@Length(min = 4,max=10,message = "用户名必须在4-10个字符之间")
private String username;
/**
* 年龄
*/
@Range(min = 1,max=120,message = "年龄不在合法范围内")
@NotNull(message = "age不能为空")
private Integer age;
}
上述代码中,参数为UserDTO对象,其中包含三个待校验的字段,由于未涉及到分组校验,因此校验方法中参数前使用@Valid注解(@Validated注解亦可),注意入参的字段名称需要和UserDTO中的名称一致。
请求参数
响应结果
分组校验
假设有这样的场景,在用户登录和修改密码中,两者分别需要如下参数,登录时需要用户名、密码,而修改时需要密码、新密码,为了方便管理,只使用了一个DTO类来存放上述的字段,此时由于这些参数需要在不同的场景下校验,而加上基础校验的注解会导致所有参数均被校验,面对此问题,我们将使用分组校验来解决。
可以看到如何代码中,均是使用了@Validate注解,且注解中含有一个分组接口,使用该接口可以帮助我们告知程序该对那些参数进行校验。
校验方法
@RestController
@Slf4j
@RequestMapping("/local/auth")
@Validated
public class LocalAuthController {
@Autowired
private LocalAuthService localAuthService;
/**
* 用户登录接口
* @param localAuthDTO 账号对象
* @return
*/
@PostMapping("/login")
LocalAuthVO loginCheck(@RequestBody @Validated(AuthCheckSequence.class) LocalAuthDTO localAuthDTO){
return localAuthService.userLoginCheck(localAuthDTO);
}
/**
* 用户修改密码接口
* @param localAuthDTO 账号对象
* @return
*/
@PutMapping("/password")
ResultDataVO userChangePassword(@RequestBody
@Validated(AuthChangeSequence.class)
LocalAuthDTO localAuthDTO){
return localAuthService.userModifyPassword(localAuthDTO);
}
}
分组接口
AuthChangeSequence
/**
* 分组校验,用户登录
*/
public interface AuthCheckSequence {
}
AuthCheckSequence
/**
* 分组校验,密码修改
*/
public interface AuthChangeSequence {
}
LocalAuthDTO
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class LocalAuthDTO {
/**
* 用户名
*/
@NotBlank(message = "用户名不能为空",groups = AuthCheckSequence.class)
@Pattern(regexp = "^[a-zA-Z0-9]{3,8}$", message = "用户名必须由3-8个字母或数字组成",
groups = AuthCheckSequence.UsernameCheck.class)
private String username;
/**
* 密码
*/
@NotBlank(message = "密码不能为空",groups = AuthCheckSequence.class)
@Size(min = 6,max = 18, message = "密码长度必须在6到18位之间",groups = AuthCheckSequence.PasswordCheck.class)
private String password;
/**
* 新密码,修改密码时使用
*/
@NotBlank(message = "新密码不能为空",groups = AuthChangeSequence.class)
@Size(min = 6,max = 18, message = "新密码长度必须在6到18位之间",groups = AuthChangeSequence.ChangePassword.class)
private String newPassword;
}
请求参数
响应结果
从上图所示请求结果可以看出,使用@Validated注解+分组校验接口后,将只对该参数中标明了对应分组接口的字段进行校验,并不会对全部参数进行校验。
顺序校验
由于用户人数增多,我们将在对用户进行分组,将在LocalAuthDTO中引入新的字段,而我们所期望的是能够按照用户类型,用户名,密码的顺序来校验,而不是像之前对象校验最终显示的结果那样,随机进行校验并返回结果,因此需要使用在分组校验的基础上做一些改进,使其可以按照我们所指定的顺序执行校验,以前面登录为例:
校验方法
@RestController
@Slf4j
@RequestMapping("/local/auth")
@Validated
public class LocalAuthController {
@Autowired
private LocalAuthService localAuthService;
/**
* 用户登录接口
* @param localAuthDTO 账号对象
* @return
*/
@PostMapping("/login")
LocalAuthVO loginCheck(@RequestBody @Validated(AuthCheckSequence.class) LocalAuthDTO localAuthDTO){
return localAuthService.userLoginCheck(localAuthDTO);
}
}
AuthCheckSequence
通过在该分组接口中新建接口,并使用@GroupSequence注解中参数的顺序来实现校验参数的顺序。如下将标识使用AuthCheckSequence分组的参数将按照用户类型、用户名、密码的顺序进行校验。
@GroupSequence({AuthCheckSequence.UserTypeCheck.class, AuthCheckSequence.UsernameCheck.class, AuthCheckSequence.PasswordCheck.class})
public interface AuthCheckSequence {
/**
* 用户类型
*/
interface UserTypeCheck {};
/**
* 用户名校验
*/
interface UsernameCheck {};
/**
* 密码校验
*/
interface PasswordCheck {};
}
LocalAuthDTO
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class LocalAuthDTO {
/**
* 用户类型 1.用户 2.商家 3.管理员,目前仅支持用户和商家类型注册
*/
@NotNull(message = "用户类型不能为空",groups = AuthCheckSequence.UserTypeCheck.class)
@Range(min = 1,max = 3,message = "用户类型不合法",groups =
AuthCheckSequence.UserTypeCheck.class)
private Integer userType;
/**
* 用户名
*/
@NotBlank(message = "用户名不能为空",groups = AuthCheckSequence.UsernameCheck.class)
@Pattern(regexp = "^[a-zA-Z0-9]{3,8}$", message = "用户名必须由3-8个字母或数字组成",
groups = AuthCheckSequence.UsernameCheck.class)
private String username;
/**
* 密码
*/
@NotBlank(message = "密码不能为空",groups = AuthCheckSequence.PasswordCheck.class)
@Size(min = 6,max = 18, message = "密码长度必须在6到18位之间",groups = AuthCheckSequence.PasswordCheck.class)
private String password;
}
请求参数
响应结果