Проверка функциональных конечных точек в Spring 5

Проверка функциональных конечных точек в Spring 5

1. обзор

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

К сожалению, в Spring 5 нет возможности автоматически запускать проверки на функциональных конечных точках, как это делаем мы на аннотированных. Приходится управлять ими вручную.

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

2. Использование Spring Validations

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

Представьте, что у нас есть следующиеRouterFunction:

@Bean
public RouterFunction functionalRoute(
  FunctionalHandler handler) {
    return RouterFunctions.route(
      RequestPredicates.POST("/functional-endpoint"),
      handler::handleRequest);
}

Этот маршрутизатор использует функцию обработчика, предоставляемую следующим классом контроллера:

@Component
public class FunctionalHandler {

    public Mono handleRequest(ServerRequest request) {
        Mono responseBody = request
          .bodyToMono(CustomRequestEntity.class)
          .map(cre -> String.format(
            "Hi, %s [%s]!", cre.getName(), cre.getCode()));

        return ServerResponse.ok()
          .contentType(MediaType.APPLICATION_JSON)
          .body(responseBody, String.class);
    }
}

Как мы видим, все, что мы делаем в этой функциональной конечной точке, - это форматируем и извлекаем информацию, которую мы получили в теле запроса, который структурирован как объектCustomRequestEntity:

public class CustomRequestEntity {

    private String name;
    private String code;

    // ... Constructors, Getters and Setters ...

}

Это работает нормально, но давайте представим, что теперь нам нужно проверить, что наш ввод соответствует некоторым заданным ограничениям, например, что ни одно из полей не может быть нулевым и что код должен содержать более 6 цифр.

Нам нужно найти способ сделать эти утверждения эффективно и, по возможности, отделить их от нашей бизнес-логики.

2.1. Реализация валидатора

As it’s explained in this Spring Reference Documentation, we can use the Spring’s Validator interface to evaluate our resource’s values:

public class CustomRequestEntityValidator
  implements Validator {

    @Override
    public boolean supports(Class clazz) {
        return CustomRequestEntity.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmptyOrWhitespace(
          errors, "name", "field.required");
        ValidationUtils.rejectIfEmptyOrWhitespace(
          errors, "code", "field.required");
        CustomRequestEntity request = (CustomRequestEntity) target;
        if (request.getCode() != null && request.getCode().trim().length() < 6) {
            errors.rejectValue(
              "code",
              "field.min.length",
              new Object[] { Integer.valueOf(6) },
              "The code must be at least [6] characters in length.");
        }
    }
}

Мы не будем вдаваться в подробности оhow, с которым работаетValidator. Достаточно знать, что все ошибки собираются при проверке объекта -an empty error collection means that the object adheres to all our constraints.

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

2.2. Выполнение валидации

Сначала мы можем подумать, что использованиеHandlerFilterFunction было бы подходящим в нашей ситуации.

Но мы должны помнить, что в этих фильтрах - так же, как и в обработчиках - мы имеем дело сasynchronous constructions - такими какMono иFlux.

Это означает, что у нас будет доступ кPublisher (объектMono илиFlux), но не к данным, которые он в конечном итоге предоставит.

Поэтому лучшее, что мы можем сделать, - это проверить тело, когда мы фактически обрабатываем его в функции-обработчике.

Давайте продолжим и изменим наш метод обработчика, включая логику проверки:

public Mono handleRequest(ServerRequest request) {
    Validator validator = new CustomRequestEntityValidator();
    Mono responseBody = request
      .bodyToMono(CustomRequestEntity.class)
      .map(body -> {
        Errors errors = new BeanPropertyBindingResult(
          body,
          CustomRequestEntity.class.getName());
        validator.validate(body, errors);

        if (errors == null || errors.getAllErrors().isEmpty()) {
            return String.format("Hi, %s [%s]!", body.getName(), body.getCode());
        } else {
            throw new ResponseStatusException(
              HttpStatus.BAD_REQUEST,
              errors.getAllErrors().toString());
        }
    });
    return ServerResponse.ok()
      .contentType(MediaType.APPLICATION_JSON)
      .body(responseBody, String.class);
}

Вкратце, наша служба теперь будет получать ответ «Bad Request», если тело запроса не соответствует нашим ограничениям.

Можно ли сказать, что мы достигли нашей цели? Что ж, мы почти у цели. We’re running the validations, but there are many drawbacks in this approach.с

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

Давай попробуем это исправить.

3. Работа над сухим подходом

