Skip to content

Latest commit

 

History

History

Spring_part_19

Spring Boot lessons part 19 - REST

В папке DOC sql-скрипты и др. полезные файлы.

Док. (ссылки) для изучения:

  • SWAGGER DOC (может понадобится прокси);


Для начала проведем предварительную подготовку (подгрузим зависимости в 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 - Введение.

В качестве введения в тему REST собрал, на скорую руку, материал ознакомительный и более специфичный на данном этапе:

Ссылки на исходные материалы содержаться в статьях, можно изучить познавательные комментарии и полезные ссылки не указанные по ходу изложения.


Lesson 95 - Практика ч.1 - первый 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. Проверим это.

На экране мы видим 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).

См. док.:


Lesson 96 - Практика ч.2 - расширенный REST контроллер и его CRUD методы.

Переносим оставшиеся в 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 объединяющую в себе эти две аннотации важные для нас (можно глянуть внутреннюю структуру этой аннотации для наглядности).


Lesson 97 - Практика ч.3 - обработчик ошибок для REST контроллеров.

Мы уже писали обработчик ошибок для наших обычных контроллеров 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"

См. док. (код):


Lesson 98 - Ручное тестирование REST методов, SWAGGER API DOCs.

При работе с ранними версиями 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. Мы так же можем специально передавать ошибки и видеть результаты.


Lesson 99 - Upload-image - 'загрузка картинок' в таблицу users БД.

Особенность нашего (и не только) 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() см. комментарии в коде методов.

Запускаем, проверяем.

См. док.:


Lesson 100 - Get-image - отображение картинок в 'профиле user-ов'.

В предыдущем примере мы научились 'загружать файл-картинку' в таблицу users нашей БД, конечно только ссылку на нее. Теперь нам хочется отображать картинку-аватар в 'профиле пользователя', т.е. на странице отображения данных о нем.

  • Шаг. 1 - Начнем с ImageService, реализуем метод *.getAvatar() для чтения (получения) уже загруженной (установленной) картинки у user-a.

Мы помним, что при получении HTML страницы происходит не один запрос, а такое их количество, которое позволяет загрузить все содержимое страницы, т.е. ссылки на картинки, файлы css и т.п. Значит на слое сервисов нам нужен метод позволяющий извлечь картинку из БД. У нас в записи пользователя хранится только название картинки, значит нужно получить ее.

Теперь, в случае, если user имеет картинку она будет динамически отображаться на его странице. При этом запрос на поиск аватарки пойдет не в UserController, а в UserRestController, т.к. запрос из блока картинки идет на адрес (endpoint) - /api/v1/users/{userId}/avatar. Если же у user-a вовсе нет картинки, то блок с аватаркой не активируется, th:if - false.

Запускаем, проверяем.


Lesson 101 - Использование ResponseEntity в методах контроллеров.

Для демонстрации кода с методами возвращающими ResponseEntity создадим отдельный контроллер - UserRestControllerV2, в котором реализуем только 4-и метода с использованием ResponseEntity

В качестве ознакомительной документации см. DOC/ResponseEntity :

Проверить работоспособность методов в 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 часа, обеспечивают более глубокое контекстное изучение тем разработки корпоративных приложений, что позволяет вам подготовиться к внедрению реальных решений.