前言
对于开发人员来说,对参数进行校验是必不可少的工作之一,在设计和开发软件时,优雅的参数校验不仅是提升系统质量的关键,也是确保用户和开发人员能够长期顺利使用和维护系统的基础。
1.传统的参数校验方法
@GetMapping("/get")
private Object getIoDetailed(@RequestBody UserDto userDto) {
String username = userDto.getUsername();
if (username == null || userName == "") {
return Rets.failure("用户名为空");
}
String password= userDto.getPassword();
if (password == null || password.length() < 8) {
return Rets.failure("密码不能为空,且长度至少为8个字符");
}
// 业务逻辑
doSomething(username ,password);
return Rets.success();
}
存在的问题:
- 代码冗余:校验逻辑散落在 Controller 里,手动进行参数校验虽然可行,但不够简洁和标准化。
- 重复劳动:类似的校验逻辑可能会出现在多个接口里,导致代码重复度极高。
- 扩展性差:万一某天需要加新的校验规则,需要到处修改校验规则,不利于系统维护。
- 业务逻辑与校验逻辑混杂:参数校验逻辑与业务逻辑混在一起,不利于代码的分离和测试。
所以,这种手写参数校验的方式,在简单场景下勉强能用,但如果业务变复杂,问题会越来越多。
2.使用SpringBoot注解进行参数校验
在 SpringBoot 中,我们可以使用 Hibernate Validator(Bean Validation 的参考实现)来实现参数校验。
它的核心思路是:把校验逻辑从业务代码里抽离出来,用注解的方式声明校验规则。
@Data
public class UserDto {
@NotNull(message = "用户名不能为空")
@Size(min = 3, max = 20, message = "用户名长度必须在3到20之间")
private String username;
@NotNull(message = "密码不能为空")
@Size(min = 8, message = "密码长度至少为8个字符")
private String password;
@NotNull(message = "年龄不能为空")
@Min(value = 1, message = "年龄必须是正整数")
@Max(value = 120, message = "年龄不能超过120")
private Integer age;
@Email(message = "邮箱格式不正确")
private String email;
}
注解解释:
- @NotNull:字段不能为空
- @Size:限制字符串长度
- @Min 和 @Max:限制数值范围
- @Email:校验邮箱格式
补充:对于判断字段是否为空的注解,可用以下这几个注解:
@NotNull
- 作用:确保字段的值 不为 null。
- 适用类型:任何类型的对象,通常适用于引用类型字段(例如,String、Integer、Date等)。基本数据类型(如 int、long)永远不会是 null,因此它们不适用于 @NotNull。
- 限制:仅验证字段是否为 null,如果字段为空字符串 “”,空集合或空数组,这些情况是允许的。
@NotEmpty
- 作用:确保字段的值 不为 null 且不为空(对于 String 来说,不能为空字符串 “”,对于集合、数组等来说不能为空集合)。
- 适用类型:String、Collection、Map、Array等。
- 限制:如果字段是 null 或者空字符串(“”)、空集合、空数组,都会被认为不合法。
@NotBlank
- 作用:确保字段的值 不为 null 且不为空且不只包含空格。该注解通常用于 String 类型字段。
- 适用类型:只适用于 String 类型。
- 限制:如果字段是 null 或者为空字符串(“”),或者只包含空白字符(如空格、制表符等),都会被认为不合法。
注解 | 适用类型 | 验证条件 |
---|---|---|
@NotNull | 任何引用类型(String, Integer, Date 等) | 字段不能为 null |
@NotEmpty | String, Collection, Map, Array 等 | 字段不能为 null,且不能为空字符串、空集合或空数组 |
@NotBlank | String 类型 | 字段不能为 null,且不能为空,且不能只包含空格 |
所以在Controller层中就可以这样写:
@GetMapping("/get")
private Object getIoDetailed(@Valid @RequestBody UserDto userDto) {
String username = userDto.getUsername();
String password= userDto.password();
// 业务逻辑
doSomething(username ,password);
return Rets.success();
}
这里的 @Valid 注解,它的作用是告诉 Spring 对请求参数进行校验
3. 统一处理校验错误
但是如果前端传的参数不合法,Spring 会抛出一个 MethodArgumentNotValidException 异常。默认情况下,这个异常返回的信息不太友好,可能是这样的:
{
"timestamp": "2024-01-01T12:00:00.000+00:00",
"status": 400,
"error": "Bad Request",
"message": "Validation failed for object='userDto'. Error count: 2",
"path": "/api/users/get"
}
对此,可以实现一个全局异常处理器进行同一格式化错误信息
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationException(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error -> {
errors.put(error.getField(), error.getDefaultMessage());
});
return Rets.failure(errors);
}
}
现在,当参数校验失败时,返回的错误信息会变成这样:
{
"username": "用户名长度必须在3到20之间",
"password": "密码不能为空"
}
4. 自定义校验注解
定义注解:
@Documented
@Constraint(validatedBy = PhoneValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidPhone {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
实现校验逻辑:
public class PhoneValidator implements ConstraintValidator<ValidPhone, String> {
private static final String PHONE_REGEX = "^1[3-9]\\d{9}$";
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return value != null && value.matches(PHONE_REGEX);
}
}
在实体类中使用自定义注解
public class UserDto {
@ValidPhone(message = "手机号格式不正确,请重新输入")
private String phone;
}
5.总结
优雅的参数校验不仅能够改善用户体验,提供清晰、易懂的错误提示,还可以提高代码的可维护性,让校验逻辑集中、简洁,降低调试难度,同时提高系统的扩展性和可测试性,便于未来功能的扩展和单元测试。