Во многих веб-приложениях есть формы, на которых пользователь вводит данные (например, форма регистрации на сайте). Почти всегда нужно проводить валидацию этих данных: заполнены ли обязательные поля, записан ли email и телефон в нужном формате и так далее.
В первую очередь валидация выполняется на фронтенде, однако при желании подкованный пользователь может её обойти и послать запрос с невалидными данными. Таким образом бэкенд не может быть уверен в корректности данных, полученных от пользователя. Соответственно необходимо валидировать данные на бэкенде.
В данной статье будет рассмотрена валидация полей ДТО (DTO – Data Transfer Object) с использованием пакета javax.validation.
Создание приложения
Для создания приложения воспользуемся Spring Initializr по адресу https://start.spring.io. Заполним поля group и artifact, а затем добавим в проект зависимости Spring Web и Lombok. Скачаем проект и откроем его в IDE.
Мы разработаем простое приложение, которое будет имитировать сохранение информации о сотрудниках некой организации. Состоять наше приложение будет из следующих частей:
- DTO с данными, которые мы будем валидировать
- Тест контроллера для демонстрации работы
- REST-контроллер, который будет обрабатывать HTTP-запрос и запускать валидацию
ДТО
Создадим в проекте класс EmployeeDto:
import lombok.AllArgsConstructor; import lombok.Data; import javax.validation.constraints.Email; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Past; import javax.validation.constraints.Pattern; import java.util.Date; @Data @AllArgsConstructor public class EmployeeDto { @NotBlank(message = "Необходимо указать имя") private String name; @Email(message = "Email должен быть корректным адресом электронной почты") private String email; @Pattern(regexp = "\\+7[0-9]{10}", message = "Телефонный номер должен начинаться с +7, затем - 10 цифр") private String phone; @Past(message = "Дата приёма на работу не должна быть больше текущей") private Date hireDate; }
Аннотации над классом относятся к библиотеке Lombok. Благодаря @Data будут сгенерированы геттеры и сеттеры для всех полей, а @AllArgsConstructor создаст конструктор, инициализирующий все поля класса.
Аннотации над полями – это основа нашей валидации. Каждая аннотация задаёт своё правило:
@NotBlank: поле обязательно для заполнения.
@Email: значение должно по формату подходить для использования в качестве email.
@Pattern: значение должно удовлетворять регулярному выражению.
@Past: дата/время должно быть в прошлом
REST-контроллер
Создим класс EmployeeController:
import lombok.AllArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import EmployeeDto; import javax.validation.Valid; @RestController public class EmployeeController { @PostMapping("/employees") public ResponseEntity<Void> add(@RequestBody @Valid EmployeeDto dto) { // Сохранение... return ResponseEntity.ok().build(); } }
Это простой контроллер, который обрабатывает запросы, поступающие методом POST на адрес http://localhost:8080/employees.
Аннотация @Valid – ключевой момент: благодаря ей будет запускаться валидация аргумента dto в соответствии с аннотациями @NotBlank, @Email и т.д, которые мы описали в EmployeeDto.
Тест контроллера
Создадим класс EmployeeControllerTest для проверки работы контроллера:
@WebMvcTest(controllers = EmployeeController.class) public class EmployeeControllerTest { @Autowired MockMvc mockMvc; }
Использование аннотации @WebMvcTest над классом позволяет не поднимать весь Spring-контекст, а инициализировать только веб-слой приложения (в частности создать контроллеры). Параметр controllers говорит о том, что нужно создать только указанные контроллеры. Если его не указывать, то будут созданы все контроллеры приложения.
Класс MockMvc, входящий состав в Spring, предоставляет удобные средства для тестирования контроллеров. Используя его можно не поднимать реальный сервер, такой как Tomcat, а тестировать с того момента, где Spring передаёт запрос в наш контроллер. Таким образом для контроллера всё будет выглядеть так, как будто был отправлен и получен реальный HTTP-запрос, но при этом не нужно тратить ресурсы на поднятие сервера.
Добавим тест, в котором будем проверять поведение контроллера, когда в ДТО нет поля name:
@WebMvcTest(controllers = EmployeeController.class) public class EmployeeControllerTest { @Autowired MockMvc mockMvc; @Test @DisplayName("Если нет поля name, то возвращается код 400") void addTest() throws Exception { mockMvc.perform(MockMvcRequestBuilders.post("/employees") .contentType(MediaType.APPLICATION_JSON_UTF8) .content("{\"email\":\"user@server.com\"}")) .andExpect(MockMvcResultMatchers.status().isBadRequest()); } }
В этом тесте отправляется неполная версия EmployeeDto в виде JSON на адрес /employees. В отправляемом JSON нет поля name. А поскольку это поле обязательно для заполнения, то валидация ДТО должна “свалиться” с кодом 400 (Bad Request). Запустите тест – он должен пройти успешно.
Если мы хотим убедиться, что ошибка валидации вызвана именно отсутствием поля name, а не чем-то другим, мы можем модифицировать наш тест:
@Test @DisplayName("Передача объекта без поля name возвращает код 400") void addTest() throws Exception { MvcResult response = mockMvc.perform(MockMvcRequestBuilders.post("/employees") .contentType(MediaType.APPLICATION_JSON_UTF8) .content("{\"email\":\"user@server.com\"}")) .andExpect(MockMvcResultMatchers.status().isBadRequest()) .andReturn(); String message = response.getResolvedException().getMessage(); assertTrue(message.contains("default message [name]")); assertTrue(message.contains("default message [Необходимо указать имя]")); }
Также мы можем предусмотреть ситуацию, когда контроллер ничего не вернёт:
@Test @DisplayName("Передача объекта без поля name возвращает код 400") void addTest() throws Exception { MvcResult response = mockMvc.perform(MockMvcRequestBuilders.post("/employees") .contentType(MediaType.APPLICATION_JSON_UTF8) .content("{\"email\":\"user@server.com\"}")) .andExpect(MockMvcResultMatchers.status().isBadRequest()) .andReturn(); String message = requireNonNull(response.getResolvedException(), "Не получено сообщение от контроллера").getMessage(); assertTrue(message.contains("default message [name]")); assertTrue(message.contains("default message [Необходимо указать имя]")); }
Принудительный запуск валидации
Аннотация @Valid выглядит так, будто одно лишь её наличие запускает валидацию, однако это не совсем так. Убедиться в этом можно, “вручную” вызвав метод контроллера.
Напишем новый тест:
@Test @DisplayName("add бросает исключение, если поле name = null") void manualValidationTest() { EmployeeController controller = new EmployeeController(); EmployeeDto emp = new EmployeeDto(null, "email@server.com", null, null); assertThrows(MethodArgumentNotValidException.class, () -> controller.add(emp)); }
Он “свалится”, поскольку исключение MethodArgumentNotValidException не будет брошено. В чём же причина? Дело в том, что просто добавить аннотации @Valid и @NotBlank – недостаточно. Вся “магия” происходит внутри Spring, а когда мы вызываем метод “вручную”, то Spring не задействован.
Но мы можем принудительно запустить валидацию. Изменим тест:
@Test @DisplayName("Принудительная валидация находит ошибку") void manualValidationTest() { EmployeeDto emp = new EmployeeDto(null, "email@server.com", null, null); Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); Set<ConstraintViolation<EmployeeDto>> violations = validator.validate(emp); ConstraintViolation<EmployeeDto> violation = violations.stream().findFirst().orElseThrow(() -> new RuntimeException("Отсутствует ошибка валидации")); assertEquals("name", violation.getPropertyPath().toString()); assertEquals("Необходимо указать имя", violation.getMessageTemplate()); }
Этот тест пройдёт успешно, а в переменной violations будет один элемент с информацией об ошибке валидации поля name.
Заключение
Рассмотренные варианты валидации – это неполный список того, что умеет пакет javax.validation. Благодаря ему можно быстро настроить валидацию в Spring Boot-приложении и сократить время разработки.
Код приложения из этой статьи можно взять на Github.
Удачной разработки!