Spring Boot 1.5中PATCH请求仅校验非空DTO字段的最优方案
处理Spring Boot 1.5中PATCH请求的部分字段校验问题
针对你在Spring Boot 1.5里用PATCH请求更新DTO、仅校验非null字段的需求,我推荐以下几种递进的处理方式,按实用性排序:
1. 利用JSR-380校验注解的默认行为(最简单)
大部分javax.validation.constraints下的注解(比如@Size、@Min、@Max)默认不对null值进行校验,只有当字段有实际值(包括DTO里的默认值)时才会触发校验。
看你的例子:
class SomeDTO { @Size(min = 5) String foo; // 客户端没传,值为null,@Size不会触发校验 @Min(2) Integer bar = 1; // 有默认值1(非null),@Min会校验这个值是否符合要求 }
这种情况下,直接在Controller方法里用@Valid注解校验DTO,就会自动跳过null字段的校验,只校验有值的字段,完全符合你的需求。
注意:像
@NotNull、@NotEmpty这类注解是专门用来校验非null/非空的,如果你不想在PATCH场景下校验这些,需要用分组校验来排除它们。
2. 分组校验(针对需要区分场景的复杂情况)
如果你的DTO需要同时支持CREATE(全字段校验)和PATCH(部分字段校验)两种场景,就可以用校验分组来区分:
步骤1:定义校验分组接口
public interface PatchValidationGroup {}
步骤2:在DTO的校验注解上指定分组
把需要在PATCH场景下校验的注解,加上groups属性:
class SomeDTO { // 仅在CREATE场景校验非空,PATCH场景跳过 @NotNull(groups = CreateValidationGroup.class) @Size(min = 5, groups = {CreateValidationGroup.class, PatchValidationGroup.class}) String foo; // CREATE和PATCH场景都校验最小值(若只想在PATCH生效,就加groups = PatchValidationGroup.class) @Min(2) Integer bar = 1; }
步骤3:在Controller的PATCH方法中使用分组校验
Spring Boot 1.5支持用@Validated注解指定校验分组:
@PatchMapping("/some-resource/{id}") public ResponseEntity<Void> updateResource( @PathVariable Long id, @Validated(PatchValidationGroup.class) @RequestBody SomeDTO dto ) { // 业务逻辑 return ResponseEntity.noContent().build(); }
这样就只会校验标注了PatchValidationGroup分组的字段,且同样遵循“null字段不触发校验”的默认规则。
3. 自定义校验器(针对特殊规则场景)
如果你的需求更特殊(比如客户端传了null但需要跳过,而默认值需要校验,或者某些字段即使是null也要校验),可以自定义一个校验器来实现“仅校验非null字段”的逻辑:
步骤1:自定义校验注解
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = PatchValidator.class) public @interface ValidatePatch { String message() default "Invalid patch request"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
步骤2:实现校验器逻辑
public class PatchValidator implements ConstraintValidator<ValidatePatch, Object> { private Validator validator; @Autowired public void setValidator(Validator validator) { this.validator = validator; } @Override public boolean isValid(Object dto, ConstraintValidatorContext context) { // 获取DTO的所有字段 Field[] fields = dto.getClass().getDeclaredFields(); Set<ConstraintViolation<Object>> violations = new HashSet<>(); for (Field field : fields) { field.setAccessible(true); try { Object value = field.get(dto); // 仅当字段值非null时,校验该字段的约束 if (value != null) { violations.addAll(validator.validateProperty(dto, field.getName())); } } catch (IllegalAccessException e) { // 处理字段访问异常 } } if (!violations.isEmpty()) { // 将校验错误添加到上下文,返回前端 for (ConstraintViolation<Object> violation : violations) { context.buildConstraintViolationWithTemplate(violation.getMessage()) .addPropertyNode(violation.getPropertyPath().toString()) .addConstraintViolation(); } return false; } return true; } }
步骤3:在DTO上使用自定义注解
@ValidatePatch class SomeDTO { @Size(min = 5) String foo; @Min(2) Integer bar = 1; }
步骤4:Controller中使用@Valid校验
@PatchMapping("/some-resource/{id}") public ResponseEntity<Void> updateResource( @PathVariable Long id, @Valid @RequestBody SomeDTO dto ) { // 业务逻辑 return ResponseEntity.noContent().build(); }
这种方式完全自定义了校验逻辑,适合复杂场景,但一般情况下前两种方法足够满足需求。
内容的提问来源于stack exchange,提问作者Opal
相关产品推荐
相关产品推荐

