告别满屏if-else!Spring Boot项目里用javax.validation注解优雅校验参数(附完整异常处理)
告别满屏if-else!Spring Boot参数校验的优雅实践
在Spring Boot项目中,参数校验是每个开发者都无法绕开的环节。你是否还在Controller中写满了一行又一行的if-else来判断参数是否为空、格式是否正确?这种传统方式不仅让代码变得臃肿,还难以维护。本文将带你用javax.validation注解彻底告别这种原始方式,实现优雅的参数校验。
1. 为什么需要注解式参数校验
想象一下这样的场景:一个用户注册接口需要验证用户名、密码、邮箱、手机号等多个字段。如果采用传统方式,你可能会写出这样的代码:
@PostMapping("/register")
public ResponseDTO register(@RequestBody UserDTO user) {
if (user.getUsername() == null || user.getUsername().isEmpty()) {
return ResponseDTO.fail("用户名不能为空");
}
if (user.getPassword() == null || user.getPassword().length() < 6) {
return ResponseDTO.fail("密码长度不能小于6位");
}
// 更多if判断...
}
这种方式的弊端显而易见:
- 代码冗余 :每个接口都需要重复编写大量校验逻辑
- 可读性差 :业务逻辑被淹没在参数校验中
- 维护困难 :校验规则变更时需要修改多处代码
而使用javax.validation注解,同样的功能可以简化为:
@Data
public class UserDTO {
@NotBlank(message = "用户名不能为空")
private String username;
@Size(min = 6, message = "密码长度不能小于6位")
private String password;
@Email(message = "邮箱格式不正确")
private String email;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String mobile;
}
@PostMapping("/register")
public ResponseDTO register(@RequestBody @Valid UserDTO user) {
// 直接处理业务逻辑
}
2. 核心注解详解
javax.validation提供了一系列强大的校验注解,下面介绍最常用的几个:
2.1 基础校验注解
@NotNull:验证对象不为null@NotEmpty:验证字符串、集合、Map或数组不为null且不为空@NotBlank:验证字符串不为null且trim后长度大于0@Size:验证字符串、集合、Map或数组长度在指定范围内@Min/@Max:验证数字是否大于/小于指定值@DecimalMin/@DecimalMax:验证BigDecimal是否大于/小于指定值@Digits:验证数字的整数位和小数位是否符合要求@Pattern:验证字符串是否符合正则表达式@Email:验证字符串是否为有效邮箱格式@Future/@Past:验证日期是否在未来/过去
2.2 分组校验
在实际开发中,同一个DTO可能在不同场景下需要不同的校验规则。比如用户对象在创建时不需要ID,但在更新时需要。这时可以使用分组校验:
public interface CreateGroup {}
public interface UpdateGroup {}
@Data
public class UserDTO {
@Null(groups = CreateGroup.class, message = "创建时不需要ID")
@NotNull(groups = UpdateGroup.class, message = "更新时需要ID")
private Long id;
@NotBlank(message = "用户名不能为空")
private String username;
}
@PostMapping("/users")
public ResponseDTO createUser(@RequestBody @Validated(CreateGroup.class) UserDTO user) {
// 创建用户逻辑
}
@PutMapping("/users/{id}")
public ResponseDTO updateUser(@PathVariable Long id,
@RequestBody @Validated(UpdateGroup.class) UserDTO user) {
// 更新用户逻辑
}
2.3 级联校验
当DTO中包含其他对象时,可以使用 @Valid 实现级联校验:
@Data
public class OrderDTO {
@NotNull
private Long id;
@Valid
@NotNull
private UserDTO user;
@Valid
@NotEmpty
private List<@Valid OrderItemDTO> items;
}
3. 全局异常处理
参数校验失败时会抛出 MethodArgumentNotValidException 或 ConstraintViolationException ,我们需要统一处理这些异常,返回友好的错误信息。
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseDTO handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
List<String> errors = e.getBindingResult()
.getFieldErrors()
.stream()
.map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage())
.collect(Collectors.toList());
return ResponseDTO.fail(400, "参数校验失败", errors);
}
@ExceptionHandler(ConstraintViolationException.class)
public ResponseDTO handleConstraintViolationException(ConstraintViolationException e) {
List<String> errors = e.getConstraintViolations()
.stream()
.map(violation -> violation.getPropertyPath() + ": " + violation.getMessage())
.collect(Collectors.toList());
return ResponseDTO.fail(400, "参数校验失败", errors);
}
}
4. 高级技巧
4.1 自定义校验注解
当内置注解无法满足需求时,可以自定义校验注解。例如,我们需要验证字符串是否为有效的手机号:
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MobileValidator.class)
public @interface Mobile {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class MobileValidator implements ConstraintValidator<Mobile, String> {
private static final Pattern MOBILE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) {
return true; // 结合@NotNull使用
}
return MOBILE_PATTERN.matcher(value).matches();
}
}
// 使用方式
@Data
public class UserDTO {
@Mobile
private String mobile;
}
4.2 校验顺序控制
默认情况下,校验是无序的。如果需要按特定顺序校验,可以使用 @GroupSequence :
@GroupSequence({FirstCheck.class, SecondCheck.class, UserDTO.class})
public interface OrderedChecks {}
@Data
public class UserDTO {
@NotBlank(groups = FirstCheck.class)
private String username;
@Size(min = 6, groups = SecondCheck.class)
private String password;
}
@PostMapping("/users")
public ResponseDTO createUser(@RequestBody @Validated(OrderedChecks.class) UserDTO user) {
// 业务逻辑
}
4.3 动态错误消息
错误消息可以动态地从属性文件或数据库加载:
@NotBlank(message = "{user.name.notblank}")
private String username;
然后在 messages.properties 中定义:
user.name.notblank=用户名不能为空
5. 性能优化建议
虽然参数校验很方便,但在高并发场景下也需要注意性能:
- 避免过度校验 :只校验必要的字段
- 合理使用分组 :不同接口只校验相关字段
- 缓存校验器 :Spring默认会缓存校验器实例
- 异步校验 :对于耗时校验可以考虑异步处理
在实际项目中,我通常会根据业务场景将参数校验分为两类:
- 基础校验 :使用注解在Controller层快速拦截明显错误
- 业务校验 :在Service层进行更复杂的业务规则校验
这种分层校验的方式既保证了接口的安全性,又避免了将过多业务逻辑放入校验注解中。
更多推荐
所有评论(0)