To create a cleaner solution we’ll start by declaring an abstract class containing the basic procedure to process a request.

Все обработчики, которым требуется проверка ввода, будут расширять этот абстрактный класс, чтобы повторно использовать его основную схему, и, следовательно, следуя принципу DRY (не повторяйтесь).

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

public abstract class AbstractValidationHandler {

    private final Class validationClass;

    private final U validator;

    protected AbstractValidationHandler(Class clazz, U validator) {
        this.validationClass = clazz;
        this.validator = validator;
    }

    public final Mono handleRequest(final ServerRequest request) {
        // ...here we will validate and process the request...
    }
}

Теперь давайте закодируем наш методhandleRequest стандартной процедурой:

public Mono handleRequest(final ServerRequest request) {
    return request.bodyToMono(this.validationClass)
      .flatMap(body -> {
        Errors errors = new BeanPropertyBindingResult(
          body,
          this.validationClass.getName());
        this.validator.validate(body, errors);

        if (errors == null || errors.getAllErrors().isEmpty()) {
            return processBody(body, request);
        } else {
            return onValidationErrors(errors, body, request);
        }
    });
}

Как видим, мы используем два метода, которые еще не создали.

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

protected Mono onValidationErrors(
  Errors errors,
  T invalidBody,
  ServerRequest request) {
    throw new ResponseStatusException(
      HttpStatus.BAD_REQUEST,
      errors.getAllErrors().toString());
}

Это просто реализация по умолчанию, но она может быть легко переопределена дочерними классами.

Finally, we’ll set the processBody method undefined -we’ll leave it up to the child classes to determine how to proceed in that case:

abstract protected Mono processBody(
  T validBody,
  ServerRequest originalRequest);

Есть несколько аспектов для анализа в этом классе.

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

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

Во время выполнения конструктор назначит фактический объект валидатора и класс, используемый для приведения тела запроса.

Мы можем взглянуть на полный классhere.

Давайте теперь посмотрим, какую пользу мы можем извлечь из этой структуры.

3.1. Адаптация нашего обработчика

Очевидно, первое, что нам нужно сделать, это расширить наш обработчик из этого абстрактного класса.

При этомwe’ll be forced to use the parent’s constructor and to define how we’ll handle our request in the processBody method:

@Component
public class FunctionalHandler
  extends AbstractValidationHandler {

    private CustomRequestEntityValidationHandler() {
        super(CustomRequestEntity.class, new CustomRequestEntityValidator());
    }

    @Override
    protected Mono processBody(
      CustomRequestEntity validBody,
      ServerRequest originalRequest) {
        String responseBody = String.format(
          "Hi, %s [%s]!",
          validBody.getName(),
          validBody.getCode());
        return ServerResponse.ok()
          .contentType(MediaType.APPLICATION_JSON)
          .body(Mono.just(responseBody), String.class);
    }
}

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

4. Поддержка аннотаций API проверки бинов

При таком подходе мы также можем воспользоваться мощным преимуществомBean Validation’s annotations, предоставляемым пакетомjavax.validation.

Например, давайте определим новую сущность с аннотированными полями:

public class AnnotatedRequestEntity {

    @NotNull
    private String user;

    @NotNull
    @Size(min = 4, max = 7)
    private String password;

    // ... Constructors, Getters and Setters ...
}

We can now simply create a new handler injected with the default Spring Validator provided by the LocalValidatorFactoryBean bean:

public class AnnotatedRequestEntityValidationHandler
  extends AbstractValidationHandler {

    private AnnotatedRequestEntityValidationHandler(@Autowired Validator validator) {
        super(AnnotatedRequestEntity.class, validator);
    }

    @Override
    protected Mono processBody(
      AnnotatedRequestEntity validBody,
      ServerRequest originalRequest) {

        // ...

    }
}

Мы должны помнить, что если в контексте присутствуют другие bean-компонентыValidator, нам, возможно, придется явно объявить их с аннотацией@Primary:

@Bean
@Primary
public Validator springValidator() {
    return new LocalValidatorFactoryBean();
}

5. Заключение

Подводя итог, в этом посте мы узнали, как проверять входные данные в функциональных конечных точках Spring 5.

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

Конечно, предлагаемое решение может не подходить для любого сценария. Нам придется проанализировать нашу ситуацию и, возможно, адаптировать структуру к нашим потребностям.

Если мы хотим увидеть весь рабочий пример, мы можем найти его вour GitHub repo.