Перейти к основному содержимому

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

· 7 мин. чтения

1. Обзор

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

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

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

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

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

Представьте, что у нас есть следующая 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 :

public class CustomRequestEntity {

private String name;
private String code;

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

}

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

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

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

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

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 ), но не к данным, которые он в конечном итоге предоставит.

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

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

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

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

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

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

Попробуем улучшить это.

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

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

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

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

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...
}
}

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

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. Поддержка аннотаций Bean Validation API

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

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

public class AnnotatedRequestEntity {

@NotNull
private String user;

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

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

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

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) {

// ...

}
}

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

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

5. Вывод

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

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

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

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