Проверка компонента является стандартом де-факто для реализации логики проверки в экосистеме Java. Он хорошо интегрирован с пружиной и пружинным ботинком.
Однако здесь есть некоторые подводные камни. В этом руководстве рассматриваются все основные варианты использования проверки и примеры спортивного кода для каждого из них.
Пример кода
Эта статья сопровождается рабочим примером кода на GitHub .
Настройка Проверки
Поддержка валидации компонентов Spring Boot поставляется с программой validation starter, которую мы можем включить в наш проект (нотация Gradle).:
implementation('org.springframework.boot:spring-boot-starter-validation')
Нет необходимости добавлять номер версии, так как плагин Spring для управления зависимостями делает это за нас. Если вы не используете плагин, вы можете найти самую последнюю версию здесь .
Однако, если мы также включили веб-стартер, стартер проверки предоставляется бесплатно:
implementation('org.springframework.boot:spring-boot-starter-web')
Обратите внимание, что запуск проверки не более чем добавляет зависимость к совместимой версии hibernate validator , которая является наиболее широко используемой реализацией спецификации проверки компонента.
Основы проверки бобов
По сути, проверка компонента работает путем определения ограничений для полей класса путем аннотирования их определенными аннотациями .
Затем вы передаете объект этого класса в Валидатор , который проверяет, выполняются ли ограничения.
Мы увидим более подробную информацию в примерах ниже.
Проверка входных данных для контроллера Spring MVC
Допустим, мы внедрили контроллер Spring REST и хотим проверить входные данные, переданные клиентом. Есть три вещи, которые мы можем проверить для любого входящего HTTP-запроса:
- тело запроса,
- переменные внутри пути (например,
идентификаторв/foos/{идентификатор}) и, - параметры запроса.
Давайте рассмотрим каждый из них более подробно.
Проверка тела запроса
В запросах POST и PUT обычно передается полезная нагрузка JSON в теле запроса. Spring автоматически сопоставляет входящий JSON с объектом Java. Теперь мы хотим проверить, соответствует ли входящий объект Java нашим требованиям.
Это наш класс входящей полезной нагрузки:
class Input {
@Min(1)
@Max(10)
private int numberBetweenOneAndTen;
@Pattern(regexp = "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$")
private String ipAddress;
// ...
}
У нас есть поле int , которое должно иметь значение от 1 до 10 включительно, и поле String , которое должно содержать IP-адрес (регулярное выражение фактически по-прежнему допускает недопустимые IP-адреса с октетами больше 255, но мы исправим это позже в учебнике).
Вот контроллер REST, который принимает Введите объект в тело запроса и подтвердите его:
@RestController
class ValidateRequestBodyController {
@PostMapping("/validateBody")
ResponseEntity validateBody(@Valid @RequestBody Input input) {
return ResponseEntity.ok("valid");
}
}
Мы просто добавили аннотацию @Valid к Введите параметр, который также помечен @RequestBody , чтобы отметить, что он должен быть прочитан из тела запроса. Делая это, мы говорим Spring передать объект Валидатору , прежде чем делать что-либо еще.
@Допустимо для сложных типов
Если класс Input содержит поле с другим сложным типом, которое должно быть проверено, это поле также должно быть помечено @Valid .
Если проверка завершится неудачей, это вызовет Методаргументнозначное исключение . По умолчанию Spring переведет это исключение в статус HTTP 400 (Неверный запрос).
Мы можем проверить это поведение с помощью интеграционного теста:
@ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = ValidateRequestBodyController.class)
class ValidateRequestBodyControllerTest {
@Autowired
private MockMvc mvc;
@Autowired
private ObjectMapper objectMapper;
@Test
void whenInputIsInvalid_thenReturnsStatus400() throws Exception {
Input input = invalidInput();
String body = objectMapper.writeValueAsString(input);
mvc.perform(post("/validateBody")
.contentType("application/json")
.content(body))
.andExpect(status().isBadRequest());
}
}
Вы можете найти более подробную информацию о тестировании контроллеров Spring MVC в моей статье о @WebMvcTest аннотации .
Проверка переменных пути и параметров запроса
Проверка переменных пути и параметров запроса работает немного по-другому.
В этом случае мы не проверяем сложные объекты Java, поскольку переменные пути и параметры запроса являются примитивными типами, такими как int или их аналогичные объекты, такие как Целое число или Строка .
Вместо аннотирования поля класса, как указано выше, мы добавляем аннотацию ограничения (в данном случае @Min ) непосредственно к параметру метода в контроллере Spring:
@RestController
@Validated
class ValidateParametersController {
@GetMapping("/validatePathVariable/{id}")
ResponseEntity validatePathVariable(
@PathVariable("id") @Min(5) int id) {
return ResponseEntity.ok("valid");
}
@GetMapping("/validateRequestParameter")
ResponseEntity validateRequestParameter(
@RequestParam("param") @Min(5) int param) {
return ResponseEntity.ok("valid");
}
}
Обратите внимание, что мы должны добавить аннотацию Spring @Validated в контроллер на уровне класса, чтобы указать Spring оценивать аннотации ограничений для параметров метода.
Аннотация @Validated в этом случае оценивается только на уровне класса, хотя ее разрешено использовать в методах (мы узнаем, почему это разрешено на уровне метода при обсуждении групп проверки позже).
В отличие от проверки тела запроса, неудачная проверка вызовет исключение ConstraintViolationException вместо Методаргументнозначное исключение . Spring не регистрирует обработчик исключений по умолчанию для этого исключения, поэтому по умолчанию он вызовет ответ со статусом HTTP 500 (Внутренняя ошибка сервера).
Если мы хотим вместо этого вернуть статус HTTP 400 (что имеет смысл, поскольку клиент предоставил недопустимый параметр, что делает его неправильным запросом), мы можем добавить пользовательский обработчик исключений в наш контроллер:
@RestController
@Validated
class ValidateParametersController {
// request mapping method omitted
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
ResponseEntity handleConstraintViolationException(ConstraintViolationException e) {
return new ResponseEntity<>("not valid due to validation error: " + e.getMessage(),
HttpStatus.BAD_REQUEST);
}
}
Позже в этом руководстве мы рассмотрим, как вернуть структурированный ответ об ошибке, содержащий подробную информацию обо всех неудачных проверках для проверки клиентом.
Мы можем проверить поведение проверки с помощью интеграционного теста:
@ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = ValidateParametersController.class)
class ValidateParametersControllerTest {
@Autowired
private MockMvc mvc;
@Test
void whenPathVariableIsInvalid_thenReturnsStatus400() throws Exception {
mvc.perform(get("/validatePathVariable/3"))
.andExpect(status().isBadRequest());
}
@Test
void whenRequestParameterIsInvalid_thenReturnsStatus400() throws Exception {
mvc.perform(get("/validateRequestParameter")
.param("param", "3"))
.andExpect(status().isBadRequest());
}
}
Проверка входных данных для метода обслуживания Spring
Вместо (или дополнительно) проверки входных данных на уровне контроллера мы также можем проверять входные данные для любых компонентов Spring. Для этого мы используем комбинацию @Validated и @Допустимые аннотации:
@Service
@Validated
class ValidatingService{
void validateInput(@Valid Input input){
// do something
}
}
Опять же, аннотация @Validated оценивается только на уровне класса, поэтому не помещайте ее в метод в этом случае использования.
Вот тест, проверяющий поведение проверки:
@ExtendWith(SpringExtension.class)
@SpringBootTest
class ValidatingServiceTest {
@Autowired
private ValidatingService service;
@Test
void whenInputIsInvalid_thenThrowsException(){
Input input = invalidInput();
assertThrows(ConstraintViolationException.class, () -> {
service.validateInput(input);
});
}
}
Проверка сущностей JPA
Последней линией защиты для проверки является уровень сохраняемости. По умолчанию в Spring Data используется режим гибернации, который поддерживает проверку компонентов из коробки.
Является ли уровень сохраняемости подходящим местом для проверки?
Обычно мы не хотим проводить проверку так поздно, как на уровне сохраняемости, потому что это означает, что приведенный выше бизнес-код работал с потенциально недопустимыми объектами, что может привести к непредвиденным ошибкам. Подробнее об этой теме в моей статье о Анти-шаблонах проверки компонентов .
Допустим, вы хотите хранить объекты нашего Введите класс в базу данных. Во-первых, мы добавляем необходимую аннотацию JPA @Сущность и добавьте поле идентификатора:
@Entity
public class Input {
@Id
@GeneratedValue
private Long id;
@Min(1)
@Max(10)
private int numberBetweenOneAndTen;
@Pattern(regexp = "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$")
private String ipAddress;
// ...
}
Затем мы создаем хранилище данных Spring, которое предоставляет нам методы для сохранения и запроса Ввод объекты:
public interface ValidatingRepository extends CrudRepository {}
По умолчанию, каждый раз, когда мы используем репозиторий для хранения Введите объект, аннотации ограничений которого нарушены, мы получим Исключение ConstraintViolationException как демонстрирует этот интеграционный тест:
@ExtendWith(SpringExtension.class)
@DataJpaTest
class ValidatingRepositoryTest {
@Autowired
private ValidatingRepository repository;
@Autowired
private EntityManager entityManager;
@Test
void whenInputIsInvalid_thenThrowsException() {
Input input = invalidInput();
assertThrows(ConstraintViolationException.class, () -> {
repository.save(input);
entityManager.flush();
});
}
}
Вы можете найти более подробную информацию о тестировании хранилищ данных Spring в моей статье о @DataJpaTest аннотации .
Обратите внимание, что проверка компонента запускается в режиме гибернации только после сброса EntityManager . Hibernate автоматически сбрасывает EntityManager при определенных обстоятельствах, но в случае нашего интеграционного теста мы должны сделать это вручную.
Если по какой-либо причине мы хотим отключить проверку компонентов в наших хранилищах данных Spring, мы можем установить свойство Spring Boot Если по какой-либо причине мы хотим отключить проверку компонентов в наших хранилищах данных Spring, мы можем установить свойство Spring Boot кому нет .
Реализация Пользовательского Валидатора
Если доступных аннотаций ограничений недостаточно для наших вариантов использования, мы можем захотеть создать их самостоятельно.
В классе Input выше мы использовали регулярное выражение для проверки того, что строка является допустимым IP-адресом. Однако регулярное выражение не является полным: оно допускает октеты со значениями больше 255 (т.е. “111.111.111.333” будет считаться допустимым).
Давайте исправим это, внедрив валидатор, который реализует эту проверку в Java вместо регулярного выражения (да, я знаю, что мы могли бы просто использовать более сложное регулярное выражение для достижения того же результата, но нам нравится реализовывать проверки в Java, не так ли?).
Сначала мы создаем аннотацию пользовательского ограничения Ip-Адрес :
@Target({ FIELD })
@Retention(RUNTIME)
@Constraint(validatedBy = IpAddressValidator.class)
@Documented
public @interface IpAddress {
String message() default "{IpAddress.invalid}";
Class>[] groups() default { };
Class extends Payload>[] payload() default { };
}
Для аннотации пользовательского ограничения требуется все следующее:
- параметр
сообщение, указывающий на ключ свойства вValidationMessages.properties, который используется для разрешения сообщения в случае нарушения, - параметр
группы, позволяющий определить, при каких обстоятельствах должна запускаться эта проверка (мы поговорим о группах проверки позже), - параметр
полезная нагрузка, позволяющий определить полезную нагрузку, передаваемую с помощью этой проверки (поскольку это редко используемая функция, мы не будем рассматривать ее в этом руководстве), и - аннотация
@Constraint, указывающая на реализацию интерфейсаConstraintValidator.
Реализация валидатора выглядит следующим образом:
class IpAddressValidator implements ConstraintValidator{ @Override public boolean isValid(String value, ConstraintValidatorContext context) { Pattern pattern = Pattern.compile("^([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})$"); Matcher matcher = pattern.matcher(value); try { if (!matcher.matches()) { return false; } else { for (int i = 1; i <= 4; i++) { int octet = Integer.valueOf(matcher.group(i)); if (octet > 255) { return false; } } return true; } } catch (Exception e) { return false; } } }
Теперь мы можем использовать аннотацию @IPAddress точно так же, как и любую другую аннотацию ограничения:
class InputWithCustomValidator {
@IpAddress
private String ipAddress;
// ...
}
Проверка Программно
Могут быть случаи, когда мы хотим вызвать проверку программно вместо того, чтобы полагаться на встроенную поддержку проверки компонентов Spring.
В этом случае мы можем просто создать Валидатор вручную и вызовите его, чтобы запустить проверку:
class ProgrammaticallyValidatingService {
void validateInput(Input input) {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set> violations = validator.validate(input);
if (!violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
}
}
Для этого вообще не требуется никакой пружинной опоры.
Однако Spring Boot предоставляет нам предварительно настроенный экземпляр Валидатора . Мы можем внедрить этот экземпляр в наш сервис и использовать этот экземпляр вместо того, чтобы создавать его вручную:
@Service
class ProgrammaticallyValidatingService {
private Validator validator;
ProgrammaticallyValidatingService(Validator validator) {
this.validator = validator;
}
void validateInputWithInjectedValidator(Input input) {
Set> violations = validator.validate(input);
if (!violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
}
}
Когда эта служба создается Spring, в конструктор автоматически вводится экземпляр валидатора .
Следующий модульный тест доказывает, что оба вышеперечисленных метода работают должным образом:
@ExtendWith(SpringExtension.class)
@SpringBootTest
class ProgrammaticallyValidatingServiceTest {
@Autowired
private ProgrammaticallyValidatingService service;
@Test
void whenInputIsInvalid_thenThrowsException(){
Input input = invalidInput();
assertThrows(ConstraintViolationException.class, () -> {
service.validateInput(input);
});
}
@Test
void givenInjectedValidator_whenInputIsInvalid_thenThrowsException(){
Input input = invalidInput();
assertThrows(ConstraintViolationException.class, () -> {
service.validateInputWithInjectedValidator(input);
});
}
}
Использование групп проверки для проверки объектов по-разному для разных случаев использования
Часто определенные объекты являются общими для разных вариантов использования.
Давайте возьмем, к примеру, типичные операции CRUD: вариант использования “Создать” и вариант использования “Обновить”, скорее всего, будут использовать один и тот же тип объекта в качестве входных данных. Однако могут быть проверки, которые следует запускать при разных обстоятельствах:
- только в случае использования “Создать”,
- только в случае использования “Обновить”, или
- в обоих случаях использования.
Функция проверки компонента, которая позволяет нам реализовывать подобные правила проверки, называется “Группы проверки” .
Мы уже видели, что все аннотации ограничений должны иметь поле группы . Это можно использовать для передачи любых классов, каждый из которых определяет определенную группу проверки, которая должна быть запущена.
Для нашего примера CRUD мы просто определяем два интерфейса маркеров onCreate и Обновление :
interface OnCreate {}
interface OnUpdate {}
Затем мы можем использовать эти интерфейсы маркеров с любой аннотацией ограничения, подобной этой:
class InputWithGroups {
@Null(groups = OnCreate.class)
@NotNull(groups = OnUpdate.class)
private Long id;
// ...
}
Это позволит убедиться, что идентификатор пуст в нашем случае использования “Создать” и что он не пуст в нашем случае использования “Обновить”.
Spring поддерживает группы проверки с аннотацией @Validated :
@Service
@Validated
class ValidatingServiceWithGroups {
@Validated(OnCreate.class)
void validateForCreate(@Valid InputWithGroups input){
// do something
}
@Validated(OnUpdate.class)
void validateForUpdate(@Valid InputWithGroups input){
// do something
}
}
Обратите внимание, что аннотацию @Validated необходимо снова применить ко всему классу. Чтобы определить, какая группа проверки должна быть активной, она также должна быть применена на уровне метода.
Чтобы убедиться, что все вышеперечисленное работает должным образом, мы можем реализовать модульный тест:
@ExtendWith(SpringExtension.class)
@SpringBootTest
class ValidatingServiceWithGroupsTest {
@Autowired
private ValidatingServiceWithGroups service;
@Test
void whenInputIsInvalidForCreate_thenThrowsException() {
InputWithGroups input = validInput();
input.setId(42L);
assertThrows(ConstraintViolationException.class, () -> {
service.validateForCreate(input);
});
}
@Test
void whenInputIsInvalidForUpdate_thenThrowsException() {
InputWithGroups input = validInput();
input.setId(null);
assertThrows(ConstraintViolationException.class, () -> {
service.validateForUpdate(input);
});
}
}
Будьте осторожны с группами проверки!
Использование групп проверки может легко стать анти-шаблоном, поскольку мы смешиваем проблемы. С группами проверки проверяемый объект должен знать правила проверки для всех вариантов использования (групп), в которых он используется. Подробнее об этой теме в моей статье о Анти-шаблонах проверки компонентов .
Возврат Структурированных Ответов На Ошибки
Когда проверка завершается неудачей, мы хотим вернуть клиенту значимое сообщение об ошибке. Чтобы позволить клиенту отображать полезное сообщение об ошибке, мы должны вернуть структуру данных, содержащую сообщение об ошибке для каждой неудачной проверки .
Во-первых, нам нужно определить эту структуру данных. Мы назовем это Ответ на ошибку проверки и он содержит список Объектов нарушения :
public class ValidationErrorResponse {
private List violations = new ArrayList<>();
// ...
}
public class Violation {
private final String fieldName;
private final String message;
// ...
}
Затем мы создаем глобальный Совет по контроллеру который обрабатывает все Ограничение Нарушенияисключение этот пузырь поднимается до уровня контроллера. Для того, чтобы отловить ошибки проверки для тела запроса , мы также будем обрабатывать Методаргументнозначное исключение :
@ControllerAdvice
class ErrorHandlingControllerAdvice {
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
ValidationErrorResponse onConstraintValidationException(
ConstraintViolationException e) {
ValidationErrorResponse error = new ValidationErrorResponse();
for (ConstraintViolation violation : e.getConstraintViolations()) {
error.getViolations().add(
new Violation(violation.getPropertyPath().toString(), violation.getMessage()));
}
return error;
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
ValidationErrorResponse onMethodArgumentNotValidException(
MethodArgumentNotValidException e) {
ValidationErrorResponse error = new ValidationErrorResponse();
for (FieldError fieldError : e.getBindingResult().getFieldErrors()) {
error.getViolations().add(
new Violation(fieldError.getField(), fieldError.getDefaultMessage()));
}
return error;
}
}
То, что мы здесь делаем, – это просто считывание информации о нарушениях из исключений и перевод их в нашу структуру данных ValidationErrorResponse .
Обратите внимание на аннотацию @ControllerAdvice , которая делает методы обработки исключений доступными глобально для всех контроллеров в контексте приложения.
Вывод
В этом руководстве мы рассмотрели все основные функции проверки, которые могут нам понадобиться при создании приложения с помощью Spring Boot.
Если вы хотите испачкать руки в примере кода, загляните в репозиторий github .
Оригинал: “https://dev.to/thombergs/all-you-need-to-know-about-bean-validation-with-spring-boot-3aak”