В папке DOC sql-скрипты и др. полезные файлы.
Док. (ссылки) для изучения:
- SWAGGER DOC (может понадобится прокси);
- Spring Boot Reference Documentation ;
- Spring Framework 6.1.5 Documentation ;
- Spring Framework 3.2.x Reference Documentation ;
- Getting Started Guides ;
- Developing with Spring Boot ;
Для начала проведем предварительную подготовку (подгрузим зависимости в build.gradle):
/*
Плагин 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"
/*
Подключим Spring Boot Starter он включает поддержку
авто-конфигурации, логирование и YAML
*/
implementation 'org.springframework.boot:spring-boot-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
/*
Автоматически Gradle создал тестовую зависимость на Junit5, мы можем
использовать как Junit4, так и TestNG
*/
test {
useJUnitPlatform()
}
/* Подключим блок для работы с БД (Spring Boot Starter Data Jpa) */
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
/* Для работы с PostgreSQL подключим и его зависимости */
implementation 'org.postgresql:postgresql'
implementation 'org.springframework.data:spring-data-envers'
/* Подключим миграционный фреймворк Liquibase */
implementation 'org.liquibase:liquibase-core'
/* Подключаем Wed - Starter */
implementation 'org.springframework.boot:spring-boot-starter-web'
/* Подключим Thymeleaf */
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
/* Подключим валидацию */
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'
В качестве введения в тему REST собрал, на скорую руку, материал ознакомительный и более специфичный на данном этапе:
- DOC/SimpleAboutREST.txt - Самая полезная из возможных статей для ознакомления с принципами REST;
- DOC/ShortAboutREST.txt - Коротко о REST из WIKI и HABR-a;
- DOC/RESTs_Mythology.txt - Очень вдумчивая и подробная статья о REST, есть ссылка на GitHub с книгой по API;
- DOC/REST_SERVICE_ON_JAVA.txt - Пример написания REST сервиса на Java, немного текста и кода;
- DOC/REST_OVER_SPRING_PROBLEMS_URL.txt - Рассмотрена проблематика отображения URL в Spring REST контроллерах;
- DOC/ImportantAspectsOfRESTfulAPI.txt - Рассмотрена методология и идеология создания REST сервисов;
- DOC/RPCvsRESTvsMQ;
- DOC/REST_vs_SOAP;
- DOC/REST_API - Обширный переводной материал с примерами, рассматриваются: REST, SWAGGER, HATEOAS и т.д.;
- DOC/JAX-RS;
- DOC/Java_Kotlin_RestController - Рассмотрен пример взаимодействия Lombok-a, Spring-a и Kotlin при написании REST контроллеров;
Ссылки на исходные материалы содержаться в статьях, можно изучить познавательные комментарии и полезные ссылки не указанные по ходу изложения.
Наше приложение написанное по шагам в прошлых уроках имеет некий недостаток. Если изучить документацию по принципам REST, приведенную выше, и заглянуть в наш контроллер - UserController.java, мы увидим, что использовали только два вида запросов Get и Post. И делали это для всех типов действий CRUD, что противоречит принципам REST. Все взаимодействие нашего приложения с внешним миром происходило по HTTP протоколу, и это хорошо, но возвращались на все запросы HTML страницы с ответами (Response).
Происходило следующее см. DOC/OurPastApplication/OurAppRestrictions.jpg:
- пользователь делал через браузер запрос;
- запрос подхватывал интегрированный (embedded) в приложение TomCat;
- сервер TomCat передавал запрос на DispatcherServlet;
- DispatcherServlet перенаправлял запрос на контроллеры;
И это работало, наше приложение - 'сервис' взаимодействовало с пользователем и только. Исходя из материалов по REST, предполагается, что наш сервис будет взаимодействовать не только с живым пользователем, но и с другими сервисами, а значит наше приложение-сервис должно уметь работать с HTTP запросами всех типов и возвращать информацию например в JSON, XML и т.п. по 'желанию' стороннего сервиса см. потрясающую статью - DOC/SimpleAboutREST.txt.
Реализуем. Трансформация нашего приложения выглядит так, см. DOC/OurPastApplication/ReconstructionAppToREST.jpg. Теперь наши контроллеры будут выстроены исходя из принципов REST API, но это те же HTTP запросы, и ответы будут обрабатываться на стороне пользователя, например, в браузере по средствам JavaScript. И тогда схема приложения будет см. DOC/OurPastApplication/REST_Interaction_Diagram.jpg
- Шаг. 1 - Мы явным образом должны отключить ModelAndView (установить в null), тогда резолверы занимающиеся отрисовкой Model будут дезактивированы и мы сможем вернуть в методе данные 'так как они есть'. Это делается расстановкой аннотации @ResponseBody над методами (или классом).
- Шаг. 2 - Создаем папку rest в папке http нашего приложения, туда мы поместим наш первый Rest контроллер.
- Шаг. 3 - Создаем наш первый Rest контроллер на базе UserController, т.е. он будет реализовывать тоже CRUD функционал, но с 'REST размахом'.
- Шаг. 4. - Реализуем простой метод *.simpleStringResponse(), в нем уже нет Model, а значит и загрузки атрибутов для ответа на запрос, в идеале - те данные которые возвращает метод будут возвращены пользователю, тут это будет обычный String.
Запускаем наше приложение SpringAppRunner.java и в браузере вводим http://localhost:8080/api/v1/users/string, ответом будет простая строка - 'String response', Content type определен как text, см. DOC/OurRestAppStepByStep/findAllStringResponse.jpg браузер сам преобразовал возвращенные данные в удобочитаемый вид, и HTML код страницы прост:
<html>
<head></head>
<body>String response</body>
</html>
И так мы аннотировали метод @ResponseBody и он возвращает String или обычный текст, что мы и получили. Если же мы захотим вернуть объект, то клиент запросивший данные скорее всего получит JSON. Проверим это.
- Шаг. 5 - Реализуем метод *.findAll() возвращающий PagePaginationResponse, т.е. список User-ов, в метод передавать ничего не будем, т.е. и фильтрация и пагинация будут - 'by default'. Запускаем приложение и обращаемся к: http://localhost:8080/api/v1/users
На экране мы видим JSON ответ см. DOC/OurRestAppStepByStep/http_localhost_8080_api_v1_users.jpg, и конечно Content type: JSON, что мы и видим на экране в HTML кодировке:
<html>
<head><meta name="color-scheme" content="light dark"></head>
<body><pre style="word-wrap: break-word; white-space: pre-wrap;">
{"content":[{"id":1,"username":"[email protected]","birthDate":"1990-01-10","firstname":"Ivan",
"lastname":"Ivanov","role":"USER","company":{"id":2,"name":"Meta"}},
/* ... полный список записей из таблицы users БД в формате JSON ... */
{"id":9,"username":"[email protected]","birthDate":"2013-07-25","firstname":"Lakdi",
"lastname":"Karib","role":"USER","company":{"id":1,"name":"Google"}}],
"metadata":{"page":0,"size":20,"totalElements":9}}
</pre>
</body>
</html>
Естественно мы можем управлять Content-type и при запросе (request) и при ответе (response), для этого в наши аннотации над методами, например, @GetMapping мы передаем параметры, в нашем случае - produces = MediaType.APPLICATION_JSON_VALUE, это вариант управления ответом (или produces), получаемые данные (или consumes).
См. док.:
Переносим оставшиеся в UserController.java методы в UserRestController.java и преобразуем в полноценные REST методы.
- Шаг. 1 - При копировании мы могли сразу опустить (или удалить после вставки) метод *.registration(), т.к. основная цель REST контроллера обмен данными между сервисами. Для регистрации user-ов у нас есть обычные контроллеры, они никуда не делись.
- Шаг. 2 - Корректируем метод *.findById(), удаляем модель, лишние аддеры в модель, наш метод будет возвращать UserReadDto или в случае отсутствия записи с предложенной ID просто вернем 404 статус (без изысков).
- Шаг. 3 - Метод *.create() возвращает просто созданную в БД запись или UserReadDto. Из метода полностью удалям BindingResult и RedirectAttributes (мы никуда не перенаправляем ответ, а возвращаем данные при удачном их создании).
Особенности метода в том, что данные в него придут НЕ с внешней формы, а в теле запроса с внешнего сервиса в формате JSON. Для этого в аннотацию @PostMapping метода передаем параметр - consumes = MediaType.APPLICATION_JSON_VALUE, т.е. явно указываем, что 'ожидаем запрос с JSON в теле'. Параметр самого метода также аннотируем, как @RequestBody.
- Шаг. 4 - Корректируем метод *.update(), теперь он как и положено аннотирован @PutMapping см. DOC/REST_API/6_Rest_API_Best_Practices.txt или DOC/ShortAboutREST.txt. Аргумент метода user получил аннотацию @RequestBody, как и в предыдущем методе, т.е. запрос на обновление будет приходить в теле запроса в JSON формате с какого либо внешнего сервиса.
Тут мы снова никуда не перенаправляем нашего пользователя (или сервис обратившийся к нашему приложению), а возвращаем данные 'как есть' в формате UserReadDto. Если запрос нужно как-то преобразовать в некий удобочитаемый формат, то это происходит на стороне клиента (т.е. того кто сделал запрос)
- Шаг. 5 - Изменяем метод (пишем с нуля, кому как нравится) *.delete(). Он получает, как и положено аннотацию @DeleteMapping в параметры аннотации идет ID удаляемой записи. В случае удачной операции мы ничего не возвращаем, поэтому метод становится void и получает аннотацию @ResponseStatus(HttpStatus.NO_CONTENT). Естественно если запрошенного ID в базе не существует мы возвращаем NOT_FOUND или 404 статус.
Мы явно не аннотировали наши методы, как @ResponseBody, сам класс не пометили как @Controller. Зато мы применили аннотацию @RestController объединяющую в себе эти две аннотации важные для нас (можно глянуть внутреннюю структуру этой аннотации для наглядности).
Мы уже писали обработчик ошибок для наших обычных контроллеров ControllerExceptionHandler.java, немного изменим его, добавив параметр в аннотацию @ControllerAdvice(basePackages = "spring.oldboy.http.controller").
Шаг. 1 - Создадим обработчик ошибок нашего REST контроллера. В папку spring/oldboy/http/handler добавим наш обработчик ошибок REST контролера - RestControllerExceptionHandler.java.
Особенности данного обработчика это аннотация @RestControllerAdvice, тут мы явно видим намек на то, что идет обработка REST методов, в параметрах явно указываем связь с папкой 'rest' - basePackages = "spring.oldboy.http.rest"
См. док. (код):
- Пакет org.springframework.web.bind.annotation (doc) ;
- Пакте org.springframework.web.bind.annotation (на GitHub) ;
При работе с ранними версиями Spring применялись другие библиотеки и прописывались другие зависимости, например:
implementation 'io.springfox:springfox-swagger-ui:3.0.0'
или
implementation "org.springdoc:springdoc-openapi-ui:1.7.0"
Изучая примеры 2-х летней давности понял, что с текущей версией Spring Boot, при подключении их, могут возникнуть определенные затруднения, изучив вопрос на stackoverflow понял, как новичку, эти горы мне не взять. Как я понял, при наличии Spring Security внедрение Swagger-a будет такой же веселой, но это позже. Поэтому пока, подключим в Spring зависимость для Swagger немного по-другому (build.gradle):
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:${versions.springdoc}"
Пропишем версию библиотеки в (version.gradle):
ext {
versions = [
'testcontainers': '1.17.6',
'postgres': '42.5.4',
'springdoc': '2.3.0'
]
}
Судя по подключенным зависимостям мы подключили те же библиотеки (или очень похожие), что были бы в первых двух случаях.
Основное назначение Swagger это автоматическая генерация документации нашего Rest API. После подключения Swagger-а легко может предоставить пользователю схему приложения: методы, конечные точки, сущности и т.д. все, что касается нашего приложения. Причем, при внесении нами изменений в структуру методов и т.п. нашего приложения Swagger может 'на лету' дополнить/изменить документацию.
Так же, Swagger имеет свой пользовательский интерфейс - UI, который позволяем нам взаимодействовать с нашим приложением посредствам его REST методов. Это позволяет, в нашем 'web-сервисе', провести грубое ручное тестирование всех наших методов.
Особенность подключенного Swagger-a в том, что он реализован, как spring boot starter. Это позволяем управлять настройками Swagger-а через наш application.yml используя префикс "springdoc".
Для получения описание нашего приложения Swagger-ом (наше приложение должно быть запущено!), нам нужно обратиться по - http://localhost:8080/v3/api-docs. Тут мы получим JSON одной строкой, скопируем в созданный нами файл - resources/test.json (применим к файлу Ctrl+Alt+Shift+L) и получим удобочитаемый JSON файл. К интерфейсу Swagger-a обратимся по - http://localhost:8080/swagger-ui/index.html
Теперь мы имеем наглядную схему API нашего приложения resources/test.json, подробно описаны методы, их входные и выходные параметры, наша модель и т.д. В web-интерфейсе нам доступны методы приложения и мы можем делать те запросы для которых они были написаны. Т.е. мы можем попробовать в действии запросы GET, PUT, DELETE, POST и т.д. При этом, можем передавать и json формат в тех запросах, где это нужно и сразу видеть результаты, и Request URL и Response Body и код ответа от сервера и Response Headers. Мы так же можем специально передавать ошибки и видеть результаты.
Особенность нашего (и не только) REST контроллера в том, что он возвращает данные 'как есть', это удобно если мы пытаемся загрузить на сервер или считать с сервера 'не текстовые данные' - фото, видео, и т.д. Реализуем на наших *.HTML страницах данный функционал:
-
сначала загрузим аватарку для user-a;
-
затем отобразим ее на странице user-a;
-
Шаг. 1 - Создадим в таблицах users и users_aud поля для ссылок на загруженные картинки. Для этого мы создаем файл - resources/db/changelog/db.changelog-3.0.sql в котором прописываем скрипты для изменения таблиц. Мы не храним сами картинки в БД, а только ссылки на них. Реальные картинки положим в корень нашего приложения, а в сети они могут храниться, как на площадях хостинг провайдера, так и в облаке.
-
Шаг. 2 - Добавляем наш новый resources/db/changelog/db.changelog-3.0.sql в db.changelog-master.yaml. Запускаем приложение и проверяем, внедрились ли наши изменения в таблицы БД:
INFO 9848 --- [main] liquibase.database : Set default schema name to public INFO 9848 --- [main] liquibase.changelog : Reading from public.databasechangelog INFO 9848 --- [main] liquibase.lockservice : Successfully acquired change log lock INFO 9848 --- [main] liquibase.command : Using deploymentId: 9106388316 INFO 9848 --- [main] liquibase.changelog : Reading from public.databasechangelog Running Changeset: db/changelog/db.changelog-3.0.sql::1::oldboy INFO 9848 --- [main] liquibase.changelog : Custom SQL executed INFO 9848 --- [main] liquibase.changelog : ChangeSet db/changelog/db.changelog-3.0.sql::1::oldboy ran successfully in 18ms Running Changeset: db/changelog/db.changelog-3.0.sql::2::oldboy INFO 9848 --- [main] liquibase.changelog : Custom SQL executed INFO 9848 --- [main] liquibase.changelog : ChangeSet db/changelog/db.changelog-3.0.sql::2::oldboy ran successfully in 9ms INFO 9848 --- [main] liquibase.util : UPDATE SUMMARY INFO 9848 --- [main] liquibase.util : Run: 2 INFO 9848 --- [main] liquibase.util : Previously run: 12 INFO 9848 --- [main] liquibase.util : Filtered out: 0 INFO 9848 --- [main] liquibase.util : ------------------------------- INFO 9848 --- [main] liquibase.util : Total change sets: 14 INFO 9848 --- [main] liquibase.util : Update summary generated INFO 9848 --- [main] liquibase.command : Update command completed successfully. Liquibase: Update has been successful. Rows affected: 2 INFO 9848 --- [main] liquibase.lockservice : Successfully released change log lock INFO 9848 --- [main] liquibase.command : Command execution complete
-
Шаг. 3 - Создадим сервис для загрузки-сохранения картинок на жесткий диск нашего компьютера см. комментарии в spring/oldboy/service/ImageService.java
-
Шаг. 4 - В форму регистрации - resources/templates/user/registration.html, добавим кнопку загрузить аватарку и поменяем атрибут в enctype="multipart/form-data".
-
Шаг. 5 - В форму отображения user-ов так же добавляем возможность отображать и изменять аватар, это файл - resources/templates/user/user.html. В нем нужно провести те же изменения, что и на шаге 4, т.е. добавить атрибут enctype="multipart/form-data"
-
Шаг. 6 - В нашем обычном контроллере UserController.java в методах *.create() и *.update() мы очень удачно используем один и тот же DTO - UserCreateEditDto.java, который мы немного подправим. Для загрузки и отображения img - аватарки, нам нужно еще поле. В org.springframework.web.multipart есть специальный класс реализующий MultipartFile, его и добавляем. Имя поля 'image' в UserCreateEditDto:
@Value @FieldNameConstants @UserInfo(groups = CreateAction.class) public class UserCreateEditDto { /* ... другие поля/other fields ... */ MultipartFile image; }
Должно совпадать с именем переменной 'name' в теге 'input' формы:
<label for="image">Image:
<input id="image" type="file" name="image">
</label><br>
- Шаг. 8 - Добавим в нашу сущность User - поле String image;
- Шаг. 9 - Необходимо отредактировать наш преобразователь сущностей или маппер - UserCreateEditMapper.java. Поскольку у нас, в БД, могут быть записи без аватарок, то желательно это учесть, как в самом методе, так и в формах. Нам понадобится 'Кот Шредингера' - Optional, см. код и комментарии UserCreateEditMapper.
- Шаг. 10 - Редактируем методы на уровне сервисов, чтобы иметь возможность загрузить картинку-аватарку. В классе UserService.java корректируем методы *.create() и *.update() см. комментарии в коде методов.
Запускаем, проверяем.
См. док.:
В предыдущем примере мы научились 'загружать файл-картинку' в таблицу users нашей БД, конечно только ссылку на нее. Теперь нам хочется отображать картинку-аватар в 'профиле пользователя', т.е. на странице отображения данных о нем.
- Шаг. 1 - Начнем с ImageService, реализуем метод *.getAvatar() для чтения (получения) уже загруженной (установленной) картинки у user-a.
Мы помним, что при получении HTML страницы происходит не один запрос, а такое их количество, которое позволяет загрузить все содержимое страницы, т.е. ссылки на картинки, файлы css и т.п. Значит на слое сервисов нам нужен метод позволяющий извлечь картинку из БД. У нас в записи пользователя хранится только название картинки, значит нужно получить ее.
-
Шаг. 2 - В классе UserService добавляем метод извлекающий картинку из БД - *.findAvatar(). К этому методу мы будем обращаться с уровня контроллеров, но на этот раз из UserRestController-а.
-
Шаг. 3 - В UserReadDto добавим поле String image, чтобы можно было использовать его в HTML форме.
-
Шаг. 4 - В UserReadMapper добавим object.getImage(), в метод *.map().
-
Шаг. 5 - В REST контроллере UserRestController создаем метод для поиска аватарок - *.findAvatar() с окончательным endpoint-ом: /api/v1/users/{id}/avatar, см. аннотации над классом @RequestMapping("/api/v1/users") и методом @GetMapping(value = "/{id}/avatar", . . .). К нему мы и будем обращаться когда захотим получить картинку - аватарку.
-
Шаг. 6 - На странице отображения resources/templates/user/user.html добавляем блок с отображением картинки:
<div th:if="${user.image}"> <img th:src="@{/api/v1/users/{userId}/avatar(userId=${user.id})}" alt="User image"> </div>
Теперь, в случае, если user имеет картинку она будет динамически отображаться на его странице. При этом запрос на поиск аватарки пойдет не в UserController, а в UserRestController, т.к. запрос из блока картинки идет на адрес (endpoint) - /api/v1/users/{userId}/avatar. Если же у user-a вовсе нет картинки, то блок с аватаркой не активируется, th:if - false.
Запускаем, проверяем.
Для демонстрации кода с методами возвращающими ResponseEntity создадим отдельный контроллер - UserRestControllerV2, в котором реализуем только 4-и метода с использованием ResponseEntity
В качестве ознакомительной документации см. DOC/ResponseEntity :
- Достоинства и недостатки ResponseEntity - DOC/ResponseEntity/ResponseEntityExploring.txt ;
- Использование Spring ResponseEntity для управления HTTP-ответом - DOC/ResponseEntity/ResponseEntityExample.txt ;
- Описание класса ResponseEntity (офф. док.) - DOC/ResponseEntity/ResponseEntityClass.txt ;
- Интерфейс ResponseEntity.HeadersBuilder (офф. док.) - DOC/ResponseEntity/ResponseEntity.HeadersBuilder.txt ;
- Интерфейс ResponseEntity.BodyBuilder (офф. док.) - DOC/ResponseEntity/ResponseEntity.BodyBuilder.txt ;
Проверить работоспособность методов в UserRestControllerV2 можно, как через Swagger, так и через HTTP запрос (для POST и GET методов) см. комментарии к методам в самом классе.
См. (при запущенном приложении-сервисе): http://localhost:8080/swagger-ui/index.html
См. официальные Guides:
- Getting Started Guides - Эти руководства, рассчитанные на 15–30 минут, содержат быстрые практические инструкции по созданию «Hello World» для любой задачи разработки с помощью Spring. В большинстве случаев единственными необходимыми требованиями являются JDK и текстовый редактор.
- Topical Guides - Тематические руководства предназначенные для прочтения и понимания за час или меньше, содержит более широкий или субъективный контент, чем руководство по началу работы.
- Tutorials - Эти учебники, рассчитанные на 2–3 часа, обеспечивают более глубокое контекстное изучение тем разработки корпоративных приложений, что позволяет вам подготовиться к внедрению реальных решений.