Русский
Русский
English
Статистика
Реклама

Перевод Валидация и обработка исключений с помощью Spring

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

В этой статье я обобщаю свой опыт и даю несколько советов по валидации интерфейсов.

Архитектура и терминология

Я создаю свои приложения, которые предоставляют веб-API, следуяшаблонулуковой архитектуры(Onion Architecture).Эта статья не об архитектуре Onion, но я хотел бы упомянуть некоторые из ее ключевых моментов, которые важны для понимания моих мыслей:

  • Контроллеры RESTи любые веб-компоненты и конфигурации являются частью внешнегоинфраструктурного уровня.

  • Среднийсервисный уровеньсодержит сервисы, которые объединяют бизнес-функции и решают общие проблемы, такие как безопасность или транзакции.

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

Набросок слоев луковой архитектуры и места размещения типичных классов Spring.Набросок слоев луковой архитектуры и места размещения типичных классов Spring.

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

  • Запрос отправляетсяконтроллеруна уровне инфраструктуры.

  • Контроллер десериализует запрос и - в случае успеха - запрашивает результат у соответствующего сервиса на уровне сервисы.

  • Служба проверяет, есть ли у текущего пользователяразрешениена вызов функции, и инициализируеттранзакциюбазы данных(при необходимости).

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

  • Сервис также может вызывать несколько репозиториев, преобразовывать и агрегировать результаты.

  • Репозиторий на уровне доменавозвращает бизнес-объекты.Этот уровень отвечает за поддержание всех объектов в допустимом состоянии.

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

Проверка на уровне запроса, уровня обслуживания и домена.Проверка на уровне запроса, уровня обслуживания и домена.

В этой архитектуре у нас есть три интерфейса, для каждого из которых требуется разная валидация:

  • Контроллер определяет первый интерфейс.Чтобы десериализовать запрос, нужно выполнить его валидацию по нашейсхеме API.Это делается неявно с помощью фреймворка маппирования, такого как Jackson, и явно с помощьюограничений,таких как @NotNull.Мы называем этовалидацией запроса.

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

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

Валидация запроса

Обычно мыдесериализуемвходящий запрос, для которого уже выполнена неявная валидация параметров запроса и тела запроса.Spring Boot автоматически настраивает Jackson десериализацию и общую обработку исключений.Например, взгляните например контроллерамоейдемонстрации BGG:

@GetMapping("/newest")Flux<ThreadsPerBoardGame> getThreads(@RequestParam String user, @RequestParam(defaultValue = "PT1H") Duration since) {    return threadService.findNewestThreads(user, since);}

Оба вызова сотсутствующим параметроминеправильным типомвозвращают сообщения об ошибках с правильнымкодом состояния:

curl -i localhost:8080/threads/newestHTTP/1.1 400 Bad RequestContent-Type: application/jsonContent-Length: 189{"timestamp":"2020-04-15T03:40:00.460+0000","path":"/threads/newest","status":400,"error":"Bad Request","message":"Required String parameter 'user' is not present","requestId":"98427b15-7"}curl -i "localhost:8080/threads/newest?user=chrigu&since=a"HTTP/1.1 400 Bad RequestContent-Type: application/jsonContent-Length: 156{"timestamp":"2020-04-15T03:40:06.952+0000","path":"/threads/newest","status":400,"error":"Bad Request","message":"Type mismatch.","requestId":"7600c788-8"}

С конфигурацией по умолчанию Spring Boot мы также получимтрассировки стека.Я выключил их, установив

server:  error:    include-stacktrace: never

вapplication.yml.Эта обработка ошибок поумолчанию обеспечивается BasicErrorControllerв классическомWeb MVCи по DefaultErrorWebExceptionHandler вWebFlux, и извлечение тела ответа от ErrorAttributes.

Связывание данных

В приведенных выше примерах демонстрируютсяатрибуты@RequestParamили любой простой атрибут метода контроллера без аннотации.Проверка запроса становится иной при проверке@ModelAttribute,@RequestBodyили непростых параметров, как в

@GetMapping("/newest/obj")Flux<ThreadsPerBoardGame> getThreads(@Valid ThreadRequest params) {    return threadService.findNewestThreads(params.user, params.since);}static class ThreadRequest {    @NotNull    private final String user;    @NotNull    private final Duration since;    public ThreadRequest(String user, Duration since) {        this.user = user;        this.since = since == null ? Duration.ofHours(1) : since;    }}

Если аннотации @RequestParam могут использоваться, чтобы сделать параметробязательнымили созначением по умолчанию, вкомандных объектахэто делается с помощьюограниченийпроверки bean-компонентов,таких как @NotNull и простой Java / Kotlin.Чтобы активировать проверку bean-компонента, аргумент метода должен быть аннотирован@Valid.

