别再写重复的校验代码了!SpringBoot3.0分组校验实战,一个实体搞定增删改查

发布时间:2026/5/22 20:03:48

别再写重复的校验代码了!SpringBoot3.0分组校验实战,一个实体搞定增删改查 SpringBoot3.0分组校验实战用优雅设计终结CRUD重复劳动每次新增一个用户管理接口就要重写一遍字段校验逻辑在update方法里发现id为空时才想起忘记加NotNull注解如果你正在经历这种校验代码地狱今天这个方案能让你彻底解脱。我们不再讨论基础校验注解怎么用而是直击开发者的真实痛点——如何让同一个实体在不同接口中智能适配差异化校验规则。1. 为什么我们需要分组校验上周排查一个线上Bug时我发现用户更新接口竟然接受了空用户名——因为在User实体里username字段只标注了NotEmpty(groups CreateGroup.class)。这个低级错误导致需要紧急发版修复而根源在于团队对分组校验的理解还停留在知道但不会用的阶段。1.1 传统校验的三大痛点先看这段典型代码public class UserDTO { NotNull(message 创建时ID必须为空) private Long id; // 创建时应该为空更新时必填 NotEmpty private String username; Size(min 6, max 20) private String password; // 创建时必填更新时可选 }问题1自相矛盾的注解id字段的NotNull与业务需求直接冲突——创建用户时ID应该由系统生成更新时却必须存在。这种注解要么导致错误提示不准确要么需要写额外的校验逻辑。问题2过度创建DTO类常见的workaround是为每个接口创建专属DTOUserCreateDTO.java UserUpdateDTO.java UserQueryDTO.java这虽然解决了问题但带来了维护成本当用户表新增avatar字段时需要在5个DTO类中重复添加相同注解。问题3全局校验的局限性即使使用Valid进行校验也无法处理这样的场景密码字段创建时必填6-20位更新时可选但一旦填写就必须符合规则手机号创建时需要验证格式更新时如果修改需要二次验证1.2 分组校验的降维打击SpringBoot3.0基于JSR-380规范的分组校验通过校验规则与业务场景解耦可以这样重构上述逻辑// 定义业务场景标记接口 public interface CreateValidation {} public interface UpdateValidation {} public class User { Null(groups CreateValidation.class) NotNull(groups UpdateValidation.class) private Long id; NotEmpty(groups {CreateValidation.class, UpdateValidation.class}) private String username; Size(min 6, max 20, groups CreateValidation.class) Size(min 6, max 20, groups UpdateValidation.class) private String password; }在Controller层的应用对比// 传统方式 PostMapping(/users) public User create(Valid RequestBody UserCreateDTO dto) {...} // 分组校验方式 PostMapping(/users) public User create(Validated(CreateValidation.class) RequestBody User user) {...}关键优势单实体维护所有校验规则不同接口激活不同规则集字段级精确控制校验场景2. 分组校验的进阶玩法理解了基础用法后我们来看几个实际项目中更复杂的应用场景。这些技巧能帮你处理90%以上的业务校验需求。2.1 组合分组与继承策略当多个接口需要共享部分校验规则时可以定义分组继承关系public interface BasicInfoValidation {} public interface CreateValidation extends BasicInfoValidation {} public interface UpdateValidation extends BasicInfoValidation {} public class Product { NotBlank(groups BasicInfoValidation.class) private String name; Positive(groups CreateValidation.class) private BigDecimal price; URL(groups UpdateValidation.class) private String detailUrl; }这样设计后CreateValidation会自动包含BasicInfoValidation的校验规则更新接口可以只校验detailUrl而不重复校验name提示继承链不宜过深建议不超过3层否则会降低代码可读性2.2 动态分组选择有些场景需要根据请求参数动态决定校验分组。通过实现ValidationGroupSequence可以实现条件校验public interface DraftValidation {} public interface PublishValidation {} public class Article { Size(max 1000, groups DraftValidation.class) Size(min 1500, groups PublishValidation.class) private String content; } // 在Controller中动态选择分组 PostMapping(/articles) public void submit( RequestParam boolean publish, Validated({ publish ? PublishValidation.class : DraftValidation.class }) RequestBody Article article) { // ... }2.3 分组校验与自定义注解结合对于复杂业务规则可以组合使用分组校验和自定义校验注解。比如实现更新时如果填写新密码则必须包含特殊字符Target({FIELD}) Retention(RUNTIME) Constraint(validatedBy PasswordUpdateValidator.class) public interface ValidUpdatePassword { Class?[] groups() default {}; String message() default 密码格式错误; } public class User { ValidUpdatePassword(groups UpdateValidation.class) private String password; }对应的校验器实现public class PasswordUpdateValidator implements ConstraintValidatorValidUpdatePassword, String { Override public boolean isValid(String value, ConstraintValidatorContext context) { // 值为null或空时不校验更新时可选 if(value null || value.isEmpty()) return true; // 非空时校验包含特殊字符 return value.matches(.*[!#$%^*].*); } }3. 实战用户中心完整校验方案让我们用一个完整的用户管理模块示例展示如何系统性地应用分组校验。假设需求如下注册需要手机号、密码、验证码登录需要手机号、密码信息更新需要用户ID可选更新各项资料密码修改需要旧密码验证3.1 实体类设计public interface RegisterGroup {} public interface LoginGroup {} public interface ProfileUpdateGroup {} public interface PasswordChangeGroup {} public class User { Null(groups RegisterGroup.class) NotNull(groups {LoginGroup.class, ProfileUpdateGroup.class, PasswordChangeGroup.class}) private Long userId; NotBlank(groups RegisterGroup.class) Pattern(regexp ^1[3-9]\\d{9}$, groups RegisterGroup.class) private String mobile; NotBlank(groups {RegisterGroup.class, LoginGroup.class}) Size(min 8, max 20, groups RegisterGroup.class) private String password; NotBlank(groups RegisterGroup.class) private String smsCode; NotBlank(groups PasswordChangeGroup.class) private String oldPassword; Size(min 2, max 10, groups ProfileUpdateGroup.class) private String nickname; }3.2 控制器实现RestController RequestMapping(/users) public class UserController { PostMapping(/register) public ResponseEntity register( Validated(RegisterGroup.class) RequestBody User user) { // 注册逻辑 } PostMapping(/login) public ResponseEntity login( Validated(LoginGroup.class) RequestBody User user) { // 登录逻辑 } PutMapping(/profile) public ResponseEntity updateProfile( Validated(ProfileUpdateGroup.class) RequestBody User user) { // 资料更新 } PutMapping(/password) public ResponseEntity changePassword( Validated(PasswordChangeGroup.class) RequestBody User user) { // 密码修改 } }3.3 校验异常处理统一处理校验异常返回友好错误信息RestControllerAdvice public class GlobalExceptionHandler { ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity handleValidationException( MethodArgumentNotValidException ex) { MapString, String errors new HashMap(); ex.getBindingResult().getAllErrors().forEach(error - { String field ((FieldError) error).getField(); String message error.getDefaultMessage(); errors.put(field, message); }); return ResponseEntity.badRequest() .body(ApiResponse.error(参数校验失败, errors)); } }当注册时手机号格式错误时将返回{ code: 400, message: 参数校验失败, data: { mobile: 必须匹配正则表达式^1[3-9]\\d{9}$ } }4. 性能优化与最佳实践虽然分组校验功能强大但不合理的使用会导致性能下降或代码难以维护。以下是我们在千万级用户系统中总结的经验。4.1 校验组的设计原则推荐做法按业务场景划分Create/Update/StatusChange按操作类型划分Read/Write/Admin保持分组正交性避免交叉依赖反模式// 错误示范过度细分 public interface UsernameValidation {} public interface PasswordValidation {} public interface EmailValidation {} // 会导致注解变成这样 NotBlank(groups {CreateGroup.class, UsernameValidation.class}) private String username;4.2 缓存校验元数据Spring默认会缓存校验元数据但自定义ConstraintValidator需要注意实现initialize方法public class CustomValidator implements ConstraintValidatorCustomAnnotation, String { private Pattern pattern; Override public void initialize(CustomAnnotation constraintAnnotation) { // 预编译正则表达式避免每次校验都编译 this.pattern Pattern.compile([A-Z]); } Override public boolean isValid(String value, ConstraintValidatorContext context) { if(value null) return true; return pattern.matcher(value).matches(); } }4.3 与Spring Validation的整合技巧当需要同时在Service层使用校验时可以这样配置Configuration public class ValidationConfig { Bean public MethodValidationPostProcessor methodValidationPostProcessor() { MethodValidationPostProcessor processor new MethodValidationPostProcessor(); processor.setValidator(validator()); return processor; } Bean public LocalValidatorFactoryBean validator() { LocalValidatorFactoryBean bean new LocalValidatorFactoryBean(); bean.setValidationMessageSource(messageSource()); return bean; } Bean public MessageSource messageSource() { ResourceBundleMessageSource source new ResourceBundleMessageSource(); source.setBasename(messages/validation); return source; } }然后在Service方法上使用Service Validated // 启用方法级校验 public class UserService { public void updateProfile( Validated(ProfileUpdateGroup.class) User user) { // 业务逻辑 } }4.4 校验规则的外部化配置对于可能需要动态调整的规则如密码强度要求可以结合Value实现public class PasswordPolicy { Value(${security.password.min-length:8}) private int minLength; Value(${security.password.max-length:20}) private int maxLength; } public class User { Size( min #{passwordPolicy.minLength}, max #{passwordPolicy.maxLength}, groups CreateGroup.class ) private String password; }

相关新闻