调整Jakarta Validations与自定义Validations的执行顺序问题
问题与需求
调用/orders/{order_id}/discounts/{discount_id}接口时,当前执行流程如下:
- 请求通过配置的过滤器链完成认证
- 执行
DiscountValidator的isValid()方法 - 触发
@Valid注解,验证@NotNull、@Digits等Jakarta标准注解 - 执行
@PreAuthorize授权逻辑
需求:调整验证顺序,让Jakarta标准注解先完成字段验证(确保discountValue等字段非空),再执行自定义验证器DiscountValidator。
现有代码
控制器方法签名
@PreAuthorize("hasRealmRoles('INTERNAL_USER','ROBOTIC_USER') or orderBelongsToAccount(#orderIdAsString)") @PutMapping(path = "orders/{order_id}/discounts/{discount_id}", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity<OrderDiscountDto> updateOrderDiscount(@Valid @RequestBody OrderDiscountRequest orderDiscountRequest, @UUIDConstraint @PathVariable("order_id") String orderIdAsString, @UUIDConstraint @PathVariable("discount_id") String discountIdAsString);
OrderDiscountRequest实体类
@AllArgsConstructor @NoArgsConstructor @Data @Builder @ValidDiscount public class OrderDiscountRequest { @Schema(description = "订单总金额中扣除的小数金额", name = "discount_value", example = "31.00", type = "string", format = "string") @JsonProperty("discount_value") @DecimalMin("0.0") @Digits(integer = 10, fraction = 2) @NotNull(message = "折扣金额不能为空,请提供字段值。") private BigDecimal discountValue; @Schema(description = "包含折扣策略信息的对象", name = "policy", implementation = DiscountPolicy.class, enumAsRef = true) @JsonProperty("policy") @NotNull(message = "折扣策略不能为空,请提供字段值。") private DiscountPolicy policy; }
ValidDiscount自定义注解
@Constraint(validatedBy = DiscountValidator.class) @Target({ElementType.PARAMETER, ElementType.TYPE, ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface ValidDiscount { String message() default "折扣请求信息无效"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
DiscountValidator验证器类
@Slf4j public class DiscountValidator implements ConstraintValidator<ValidDiscount, OrderDiscountRequest> { @Override public boolean isValid(OrderDiscountRequest discountRequest, ConstraintValidatorContext context) { validatePercentagePolicy(discountRequest); return true; } private void validatePercentagePolicy(OrderDiscountRequest discountRequest) { if (DiscountPolicy.PERCENTAGE.equals(discountRequest.getPolicy()) && discountRequest.getDiscountValue().compareTo(MAX_PERCENTAGE) == 1) { log.warn("折扣百分比值大于100.00"); throw new ConstraintViolationException(ValidationMessages.ORDER_DISCOUNT_PERCENTAGE_VALUE_EXCEEDED, new HashSet<>()); } } }
验证配置类
@Configuration public class ValidationConfiguration { @Bean public MethodValidationPostProcessor methodValidationPostProcessor() { MethodValidationPostProcessor processor = new MethodValidationPostProcessor(); processor.setBeforeExistingAdvisors(true); return processor; } }
解决方案
通过验证分组+组序列可以精准控制验证顺序,具体步骤如下:
1. 定义验证分组接口
创建两个空接口,分别标记字段验证和自定义验证的阶段:
public interface FieldValidationGroup {} public interface CustomValidationGroup {}
2. 为验证注解分配分组并指定执行顺序
修改OrderDiscountRequest,给Jakarta标准字段注解指定FieldValidationGroup,给自定义注解@ValidDiscount指定CustomValidationGroup,并通过@GroupSequence规定执行顺序:
@AllArgsConstructor @NoArgsConstructor @Data @Builder @ValidDiscount(groups = CustomValidationGroup.class) // 强制先执行字段验证组,再执行自定义验证组 @GroupSequence({FieldValidationGroup.class, CustomValidationGroup.class}) public class OrderDiscountRequest { @Schema(description = "订单总金额中扣除的小数金额", name = "discount_value", example = "31.00", type = "string", format = "string") @JsonProperty("discount_value") @DecimalMin(value = "0.0", groups = FieldValidationGroup.class) @Digits(integer = 10, fraction = 2, groups = FieldValidationGroup.class) @NotNull(message = "折扣金额不能为空,请提供字段值。", groups = FieldValidationGroup.class) private BigDecimal discountValue; @Schema(description = "包含折扣策略信息的对象", name = "policy", implementation = DiscountPolicy.class, enumAsRef = true) @JsonProperty("policy") @NotNull(message = "折扣策略不能为空,请提供字段值。", groups = FieldValidationGroup.class) private DiscountPolicy policy; }
3. 修改控制器的验证注解
将控制器中的@Valid替换为@Validated,并指定要执行的分组(或直接指定实体类,自动应用组序列):
@PreAuthorize("hasRealmRoles('INTERNAL_USER','ROBOTIC_USER') or orderBelongsToAccount(#orderIdAsString)") @PutMapping(path = "orders/{order_id}/discounts/{discount_id}", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity<OrderDiscountDto> updateOrderDiscount( // 方式1:直接指定分组顺序 @Validated({FieldValidationGroup.class, CustomValidationGroup.class}) @RequestBody OrderDiscountRequest orderDiscountRequest, @UUIDConstraint @PathVariable("order_id") String orderIdAsString, @UUIDConstraint @PathVariable("discount_id") String discountIdAsString);
或者利用实体类上的@GroupSequence,简化写法:
@Validated(OrderDiscountRequest.class) @RequestBody OrderDiscountRequest orderDiscountRequest
4. 优化自定义验证器的异常处理(可选)
建议通过ConstraintValidatorContext收集错误信息,替代直接抛出异常,实现所有验证错误的统一返回:
@Slf4j public class DiscountValidator implements ConstraintValidator<ValidDiscount, OrderDiscountRequest> { @Override public boolean isValid(OrderDiscountRequest discountRequest, ConstraintValidatorContext context) { // 禁用默认错误消息,避免重复输出 context.disableDefaultConstraintViolation(); boolean isValid = true; if (DiscountPolicy.PERCENTAGE.equals(discountRequest.getPolicy()) && discountRequest.getDiscountValue().compareTo(MAX_PERCENTAGE) == 1) { log.warn("折扣百分比值大于100.00"); // 添加自定义错误信息到验证上下文 context.buildConstraintViolationWithTemplate(ValidationMessages.ORDER_DISCOUNT_PERCENTAGE_VALUE_EXCEEDED) .addPropertyNode("discount_value") .addConstraintViolation(); isValid = false; } return isValid; } }
调整后执行流程变为:
- 请求通过过滤器链完成认证
- 执行Jakarta标准字段注解验证(
@NotNull、@Digits等) - 执行自定义
DiscountValidator的验证逻辑 - 执行
@PreAuthorize授权逻辑
内容的提问来源于stack exchange,提问作者fanis798
相关产品推荐
相关产品推荐

