You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

调整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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 11:17:13