前言
参数校验是后端请求的第一道防线,不符合条件的请求,越在前面拦截掉,消耗的资源越少。
对参数进行校验,我们可能会出现如下类似代码:
@RestController
@RequestMapping("/user")
public class UserController extends BaseController {
@PostMapping("/add")
public ApiResult addUser(@RequestBody User user) {
if (user == null) {
return ApiResult.fail("对象不能为空");
}
if (StringUtils.isEmpty(user.getAccount()) || StringUtils.isEmpty(user.getPassword()) || StringUtils.isEmpty(user.getEmail())) {
return ApiResult.fail("账号、密码或邮箱不能为空");
}
if (user.getAccount().length() < 6 || user.getAccount().length() > 11) {
return ApiResult.fail("账号长度必须是6-11个字符");
}
if (user.getPassword().length() < 6 || user.getPassword().length() > 16) {
return ApiResult.fail("密码长度必须是6-16个字符");
}
if (!Pattern.matches("^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$", user.getEmail())) {
return ApiResult.fail("邮箱格式不正确");
}
// 新增用户操作
return ApiResult.success();
}
}
这样实现并没有什么错,但是看起来实在不够优雅。
下面使用Spring Validator对该代码进行优化。
具体实现
Validator + BindResult
首先在对象上通过注解的方式定义校验规则,并指定校验失败后的信息,如下:
@Getter
@Setter
public class User {
@NotNull(message = "用户id不能为空")
private Long id;
@NotNull(message = "用户账号不能为空")
@Size(min = 6, max = 11, message = "账号长度必须是6-11个字符")
private String account;
@NotNull(message = "用户密码不能为空")
@Size(min = 6, max = 11, message = "密码长度必须是6-16个字符")
private String password;
@NotNull(message = "用户邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
}
校验规则定义完后,在接口上添加@Vaild注解和BindResult参数即可完成校验,如下:
@RestController
@RequestMapping("/user")
public class UserController extends BaseController {
@PostMapping("/add")
public ApiResult addUser(@RequestBody @Valid User user, BindingResult bindingResult) {
// 参数校验失败,会将错误信息封装成在BindingResult
for (ObjectError error : bindingResult.getAllErrors()) {
return ApiResult.fail(error.getDefaultMessage());
}
// 新增用户操作
return ApiResult.success();
}
}
当我们在访问接口时,未填写用户账户,则会返回如下结果:
{
"code": 500,
"data": null,
"message": "用户账号不能为空"
}
使用该方式已经是非常方便的进行参数校验方式了。 但是不难发现,当有多个接口需要进行参数验证时,就需要在每个接口中添加参数BindingResult。 重复写如下代码:
// 参数校验失败,会将错误信息封装成在BindingResult
for (ObjectError error : bindingResult.getAllErrors()) {
return ApiResult.fail(error.getDefaultMessage());
}
程序员可不是只会Ctrl+C和Ctrl+V的,我们可以通过异常统一处理来解决这个问题。
Validator + 异常统一处理
如果在接口中不添加参数BindingResult,校验失败则会抛出MethodArgumentNotValidException
异常,在异常中包含校验失败的信息。
所以只要统一处理MethodArgumentNotValidException异常即可。
在【异常统一处理的三种方式】这一篇文章中, 介绍了三种异常统一处理的方式,这里使用@ControllerAdvice + @ExceptionHandler的方式
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* MethodArgumentNotValidException 异常处理
*/
@ResponseBody
@ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResult methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
for (ObjectError error : bindingResult.getAllErrors()) {
// 返回检验信息
return ApiResult.fail(error.getDefaultMessage());
}
return ApiResult.fail("服务异常,请稍后重试");
}
}
在对异常进行统一处理后,在接口中只需添加注解@Vaild即可,相当的简洁。
@RestController
@RequestMapping("/user")
public class UserController extends BaseController {
@PostMapping("/add")
public ApiResult addUser(@RequestBody @Valid User user) {
// 新增用户操作
return ApiResult.success();
}
}
至此,已经能很优雅的使用参数校验。
但是别以为这样就万事大吉了。虽然默认提供的注解能够校验绝大部分的情况,但是还有一些特殊的情况,比如校验用户手机号,这没有现成的Validator。
这时,就需要自定义Validator。
自定义Validator
需要自定义注解和实现校验逻辑。
这里定义注解@Phone,来校验手机号格式:
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {PhoneValidator.class})
public @interface Phone {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
@Constraint(validatedBy = {PhoneValidator.class}) 用来指定校验类为PhoneValidator。
public class PhoneValidator implements ConstraintValidator<Phone, String> {
@Override
public boolean isValid(String phone, ConstraintValidatorContext context) {
// 校验手机格式
return Pattern.matches("^1[3-9]\\d{9}", phone);
}
}
这样就能够通过@Phone来校验手机号格式。
public class User {
@NotNull(message = "用户手机号不能为空")
@Phone(message = "手机号格式不正确")
private String phone;
}
总结
通过Validator和异常统一处理,很优雅的实现了参数校验。并通过自定义Validator,可以实现各种复杂的校验。
感谢阅读,如果感觉有帮助的话,不妨随手点个赞!