В микросервисной архитектуре службы могут принимать несколько, если не много, одних и тех же входных данных. Этот шаблон может легко привести к дублированию кода и избыточности между службами. Стремясь смягчить эти недостатки и сохранить ориентированность на сервисный код, мы можем разработать надежное решение, включающее несколько API, предоставляемых Spring и Java.
Следующее руководство предполагает некоторые практические знания Java и Spring Boot, но будет соответствовать целому ряду уровней квалификации. Как бы то ни было, никогда не помешает посмотреть код другого разработчика!
Фон
Наше решение будет включать в себя объединение API-интерфейсов Java и Spring Boot, ConstraintValidator
и ResponseEntityExceptionHandler
соответственно.
Hibernate Validator , улучшенный как часть JSR 380 , представляет собой спецификацию Java API для стандартной проверки Bean/| . В контексте приложений Spring Boot вы, возможно, использовали это, не задумываясь дважды. Примеры включают:
@NotNull
@Мин
@Макс
@Шаблон
@Прошлое
@Электронная почта
@PositiveOrZero Положительное значение
В этом руководстве мы рассмотрим, как выйти за рамки этих базовых проверок, используя интерфейс ConstraintValidator
, чтобы определить наш собственный набор ограничений.
В то время как в экосистеме Spring существуют другие формы обработки исключений, ResponseEntityExceptionHandler
обеспечивает глобальную (и централизованную) обработку исключений в рамках службы. Эта глобализация является ключом к эффективности нашей пользовательской аннотации ограничений, поскольку она позволяет нам проверять несколько компонентов (или полей внутри них). Тем не менее, мы исследуем, как мы можем использовать этот класс для изящной обработки нарушений наших ограничений.
Реализация
Давайте нырнем внутрь. Чтобы избежать переполнения руководства шаблонным кодом, вы найдете только необходимые блоки кода в разделах ниже. Эта статья сопровождается рабочим примером на GitHub .
Примечание: Я буду ссылаться на этот пример проекта на протяжении всего урока.
Зависимости
Список короткий и приятный:
implementation 'org.springframework.boot:spring-boot-starter-validation'
Создание аннотаций
Мы начнем с простого. Допустим, мы внедрили сервис Холодильник и Кладовая , который позволяет нам:
- Управляйте хранилищами холодильников и кладовых
- Принимайте запросы POST и/или PUT с полезной нагрузкой JSON
Мы хотим проверить общие поля между моделями запросов обеих служб. Ваша модель запроса может выглядеть примерно так:
public class FoodRequestModel { private String name; @PositiveOrZero(message = "Quantity must be positive") @Max(value = 25, message = "Quantity must not exceed 25") private int quantity; private String category private boolean refrigerated; }
Одно из простейших ограничений, которые мы можем создать, будет включать составление существующих ограничений, таких как @PositiveOrZero
и @Max
в приведенном выше примере. Это позволяет нам наложить явный ярлык на общие ограничения и назвать это “бизнес-логикой”. Ниже мы определяем @FoodQuantity
:
@Documented @PositiveOrZero(message = "Quantity must be positive") @Max(value = 25, message = "Quantity must not exceed 25") @Constraint(validatedBy = {}) @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.PARAMETER}) public @interface FoodQuantity { String message() default "Invalid quantity"; Class>[] groups() default {}; Class extends Payload>[] payload() default {}; }
Здесь многое происходит, так что давайте разберемся с этим:
@Constraint
помечает аннотацию как ограничение проверки компонента и позволяет нам указыватьConstraintValidator
реализации; здесь приветствуется ноль, одна или несколько реализаций.@Retention
устанавливается таким образом, чтобы наша аннотация сохранялась во время выполнения@Target
установлен таким образом, мы можем проверять различные типы входных данных для наших сервисовсообщение
,группы
иполезная нагрузка
требуются для@Ограничение
но их не обязательно устанавливать – они обеспечивают специфику, выходящую за рамки того, что мы рассмотрим сегодня
Это ни в коем случае не является упрощением. Не обращая внимания на многословие, аннотация открывает немало возможностей для упрощения обработки, как мы увидим в следующем примере.
Давайте определим ограничение для поля category таким образом, чтобы:
- Категория должна быть передана и не может быть пустой
- Разрешается проходить только определенные категории
- Категории могут отличаться между услугами Холодильник и Кладовая
Чтобы реализовать эту аннотацию, мы расширим предпосылку нашей первой аннотации, добавив пользовательский параметр и предоставив реализацию интерфейса ConstraintValidator
. Результат выглядит примерно так:
@Documented @NotNull(message = "Category must be present") @NotEmpty(message = "Category must not be empty") @Constraint(validatedBy = FoodCategoryValidator.class) @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.PARAMETER}) public @interface FoodCategory { String message() default "Invalid category"; Class>[] groups() default {}; Class extends Payload>[] payload() default {}; String[] allowed() default {"dairy", "grain"}; }
В этой аннотации происходит еще несколько вещей по сравнению с @FoodQuantity
. Мы указали новый параметр allowed
, чтобы ограничить то, что может быть передано в category
. Обратите внимание на значение по умолчанию – на этот массив ссылаются только в том случае, если значения не передаются в @FoodCategory
. Чтобы справиться с этим ограничением, мы внедрили Средство проверки категории продуктов питания
:
@Slf4j public class FoodCategoryValidator implements ConstraintValidator{ List allowed; @Override public void initialize(FoodCategory constraintAnnotation) { this.allowed = Arrays.asList(constraintAnnotation.allowed()); } @Override public boolean isValid(String value, ConstraintValidatorContext context) { log.info("isValid: value=[{}]", value); if (!allowed.contains(value.toLowerCase())) { String err = "Category must be one of the following: " + allowed; context.disableDefaultConstraintViolation(); context.buildConstraintViolationWithTemplate(err) .addConstraintViolation(); return false; } return true; } }
Давайте разберем наш новый класс валидатора:
ConstraintValidator
параметризуется классом аннотаций и проверяемым типом–aСтрока
, содержащая значение категорииГлобальное поле
разрешено
, заданное в переопределенном методеinitialize
- Именно в рамках этого метода мы получаем доступ к параметрам
@FoodCategory
для использования во всем классе validator
- Именно в рамках этого метода мы получаем доступ к параметрам
является действительным
это мясо и кости нашей проверки ограничений- Для недопустимых сценариев мы отключаем нарушение ограничений по умолчанию, создаем правильное сообщение об ошибке и возвращаем false – это в конечном итоге создает исключение, которое нас заинтересует позже
Наконец, чтобы получить максимальную отдачу от нашей аннотации, мы разделим поле категории на два подкласса, относящихся к каждой службе.
После всей нашей напряженной работы мы получили чистый набор моделей запросов, готовых к бомбардировке недопустимыми значениями:
public class FoodRequestModel { private String name; @FoodQuantity private int quantity; private boolean refrigerated; } public class FridgeRequestModel extends FoodRequestModel { @FoodCategory(allowed = {"dairy", "vegetables", "beer"}) private String category; } public class PantryRequestModel extends FoodRequestModel { @FoodCategory(allowed = {"grains", "canned", "snacks"}) private String category; }
Обработка Ошибок Проверки
До сих пор мы определяли только ограничения, которым мы (и наши потребители) должны следовать. Давайте откроем конечную точку, позволяющую добавлять продукты в холодильник, и проверим наши ограничения:
@Slf4j @Validated @RestController @RequestMapping(path = "/api/v1/fridge") @RequiredArgsConstructor(onConstructor_ = @Autowired) public class FridgeController { private final FridgeService fridgeService; @PostMapping("/food") public ResponseEntityaddFoodToFridgeV1( @Valid @RequestBody FridgeRequestModel request) { log.info("addFoodToFridgeV1: request=[{}]", request); FoodResponseModel response = fridgeService.addFoodToFridge(request); return ResponseEntity.ok(response); } }
Есть несколько важных аспектов, на которые следует обратить внимание, чтобы наши аннотации работали должным образом:
@Validated
должен использоваться либо на уровне class , либо method , чтобы указать, где необходимо выполнить проверку@Valid
используется для пометки свойства для каскадной проверки, которая запускает наши ограничения
Давайте отправим полезную нагрузку:
{ "name": "milk", "category": "dairy", "quantity": 2, "refrigerated": true }
Успех! Но давайте посмотрим, что произойдет, когда мы отправим другую полезную нагрузку, которая, как мы знаем, приведет к ошибке:
{ "name": "pinto beans", "category": "legumes", "quantity": -3, "refrigerated": true }
Обратите внимание, что мы нарушаем несколько ограничений для этой модели запроса. Вы должны увидеть подробный ответ, содержащий подробную информацию об обнаруженных ошибках и вызванных исключениях. Такое многословие не идеально подходит для нас (или наших потребителей), поэтому давайте отфильтруем важные детали с помощью простого переопределения ResponseEntityExceptionHandler
.
При дальнейшем изучении ответа на ошибку, предоставленного Spring, вы можете заметить выданное исключение: методargumentnotvalidexception
. Это исключение, которое нас будет интересовать для обработки нарушений ограничений в обработчике исключений.
Во-первых, нам нужна модель для сбора соответствующей информации. Мы можем выделить следующее из исходного ответа Spring:
public class ErrorResponseModel { private final String errorMessage; private final LocalDateTime timestamp; private final String path; }
Далее мы создадим как глобальный обработчик исключений, так и метод для обработки наших ограничений:
@Slf4j @RestControllerAdvice public class ErrorController extends ResponseEntityExceptionHandler { @Override protected final ResponseEntity
Давайте отметим здесь несколько моментов:
@RestControllerAdvice
– это просто специализированный компонент для классов, которые объявляют@ExceptionHandler
методы, общие для нескольких классов контроллеровМы переопределяем
ResponseEntityExceptionHandler
‘sАргумент метода обработки Недопустимый
метод так что мы мочь- Регистрируйте важную информацию
- Создайте небольшой целенаправленный ответ на ошибку
- Возвращает код состояния HTTP по нашему выбору, основанный на ограничении нарушенный
При отправке полезной нагрузки, которая нарушает определенные нами ограничения, вы должны увидеть краткий ответ, указывающий, где мы ошиблись:
{ "errorMessage": "Quantity must be positive", "timestamp": "2020-11-20T19:18:31.1899697", "path": "uri=/api/v1/pantry/food" }
Вызов
В этом руководстве представлены некоторые основные формы проверки ограничений в REST-сервисе на базе Spring/Java. Если вы хотите продвинуться немного дальше, вот несколько мест, с которых вы можете начать:
- Исследуйте, что может отличаться при нарушении ограничений
@PathVariable
или@RequestParam
- Реализовать вложенные ограничения в рамках сложной модели запроса
- Увеличьте гибкость кода состояния HTTP, возвращаемого потребителю
- Разверните пример проекта, чтобы учесть нюансы составной службы – например, службы Picnic
- Изучите
@Constraint
API подробнее – что может быть полезной нагрузкойи
группы
будут использоваться для?
Закрытие
На этом заканчивается руководство по реализации пользовательских средств проверки ограничений с использованием аннотаций! Не бойтесь дать мне знать, если я что-то пропустил. Я, безусловно, приветствую (и ценю) критику, вопросы и тому подобное.
Для дополнительной справки, вот репозиторий GitHub с полным рабочим кодом и примерами, представленными в этой статье:
Verley93/проверка аннотации
⚡ Учебное пособие по проверке запроса на загрузку Spring
Описание
Этот репозиторий дополняет учебное пособие, написанное на Medium Девлином Верли II. Проверить это здесь !
содержание
Установка
- Запустить
git clone https://github.com/Verley93/annotation-validation.git
в инструменте командной строки - Откройте проект в IntelliJ или в редакторе по выбору и позвольте проекту загрузить дистрибутив Gradle и правильно проиндексировать
- Запустите проект или создайте и запустите jar в терминале
Использование
Как только служба будет запущена локально ( http://localhost:8080 по умолчанию), вы сможете свободно отправлять запросы через Postman или вашу любимую платформу для вызова API.
Оригинал: “https://dev.to/verley93/constraint-validation-in-spring-boot-microservices-3ikm”