Рубрики
Без рубрики

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

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

Автор оригинала: Ger Roza.

1. Обзор

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

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

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

2. Использование Весенних Валидаций

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

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

@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. Внедрение валидатора

Как объяснено в справочной документации this 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.");
        }
    }
}

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

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

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

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

Но мы должны иметь в виду, что в этих фильтрах-так же, как и в обработчиках – мы имеем дело с асинхронными конструкциями -такими как 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);
}

В двух словах, наш сервис теперь будет получать ответ ” Плохой запрос “, если тело запроса не соответствует нашим ограничениям.

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

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

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

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

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

Все обработчики, которым требуется проверка ввода, расширят этот абстрактный класс, чтобы повторно использовать его основную схему и, следовательно, следовать принципу 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());
}

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

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

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

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

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

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

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

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

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

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

Первое, что нам, очевидно, придется сделать, – это расширить наш обработчик из этого абстрактного класса.

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

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

При таком подходе мы также можем воспользоваться преимуществами мощного Аннотации проверки бобов предоставлено предоставлено пакет.

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

public class AnnotatedRequestEntity {
 
    @NotNull
    private String user;

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

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

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

public class AnnotatedRequestEntityValidationHandler
  extends AbstractValidationHandler {

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

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

        // ...

    }
}

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

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

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

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

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

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

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