头图

在 Java 开发过程中,参数校验是一个非常关键的部分。简单的校验规则,如通过 @NotNull@NotBlank@Size 等注解能够很方便地实现常见的校验逻辑,但对于稍微复杂一些的场景,比如多个参数之间的逻辑关系依赖,或者业务上下文相关的校验,注解往往无法胜任。为了能够优雅地处理这些复杂的校验场景,我们需要采用更加灵活且扩展性强的方案。

背景问题解读

让我们具体看一下你提到的场景。假设有一个接口,包含以下两个参数:

private boolean switch;
private String str;

根据业务逻辑,当 switchtrue 时,str 必须不为 null,否则接口请求应被视为无效。这样的逻辑在注解中难以直接实现,因为 @NotNull 只适用于单一字段,而无法根据其他字段的值来决定当前字段是否应该校验为非空。这就是为什么我们需要探索更加灵活的校验策略。

解决方案探讨

为了优雅地解决类似的参数校验问题,我们可以采用以下几种策略:

1. 手动编写逻辑校验

最直接的方法是在业务逻辑层中手动编写校验规则。通过简单的 if 语句,可以实现对参数之间逻辑关系的验证。这种方式是最常见、最基础的实现方式。代码如下:

public void validateInput(boolean switch, String str) {
    if (switch && str == null) {
        throw new IllegalArgumentException("When switch is true, str must not be null.");
    }
}

优点在于它非常直观,逻辑清晰,适合一些简单的场景。但缺点同样显而易见,手动校验的方式分散在代码中,容易造成重复校验逻辑,并且随着业务复杂度的增加,代码的可读性和可维护性都会下降。

2. 定制校验注解

为了更好地分离校验逻辑和业务逻辑,Java 提供了自定义注解的功能,结合 javax.validation 中的 Constraint 注解,能够实现基于注解的复杂校验。

步骤如下:

  1. 定义自定义注解:
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = { SwitchStrValidator.class })
public @interface ValidSwitchStr {
    String message() default "Invalid parameters";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

这里定义了一个新的注解 @ValidSwitchStr,它将作用于类级别(ElementType.TYPE),并且通过 Constraint 指定了一个验证器 SwitchStrValidator

  1. 编写验证器:
public class SwitchStrValidator implements ConstraintValidator<ValidSwitchStr, MyRequest> {

    @Override
    public boolean isValid(MyRequest request, ConstraintValidatorContext context) {
        if (request.isSwitch() && request.getStr() == null) {
            return false;
        }
        return true;
    }
}

这里的 SwitchStrValidator 实现了 ConstraintValidator 接口,并将 isValid 方法的逻辑定义为:当 switchtrue 时,str 必须不为 null

  1. 在类上使用自定义注解:
@ValidSwitchStr
public class MyRequest {
    private boolean switch;
    private String str;

    // getters and setters
}

通过这样的方式,我们可以将校验逻辑封装到注解和验证器中,业务代码中只需要声明 @ValidSwitchStr,校验逻辑会自动生效。这种方式的好处在于分离了业务逻辑和校验逻辑,使得代码更加模块化,校验规则清晰明确。

3. 使用 Spring Validator 机制

在 Spring 框架中,Validator 是一个常见的校验机制。相比于简单的手动校验逻辑,Validator 提供了更好的可扩展性和复用性。我们可以通过实现 org.springframework.validation.Validator 接口,编写复杂的校验逻辑。

  1. 编写自定义 Validator:
@Component
public class MyRequestValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return MyRequest.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        MyRequest request = (MyRequest) target;

        if (request.isSwitch() && request.getStr() == null) {
            errors.rejectValue("str", "str.null", "When switch is true, str must not be null.");
        }
    }
}
  1. 在 Controller 中使用 Validator:
@RestController
public class MyController {

    @Autowired
    private MyRequestValidator myRequestValidator;

    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.addValidators(myRequestValidator);
    }

    @PostMapping("/submit")
    public ResponseEntity<String> submit(@Valid @RequestBody MyRequest request, BindingResult result) {
        if (result.hasErrors()) {
            return ResponseEntity.badRequest().body(result.getAllErrors().toString());
        }

        // 处理业务逻辑
        return ResponseEntity.ok("Success");
    }
}

