6

如何使用责任链默认优雅地进行参数校验? - JAVA旭阳

 1 year ago
source link: https://www.cnblogs.com/alvinscript/p/17264009.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

如何使用责任链默认优雅地进行参数校验?

项目中参数校验十分重要,它可以保护我们应用程序的安全性和合法性。我想大家通常的做法是像下面这样做的:

@Override
public void validate(SignUpCommand command) {
    validateCommand(command); // will throw an exception if command is not valid
    validateUsername(command.getUsername()); // will throw an exception if username is duplicated
    validateEmail(commend.getEmail()); // will throw an exception if email is duplicated
}

这么做最大的优势就是简单直接,但是如果验证逻辑很复杂,会导致这个类变得很庞大,而且上面是通过抛出异常来改变代码执行流程,这也是一种不推荐的做法。

那么有什么更好的参数校验的方式呢?本文就推荐一种通过责任链设计模式来优雅地实现参数的校验功能,我们通过一个用户注册的例子来讲明白如何实现。

  • 有效的注册数据——名字、姓氏、电子邮件、用户名和密码。
  • 用户名必须是唯一的。
  • 电子邮件必须是唯一的。

定义用户注册和验证结果类

  1. 定义一个SignUpCommand类用来接受用户注册的属性信息。并且使用 @Value 注解让这个类不可变。
import lombok.Value;

import javax.validation.constraints.*;

@Value
public class SignUpCommand {

    @Min(2)
    @Max(40)
    @NotBlank
    private final String firstName;

    @Min(2)
    @Max(40)
    @NotBlank
    private final String lastName;

    @Min(2)
    @Max(40)
    @NotBlank
    private final String username;

    @NotBlank
    @Size(max = 60)
    @Email
    private final String email;

    @NotBlank
    @Size(min = 6, max = 20)
    private final String rawPassword;
  • 使用javax.validation中的注解如@NotBlank@Size来验证用户注册信息是否有效。
  • 使用lombok的注解@Value,因为我希望命令对象是不可变的。注册用户的数据应与注册表中填写的数据相同。
  1. 定义存储验证结果类ValidationResult,如下所示:
@Value
public class ValidationResult {
    private final boolean isValid;
    private final String errorMsg;

    public static ValidationResult valid() {
        return new ValidationResult(true, null);
    }

    public static ValidationResult invalid(String errorMsg) {
        return new ValidationResult(false, errorMsg);
    }

    public boolean notValid() {
        return !isValid;
    }
}
  • 在我看来,这是一种非常方便的方法返回类型,并且比抛出带有验证消息的异常要好。
  1. 既然是责任链,还需要定义一个“链”类ValidationStep,它是这些验证步骤的超类,我们希望将它们相互“链接”起来。
public abstract class ValidationStep<T> {

    private ValidationStep<T> next;

    public ValidationStep<T> linkWith(ValidationStep<T> next) {
        if (this.next == null) {
            this.next = next;
            return this;
        }
        ValidationStep<T> lastStep = this.next;
        while (lastStep.next != null) {
            lastStep = lastStep.next;
        }
        lastStep.next = next;
        return this;
    }

    public abstract ValidationResult validate(T toValidate);

    protected ValidationResult checkNext(T toValidate) {
        if (next == null) {
            return ValidationResult.valid();
        }

        return next.validate(toValidate);
    }
}

核心验证逻辑

现在我们开始进行参数校验的核心逻辑,也就是如何把上面定义的类给串联起来。

  1. 我们定义一个用于注册验证的接口类SignUpValidationService
public interface SignUpValidationService {
    ValidationResult validate(SignUpCommand command);
}
  1. 现在我们可以使用上面定义的类和责任链模式来轻松的实现,代码如下:
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.Set;

@Service
@AllArgsConstructor
public class DefaultSignUpValidationService implements SignUpValidationService {

    private final UserRepository userRepository;

    @Override
    public ValidationResult validate(SignUpCommand command) {
        return new CommandConstraintsValidationStep()
                .linkWith(new UsernameDuplicationValidationStep(userRepository))
                .linkWith(new EmailDuplicationValidationStep(userRepository))
                .validate(command);
    }

    private static class CommandConstraintsValidationStep extends ValidationStep<SignUpCommand> {

        @Override
        public ValidationResult validate(SignUpCommand command) {
            try (ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory()) {
                final Validator validator = validatorFactory.getValidator();
                final Set<ConstraintViolation<SignUpCommand>> constraintsViolations = validator.validate(command);

                if (!constraintsViolations.isEmpty()) {
                    return ValidationResult.invalid(constraintsViolations.iterator().next().getMessage());
                }
            }
            return checkNext(command);
        }
    }

    @AllArgsConstructor
    private static class UsernameDuplicationValidationStep extends ValidationStep<SignUpCommand> {

        private final UserRepository userRepository;

        @Override
        public ValidationResult validate(SignUpCommand command) {
            if (userRepository.findByUsername(command.getUsername()).isPresent()) {
                return ValidationResult.invalid(String.format("Username [%s] is already taken", command.getUsername()));
            }
            return checkNext(command);
        }
    }

    @AllArgsConstructor
    private static class EmailDuplicationValidationStep extends ValidationStep<SignUpCommand> {

        private final UserRepository userRepository;

        @Override
        public ValidationResult validate(SignUpCommand command) {
            if (userRepository.findByEmail(command.getEmail()).isPresent()) {
                return ValidationResult.invalid(String.format("Email [%s] is already taken", command.getEmail()));
            }
            return checkNext(command);
        }
    }
}
  • validate方法是核心方法,其中调用linkWith方法组装参数的链式校验器,其中涉及多个验证类,先做基础验证,如果通过的话,去验证用户名是否重复,如果也通过的话,去验证Email是否重复。
  • CommandConstraintsValidationStep类,此步骤是一个基础验证,所有的javax validation annotation都会被验证,比如是否为空,Email格式是否正确等等。这非常方便,我们不必自己编写这些验证器。如果一个对象是有效的,那么调用checkNext方法让流程进入下一步,checkNext,如果不是,ValidationResult 将立即返回。
  • UsernameDuplicationValidationStep类,此步骤验证用户名是否重复,主要需要去查数据库了。如果是,那么将立即返回无效的ValidationResult,否则的话继续往后走,去验证下一步。
  • EmailDuplicationValidationStep 类,电子邮件重复验证。因为没有下一步,如果电子邮件是唯一的,则将返回ValidationResult.valid()

上面就是通过责任链模式来实现我们参数校验的完整过程了,你学会了吗?这种方式可以优雅的将验证逻辑拆分到单独的类中,如果添加新的验证逻辑,只需要添加新的类,然后组装到“校验链”中。但是在我看来,这比较适合于用于校验相对复杂的场景,如果只是简单的校验就完全没必要这么做了,反而会增加代码的复杂度。

欢迎关注个人公众号【JAVA旭阳】交流学习


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK