В папке DOC sql-скрипты и др. полезные файлы.
Док. (ссылки) для изучения:
- Spring Boot Reference Documentation ;
- Spring Framework 6.1.5 Documentation ;
- Spring Framework 3.2.x Reference Documentation ;
- Getting Started Guides ;
- Developing with Spring Boot ;
Для начала проведем предварительную подготовку (первые 6-ть шагов из предыдущих частей, некоторые пропущены):
Шаг 1. - в файле build.gradle добавим необходимые plugin-ы:
/*
Плагин Spring Boot добавляет необходимые задачи в Gradle
и имеет обширную взаимосвязь с другими plugin-ами.
*/
id 'org.springframework.boot' version '3.1.3'
/*
Менеджер зависимостей позволяет решать проблемы несовместимости
различных версий и модулей Spring-а
*/
id "io.spring.dependency-management" version '1.0.11.RELEASE'
/* Подключим Lombok */
id "io.freefair.lombok" version "8.3"
Шаг 2. - подключаем Spring Boot starter:
/*
Подключим Spring Boot Starter он включает поддержку
авто-конфигурации, логирование и YAML
*/
implementation 'org.springframework.boot:spring-boot-starter'
Шаг 3. - подключаем блок тестирования (Spring Boot Starter Test) (он будет активен на этапе тестирования):
testImplementation 'org.springframework.boot:spring-boot-starter-test'
Шаг 4. - автоматически Gradle создал тестовую зависимость на Junit5 (мы можем использовать как Junit4, так и TestNG):
test {
useJUnitPlatform()
}
Шаг 5. - подключим блок для работы с БД (Spring Boot Starter Data Jpa):
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
Для работы с PostgreSQL подключим и его зависимости:
implementation 'org.postgresql:postgresql'
Шаг 6. - Для использования средств подобных Hibernate ENVERS:
implementation 'org.springframework.data:spring-data-envers'
Шаг 7. - Подключим миграционный фреймворк Liquibase:
implementation 'org.liquibase:liquibase-core'
Шаг 8. - Подключаем Wed - Starter:
implementation 'org.springframework.boot:spring-boot-starter-web'
Шаг 9. - Поскольку сейчас мы начинаем изучение Thymeleaf, то нам нужно его подключить:
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
Хотя мы уже не используем явно *.JSP страницы, мы все же оставим зависимость Jasper-a:
implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'
Для подгрузки зависимостей Validation Starter-а прописываем его в нашем build.gradle:
implementation 'org.springframework.boot:spring-boot-starter-validation'
Если все прошло нормально, то в зависимостях Gradle-а мы увидим validation-api (Hibernate JSR303) см. DOC/GradleDependencies.jpg совсем немного мы затронули данный вопрос в Hibernate lessons part 12 - Practice урок Lesson 47. Если нам не нужны особые способы обработки ошибок при валидации, то обычно встроенных аннотаций из данной спецификации будет достаточно.
Документация см. (ENG):
- Hibernate Validator 8.0.1.Final;
- Validation APIs;
- Пакет javax.validation;
- Java Bean Validation;
- Jakarta Bean Validation specification;
Краткое описание валидации в статьях см.:
В качестве примера:
Шаг 1. - поставим некоторые аннотации над классами нашей модели, например, UserCreateEditDto.java см. комментарии в коде класса;
Шаг 2. - запускаем процесс валидации аннотировав соответствующим образом аргумент UserCreateEditDto переданный в метод контроллера, в нашем случае это метод *.update() из UserController.java:
@PostMapping("/{id}/update")
public String update(@PathVariable("id") Long id, @ModelAttribute @Validated UserCreateEditDto user) {
// ... some code ...
}
Тут мы могли применить либо @Valid из jakarta.validation, либо, как в нашем случае @Validated из org.springframework.validation.annotation. Особенность аннотации @Validated в том, что она к основному функционалу добавляет возможность валидации по группам (хотя с jakarta.validation сие тоже возможно).
В данном случае мы сделали грубую валидацию, без каких либо обработок и подсказок на стороне фронтэнда, что легко проверить запустив приложение и в форме templates/user/user.html умышленно внести ошибку, например, вместо email-a передать бессмысленную строку. Мы получим: Whitelabel Error Page с There was an unexpected error (type=Bad Request, status=400).
См. док.:
Первый пример валидации показал, что использование аннотаций при валидации данных, вроде бы простая процедура. Однако такой грубый вариант не дружественен к пользователю приложения и необходимо такие неверные запросы обрабатывать и подсказывать пользователю, что нужно сделать для исправления ситуации, т.к. это все же его ошибка. Чуть улучшим дружелюбие интерфейса на примере формы регистрации - templates/user/registration.html.
Шаг 1. - Добавим аннотацию @Validated к передаваемому аргументу 'UserCreateEditDto user' метода *.create() нашего контроллера;
Шаг 2. - Туда же в аргументы внедряем новый объект BindingResult из org.springframework.validation см. ниже;
@PostMapping
public String create(@ModelAttribute @Validated UserCreateEditDto user,
BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
// ... some code ...
}
!!! Особенность !!! Как это ни странно, при добавлении BindingResult важно его местоположение в последовательности
передаваемых аргументов метода - сразу после аргумента помеченного как @Validated !!!
В данном случае мы указываем системе, что сами планируем обрабатывать ошибки и объект BindingResult будет их хранить. Естественно мы получая к ним доступ можем взаимодействовать, как с приложением, так и с пользователем см. DOC/BindingResult.txt.
Шаг 3. - В форму регистрации templates/user/registration.html добавляем 'обработчик ошибок' - соответствующий блок отображающий проблемы при валидации введенных данных.
<div th:if="${errors}">
<p style="color: red" th:each="error : ${errors}" th:text="${error.defaultMessage}">Error message</p>
</div>
Запускаем приложение, переходим в форму регистрации, вносим ошибки при заполнении формы и смотрим, что получилось.
См. док.:
Предположим, что написанных за нас и для нас аннотаций из validation пакетов Jakarta и org.springframework.validation.annotation не хватает. Мы можем сами создать интерфейс валидатора:
Шаг 1. - Создадим папку validation, где будем хранить наши файлы ограничений (валидации).
Шаг 2. - В папке validation создадим нашу первую самописную аннотацию для валидации UserInfo.java, в которой должны быть обязательные поля см. ниже:
String message() default "... некое сообщение об ошибке валидации ... "; // Задали выводимое сообщение
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
И аннотации нас самим классом:
@Constraint(validatedBy = { })
@Target(ElementType.TYPE, ...)
@Retention(RetentionPolicy.RUNTIME)
См. более подробные примеры в DOC/ValidationWithSpringBoot/ValidationWithSpringBoot.txt и DOC/ArticleAboutValidation.
Шаг 3. - Создаем сам валидатор, который будет обрабатывать нашу аннотацию UserInfo, это будет UserInfoValidator.java и реализуем метод *.isValid(UserCreateEditDto value, ConstraintValidatorContext context).
Шаг 4. - Применяем нашу аннотацию над классом UserCreateEditDto и удаляем аннотации @NotNull с полей lastname и firstname, т.к. проверку валидности этих переменных будет отрабатывать наш самописный валидатор.
Хороший пример, который описывает наши шаги приведен в DOC/ValidationWithSpringBoot/ValidationWithSpringBoot.txt
Шаг 5. - Запускаем наше приложение, обращаемся к форме регистрации, вносим ошибки при заполнении, и видим результат работы нескольких валидаций в классе UserCreateEditDto и нашего валидатора, в том числе.
См. док.:
Используя аннотацию @Validated см. DOC/InterfaceValidated.txt мы можем управлять группами см. DOC/ValidationWithSpringBoot/ValidationWithSpringBoot.txt, как бы разделяя логику проверки на различные этапы и при различных условиях работы приложения.
Шаг 1. - Зайдем в наш UserController.java, и в методе *.create() в аннотации @Validated добавлю группу Default.class, она запускает все аннотации не использующие группы и предоставленные Spring. Если ее не указать в списке групп, то мы переназначим настройки запускаемых аннотаций (лучше ее оставлять):
@PostMapping
public String create(@ModelAttribute @Validated ({Default.class, CreateAction.class}) UserCreateEditDto user,
BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
}
Шаг 2. - Создадим интерфейс метку CreateAction, группа это обычно класс или интерфейс маркер и никакой программной логики в себе не несет. Наши интерфейсы метки поместим в папку java/spring/oldboy/validation/group. Туда же добавим еще интерфейс UpdateAction. Его мы добавим в аннотацию метода *.update():
@PostMapping("/{id}/update")
public String update(@PathVariable("id") Long id,
@ModelAttribute @Validated({Default.class, UpdateAction.class}) UserCreateEditDto user) {
}
В данном случае мы как бы разделяем проверку на два отдельных направления первая будет активна на этапе создания User, а вторая на этапе обновления его.
Шаг 3. - Добавляем нашу метку (интерфейс маркер) или группу в аннотацию @UserInfo, класса UserCreateEditDto:
@UserInfo(groups = CreateAction.class)
public class UserCreateEditDto {
}
Применив (groups = CreateAction.class), мы как бы говорим, что проверку валидности нужно проводить на этапе создания и только. Если же мы применим (groups = UpdateAction.class), то проверок на этапе создания проводится не будет, только обновления. Это легко проверить изменяя маркерные интерфейсы в параметрах аннотации @UserInfo. При различных параметрах будут активироваться разные сообщения об ошибках валидности, в зависимости от примененного группы - интерфейса маркера.
В уроке 90, мы пытались по своему обработать отклик стандартного Spring валидатора на умышленно внесенных ошибки, мы сделали страницу отображения более информативной для пользователя, показывая ему подсказки связанные с ошибками. Для этого мы использовали объект BindingResult. В принципе это интересный вариант обработки ошибок валидации.
Однако существует другой вариант, когда мы применяем обработчики исключений - ExceptionHandler. Мы также можем пойти двумя путями и в первом случае написать метод обработчик исключений прямо в теле контроллера, например, UserController.
Данный способ более универсальный и часто используется в REST API.
REST (от англ. REpresentational State Transfer — «передача репрезентативного состояния» или «передача „самоописываемого“ состояния») — архитектурный стиль взаимодействия компонентов распределённого приложения в сети. Другими словами, REST - это набор правил того, как программисту организовать написание кода серверного приложения, чтобы все системы легко обменивались данными и приложение можно было масштабировать.
REST представляет собой согласованный набор ограничений, учитываемых при проектировании распределённой гипермедиа- системы. В определённых случаях (интернет-магазины, поисковые системы; прочие системы, основанные на данных) это приводит к повышению производительности и упрощению архитектуры.
Попробуем реализовать наш обработчик исключений валидации:
Шаг 1. - Создадим локальный обработчик ошибок - метод в UserController, и аннотируем наш контроллер @Slf4j:
@ExceptionHandler(Exception.class)
public String handleExceptions(Exception exception, HttpServletRequest request) {
log.error("Failed to return response", exception);
return "error/error500";
}
Теперь наш обработчик ловит исключения и отображает некую страницу с ошибкой.
Шаг 2. - В папке где хранятся наши *.HTML страницы создадим еще папку 'error', где будут храниться страницы отображения ошибок, возвращаемые пользователю при бросках исключений.
Шаг 3. - Создадим страницу отображения в папке 'error' для показа пользователю, в нашем методе *.handleExceptions() она уже прописана как возвращаемая на запрос в случае броска исключения - templates/error/error500.html.
Щаг 4. - Проверим работу обработчика - запустим приложение и обратимся к методу *.update(). Поскольку в него мы не передаем BindingResult, то ошибка валидации при работе этого метода подхватится нашим глобальным обработчиком исключений и отобразится страница - error500.html.
При запуске приложения в режиме DEBUG мы можем наглядно отследить, как происходит работа метода обработчика исключений и какие объекты созданы, какие объекты сгенерировали ошибки, какие ошибки отловлены см. DOC/MyHandleExceptionsWork.jpg Фактически мы снова получили в распоряжение BindingResult завернутый в исключение.
Естественно так будет вести себя наш обработчик только внутри нашего UserController-а, а что если мы хотим подобным образом обрабатывать ошибки валидации во всех контроллерах.
Сделаем это.
Для решения вопроса обработки исключения в процессе валидации поучаемых данных, мы вынесем ранее разработанный метод *.handleExceptions() из нашего контроллера UserController в отдельный класс, чтобы и другие контроллеры могли к нему обращаться минуя прежние неудобства (сам метод закомментируем).
Шаг 1. - Создадим папку 'handler', а в ней создадим класс контроллер-обработчик ошибок - ControllerExceptionHandler.java.
Шаг 2. - Из класса UserController.java перенесем метод *.handleExceptions() в ControllerExceptionHandler.java.
Шаг 3. - Для того чтобы другие контроллеры смогли использовать новый класс обработчик помечаем его аннотацией @ControllerAdvice см.:
@Slf4j
@ControllerAdvice
public class ControllerExceptionHandler {
. . . some method . . .
}
Шаг 4. - Запускаем приложение для тестирования работы обработчика-валидации.
См. док.:
См. официальные Guides:
- Getting Started Guides - Эти руководства, рассчитанные на 15–30 минут, содержат быстрые практические инструкции по созданию «Hello World» для любой задачи разработки с помощью Spring. В большинстве случаев единственными необходимыми требованиями являются JDK и текстовый редактор.
- Topical Guides - Тематические руководства предназначенные для прочтения и понимания за час или меньше, содержит более широкий или субъективный контент, чем руководство по началу работы.
- Tutorials - Эти учебники, рассчитанные на 2–3 часа, обеспечивают более глубокое контекстное изучение тем разработки корпоративных приложений, что позволяет вам подготовиться к внедрению реальных решений.