«1. Обзор

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

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

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

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

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

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

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

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

@Component
public class FunctionalHandler {

    public Mono<ServerResponse> handleRequest(ServerRequest request) {
        Mono<String> 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: например, что ни одно из полей не может быть пустым и что код должен содержать более 6 цифр.

public class CustomRequestEntity {
    
    private String name;
    private String code;

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

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

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

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

Мы не будем вдаваться в подробности о том, как работает валидатор. Достаточно знать, что все ошибки собираются при проверке объекта — пустая коллекция ошибок означает, что объект соответствует всем нашим ограничениям.

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.");
        }
    }
}

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

2.2. Выполнение проверки

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

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

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

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

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

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

public Mono<ServerResponse> handleRequest(ServerRequest request) {
    Validator validator = new CustomRequestEntityValidator();
    Mono<String> 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);
}

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

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

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

3. Работа над СУХИМ подходом

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

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

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

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

public abstract class AbstractValidationHandler<T, U extends Validator> {

    private final Class<T> validationClass;

    private final U validator;

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

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

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

public Mono<ServerResponse> 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<ServerResponse> onValidationErrors(
  Errors errors,
  T invalidBody,
  ServerRequest request) {
    throw new ResponseStatusException(
      HttpStatus.BAD_REQUEST,
      errors.getAllErrors().toString());
}

Наконец, мы установим метод processBody неопределенным — мы оставим дочерним классам решать, как действовать в этом случае:

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

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

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

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

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

Мы можем посмотреть весь класс здесь.

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

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

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

Сделав это, мы будем вынуждены использовать родительский конструктор и определить, как мы будем обрабатывать наш запрос в методе processBody:

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

@Component
public class FunctionalHandler
  extends AbstractValidationHandler<CustomRequestEntity, CustomRequestEntityValidator> {

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

    @Override
    protected Mono<ServerResponse> 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 проверки компонентов

При таком подходе мы также можем воспользоваться преимуществами мощных аннотаций проверки компонентов, предоставляемых пакетом javax.validation.

Например, давайте определим новый объект с аннотированными полями:

Теперь мы можем просто создать новый обработчик, внедряемый с Spring Validator по умолчанию, предоставленным bean-компонентом LocalValidatorFactoryBean:

public class AnnotatedRequestEntity {
 
    @NotNull
    private String user;

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

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

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

public class AnnotatedRequestEntityValidationHandler
  extends AbstractValidationHandler<AnnotatedRequestEntity, Validator> {

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

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

        // ...

    }
}

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

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

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

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

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

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

«