Динамическая проверка POJO на основе групп в spring

Рассмотрим следующую ссылку для справки:

public class User{

    private  String username;
    private String firstName;
    private String middleName;
    private String lastName;
    private String phone;

    //getters and setters

}

Мое приложение представляет собой API REST, основанный на принципе spring -boot, который предоставляет две конечные точки, один для создания пользователя, а другой - для пользователя.

"Пользователи" попадают в определенные категории, group-a, group-b и т.д., которые я получаю из заголовков почтового запроса.

Мне нужно проверить данные пользователя во время выполнения, и проверки могут отличаться в зависимости от группы пользователей.

например, пользователи, которые попадают в группу-a, могут иметь номера телефонов в качестве необязательного поля, тогда как это может быть обязательное поле для какой-либо другой группы.

Регулярное выражение также может меняться в зависимости от их групп.

Мне нужно настроить spring, чтобы как-то динамически проверить мое pojo, как только они будут созданы, и их соответствующий набор проверок запускается на основе их групп.

Может быть, я могу создать конфигурацию yml/xml, которая позволила бы мне включить это?

Я бы предпочел не комментировать мои private String phone с помощью @NotNull и @Pattern.

Моя конфигурация выглядит следующим образом:

public class NotNullValidator implements Validator {
    private String group;
    private Object target;

    public String getGroup() {
        return group;
    }

    public void setGroup(String group) {
        this.group = group;
    }

    public Object getTarget() {
        return target;
    }

    public void setTarget(Object target) {
        this.target = target;
    }

    @Override
    public void validate(Object o) {
        if (Objects.nonNull(o)) {
            throw new RuntimeException("Target is null");
        }
    }
}


public interface Validator {
    void validate(Object o);
}


@ConfigurationProperties(prefix = "not-null")
@Component
public class NotNullValidators {
    List<NotNullValidator> validators;

    public List<NotNullValidator> getValidators() {
        return validators;
    }

    public void setValidators(List<NotNullValidator> validators) {
        this.validators = validators;
    }
}


application.yml

not-null:
  validators:

    -
      group: group-a
      target: user.username

    -
      group: group-b
      target: user.phone

Я хочу настроить мое приложение так, чтобы позволить валидаторам выбирать свои цели (фактические объекты, не строки, упомянутые в yml), и вызывать их соответствующие public void validate(Object o) для своих целей.

P.S.

Пожалуйста, не стесняйтесь редактировать вопрос, чтобы сделать его лучше.

Я использую jackson для сериализации и десериализации JSON.

Ответ 1

Самое простое решение вашей проблемы, как я вижу, - это не с Spring или самими POJO, а с шаблоном проектирования.

Проблема, которую вы описываете, легко решается решением стратегии.

Вы сопоставляете стратегию с использованием заголовка, который вы ожидаете в запросе, который описывает тип пользователя, а затем вы выполняете указанные проверки внутри самой стратегии.

Это позволит вам использовать один и тот же POJO для всего подхода и учитывать специфику обработки/обработки и проверки данных в соответствии с каждым типом стратегии пользователя.

Здесь ссылка из wiki-книг с подробным объяснением шаблона

Шаблон стратегии

Предположим, у вас есть базовый интерфейс для ваших стратегий:

interface Strategy { 

    boolean validate(User user);
}

И у вас есть две разные реализации для двух разных типов пользователей:

public class StrategyA implements Strategy {

    public boolean validate(User user){

         return user.getUsername().isEmpty();
    }
}

public class StrategyB implements Strategy {

    public boolean validate(User user){

         return user.getPhone().isEmpty();
    }
}

Вы добавляете атрибут стратегии к своему User POJO и назначаете правильную реализацию атрибута Strategy этому атрибуту при получении запроса на отправку.

Каждый раз, когда вам нужно проверить данные для этого пользователя, вам просто нужно вызвать метод validate для назначенной стратегии.

Если каждый User может соответствовать нескольким стратегиям, вы можете добавить List<Strategy> в качестве атрибута вместо одного.

Если вы не хотите изменять POJO, вы должны проверить правильность стратегии каждый раз, когда вы получаете почтовый запрос.

Помимо метода validate вы можете добавлять методы обработки данных, специфичные для каждой стратегии.

Надеюсь, что это поможет.

Ответ 2

Вы можете использовать группы проверки для управления тем типом пользователя, для которого поле проверяется. Например:

@NotBlank(groups = {GroupB.class})
private String phone;

@NotBlank(groups = {GroupA.class, GroupB.class})
private String username;

Затем вы используете заголовки из запроса, который вы упомянули, чтобы решить, для какой группы необходимо выполнить проверку.

Для полного примера см. http://blog.codeleak.pl/2014/08/validation-groups-in-spring-mvc.html?m=1.


Обновлен, чтобы включить более полный пример:

public class Val {
    private Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

    public boolean isValid(User user, String userType) {
        usergroups userGroup = usergroups.valueOf(userType);
        Set<ConstraintViolation<User>> constraintViolations = validator.validate(user, userGroup.getValidationClass());
        return constraintViolations.isEmpty();
    }

    public interface GroupA {}
    public interface GroupB {}

    public enum usergroups {
        a(GroupA.class),
        b(GroupB.class);

        private final Class clazz;

        usergroups(Class clazz) {
            this.clazz = clazz;
        }

        public Class getValidationClass() {
            return clazz;
        }
    }
}

Это не использует application.yaml, вместо этого сопоставление того, какие поля проверяются для каждой группы, задается в аннотации, аналогичные результаты с использованием Spring встроены в поддержку проверки.

Ответ 3

Мне удалось решить мою проблему с помощью Jayway JsonPath. Мое решение выглядит следующим образом:

  • Добавить фильтр в ваш API, который может кэшировать InputStream ServletRequest, так как он может быть прочитан только один раз. Для этого следуйте этой ссылке.
  • Создайте кучу валидаторов и настройте их в файле application.yml с помощью @ConfigurationProperties. Для этого следуйте этой ссылке
  • Создайте оболочку, которая будет содержать все ваши валидаторы в виде списка и инициализирует ее с помощью @ConfigurationProperties и следующей конфигурации:

    validators:
      regexValidators:
        -
          target: $.userProfile.lastName
          pattern: '[A-Za-z]{0,12}'
          group: group-b
    
      minMaxValidators:
        -
          target: $.userProfile.age
          min: 18
          max: 50
          group: group-b
    
  • Вызвать метод validate в этой оболочке с группой, которая входит в заголовок, а затем вызвать validate отдельных валидаторов. Для этого я написал следующий код в моей обертке:

    public void validate(String input, String group) {
        regexValidators.stream()
                .filter(validator -> group.equals(validator.getGroup()))
                .forEach(validator -> validator.validate(input));
    
        minMaxValidators.stream()
                .filter(validator -> group.equals(validator.getGroup()))
                .forEach(validator -> validator.validate(input));
    }
    

и следующий метод в моем валидаторе:

public void validate(String input) {
    String data = JsonPath.parse(input).read(target);
    if (data == null) {
        throw new ValidationException("Target:  " + target + "  is NULL");
    }
    Matcher matcher = rule.matcher(data);
    if (!matcher.matches()) {
        throw new ValidationException("Target:  " + target + "  does not match the pattern:  " + pattern);
    }
}

Я создал функционирующий проект, чтобы продемонстрировать валидации, и его можно найти здесь.
Я понимаю, что один ответ может быть не совсем понятным, пожалуйста, следуйте указанному выше URL-адресу для полного исходного кода.