Когда проверка bean-компонента завершается неудачно,в реактивном стеке выдаетсяисключениеBindExceptionилиWebExchangeBindException.Оба исключения реализуют BindingResult, который предоставляет вложенные ошибки для каждого недопустимого значения поля.Вышеуказанный метод контроллера приведет к сообщениям об ошибках, например

curl "localhost:8080/java/threads/newest/obj" -iHTTP/1.1 400 Bad RequestContent-Type: application/jsonContent-Length: 1138{"timestamp":"2020-04-17T13:52:39.500+0000","path":"/java/threads/newest/obj","status":400,"error":"Bad Request","message":"Validation failed for argument at index 0 in method: reactor.core.publisher.Flux<ch.chrigu.bgg.service.ThreadsPerBoardGame> ch.chrigu.bgg.infrastructure.web.JavaThreadController.getThreads(ch.chrigu.bgg.infrastructure.web.JavaThreadController$ThreadRequest), with 1 error(s): [Field error in object 'threadRequest' on field 'user': rejected value [null]; codes [NotNull.threadRequest.user,NotNull.user,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [threadRequest.user,user]; arguments []; default message [user]]; default message [darf nicht null sein]] ","requestId":"c87c7cbb-17","errors":[{"codes":["NotNull.threadRequest.user","NotNull.user","NotNull.java.lang.String","NotNull"],"arguments":[{"codes":["threadRequest.user","user"],"arguments":null,"defaultMessage":"user","code":"user"}],"defaultMessage":"darf nicht null sein","objectName":"threadRequest","field":"user","rejectedValue":null,"bindingFailure":false,"code":"NotNull"}]}

Настройка обработки исключений

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

curl "localhost:8080/java/threads/newest/obj?user=chrigu&since=a" -iHTTP/1.1 500 Internal Server ErrorContent-Type: application/jsonContent-Length: 513{"timestamp":"2020-04-17T13:56:42.922+0000","path":"/java/threads/newest/obj","status":500,"error":"Internal Server Error","message":"Failed to convert value of type 'java.lang.String' to required type 'java.time.Duration'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.time.Duration] for value 'a'; nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [a]","requestId":"4c0dc6bd-21"}

Он также возвращает неправильный код ошибки, подразумевающий ошибку сервера, даже если клиент указал неправильный тип для параметра since.Оба примера были сгенерированы с помощью реактивного стека, MVC имеет лучшие значения по умолчанию.Для обоих случаев нам нужно настроить обработку исключений.Это можно сделать, предоставив собственный bean-компонент ErrorAttributes, который записываетжелаемоетело ответа.Код состояния ответа предоставляется значением status.

Или мы можем пойти на меньшее вмешательство и использовать реализацию DefaultErrorAttributes,либо добавив в исключенияаннотацию @ResponseStatus, либо позволив всем исключениям расширять ResponseStatusException.Оба способа позволяют настроить статус ответа и значение сообщения.К сожалению, большинство исключений, создаваемых на уровне инфраструктуры, предоставляются фреймворком и не могут быть настроены, поэтому нам нужно другое решение.Одна из возможностей для аннотированных контроллеров - использовать @ExceptionHandler для отдельных исключений. Тогда мы могли бы создать ответ с нуля, но это пропустило бы обработку исключений по умолчанию, и мы хотели бы иметь одинаковую обработку для каждого исключения. Таким образом, чтобы улучшить ответ выше, просто повторно вызовите исключения (rethrow):

@ControllerAdviceclass GlobalExceptionHandler {    @ExceptionHandler(TypeMismatchException::class)    fun handleTypeMismatchException(e: TypeMismatchException): HttpStatus {        throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid value '${e.value}'", e)    }    @ExceptionHandler(WebExchangeBindException::class)    fun handleWebExchangeBindException(e: WebExchangeBindException): HttpStatus {        throw object : WebExchangeBindException(e.methodParameter!!, e.bindingResult) {            override val message = "${fieldError?.field} has invalid value '${fieldError?.rejectedValue}'"        }    }}

Резюме

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

  • Непосредственно в контроллере с помощьюtry/catch(MVC) илиonErrorResume()(Webflux).Я не рекомендую это в большинстве случаев, потому что сквозная проблема, такая как обработка исключений, должна быть определена глобально, чтобы гарантировать согласованное поведение.

  • Перехватить исключения вфункциях @ExceptionHandler.Создайте свои собственные ответы с помощью @ExceptionHandler (Throwable.class) для случая по умолчанию.

  • Илиповторно генерируйте исключения, аннотируйте их с помощью @ResponseStatus или расширяйте ResponseStatusException, чтобы настроить ответ для определенных случаев.

Мне нравится запускать приложения Spring Boot с конфигурацией по умолчанию и заменять части там, где это необходимо.В этом случае я рекомендовал начать с третьего варианта, а если требуется дополнительная настройка, переключиться на второй.

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

Источник: habr.com
К списку статей
Опубликовано: 18.10.2020 12:20:24
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Java

Spring boot

Bean validation api

Spring mvc

Spring webflux

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru