(七)从零搭建后端框架——优雅地进行参数校验

1,737 阅读4分钟

前言

参数校验是后端请求的第一道防线,不符合条件的请求,越在前面拦截掉,消耗的资源越少。

对参数进行校验,我们可能会出现如下类似代码:

@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,可以实现各种复杂的校验。

感谢阅读,如果感觉有帮助的话,不妨随手点个赞!

源码

github.com/zhuqianchan…

往期回顾