通过 @InitBinder 方法将 Validator 绑定到 WebDataBinder 上,我们可以在请求处理之前进行自定义校验。这样做不仅能够实现灵活的校验逻辑,还可以将错误信息集中处理。

4. 使用 Bean Validation API 进行高级校验

如果项目使用的是 Java EE 或者 Spring 框架,Bean Validation(即 JSR 303/JSR 380)提供了强大的校验支持。通过结合 @Valid 和自定义约束注解,能够解决复杂的参数校验问题。这里我们以组合式校验为例,演示如何使用 Bean Validation 进行高级参数校验。

  1. 定义类级别的校验:

    假设有一个接口,包含多个相关参数,而这些参数之间的约束逻辑相对复杂,如下所示:

    public class AdvancedRequest {
    
        @NotNull
        private Boolean switch;
    
        private String str;
    
        // getters and setters
    }

    switch 决定了 str 的合法性。为了处理这种场景,可以使用类级别的校验注解:

    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Constraint(validatedBy = AdvancedRequestValidator.class)
    public @interface ValidAdvancedRequest {
        String message() default "Invalid request parameters";
        Class<?>[] groups() default {};
        Class<? extends Payload>[] payload() default {};
    }
  2. 实现验证逻辑:

    编写对应的验证器,实现 isValid 方法,来完成实际的校验逻辑:

    public class AdvancedRequestValidator implements ConstraintValidator<ValidAdvancedRequest, AdvancedRequest> {
    
        @Override
        public boolean isValid(AdvancedRequest request, ConstraintValidatorContext context) {
            if (request.getSwitch() != null && request.getSwitch() && request.getStr() == null) {
                return false;
            }
            return true;
        }
    }
  3. 使用注解:

    最后,将这个校验注解应用到 AdvancedRequest 类上:

    @ValidAdvancedRequest
    public class AdvancedRequest {
        // Fields, getters, and setters
    }

5. 使用第三方库:如 Apache Commons 或 Google Guava

除了手动编写逻辑或使用框架自带的工具,还可以引入一些第三方库来简化参数校验。比如 Apache Commons 提供了 Validate 类,可以帮助我们快速进行常见的校验。

示例代码如下:

public void validateInput(boolean switch, String str) {
    if (switch) {
        Validate.notNull(str, "When switch is true, str must not be null");
    }
}

这种方式虽然简单,但仍然只是手动校验的一个变种,没有解决复用和可维护性的问题。它适合那些项目较为简单的场景,如果项目规模较大,建议还是采用更正式的校验机制。

6. 在大型项目中的应用案例

在一个真实的企业级项目中,假设你正在开发一个电商平台的订单服务。订单创建时,用户需要提供支付方式和支付凭证。但有些支付方式(如信用卡支付)要求提供完整的支付凭证,而其他支付方式(如货到付款)则不需要提供这些信息。这就类似于上述的 switchstr 的问题。

在这种情况下,如果我们用手动校验或者简单的注解来处理,代码会变得非常繁琐。因此,采用定制化的注解和验证器机制是更好的选择。在订单创建的请求对象上,我们

可以创建一个类似 @ValidPayment 的注解,用于根据支付方式动态验证支付凭证是否为必填字段。

这不仅让代码更加清晰,也让团队能够快速扩展和维护相关的校验逻辑。

结语

通过以上几种方法,复杂的参数校验问题可以在 Java 中得到优雅的解决。无论是手动校验、自定义注解、Spring Validator 机制,还是使用 Bean Validation API,每种方案都有其独特的优势和适用场景。在实际项目中,选择适合的方案需要根据具体的业务需求和项目规模来决定。

无论采用哪种方式,都要注意保持校验逻辑的简洁、模块化和可维护性,使代码在应对复杂的业务变化时能够轻松应对。


注销
1k 声望1.6k 粉丝

invalid