Сборка Java приложения в Docker

В этом уроке соберем простейшее Spring Boot приложение в Docker-образ (Docker image), запустим его и загрузим на Docker Hub.

Его структура приведена ниже. В ней ничего особенного за исключением того, что в корне проекта добавлен файл с именем Dockerfile.

В контроллере один простой обработчик, который возвращает строку Hello World!!!.

package com.SpringBootApps.simpleSpringBootApp.controller; import org.springframework.web.bind.annotation.GetMapping; @RestController public class MainController { @GetMapping(“/helloWorld”) public String helloWorld() { return “Hello world!!!”; } }

Настройка сборки

Сборка Docker Image настраивается с помощью файла под названием Dockerfile, который нужно создать в корне проекта — как показано в структуре выше.

Итак. В прошлых уроках было упомянуто, что всё, что нужно для запуска приложения докером должно уже быть в его Docker Image, чтобы не пришлось развертывать приложение и устанавливать кучу всего по отдельности.

Откуда же внутри образа (docker image), который получается в результате сборки, берётся всё необходимое ПО: то, что будет запускать наше приложение (например, JDK), то, что будет скачивать зависимости (например, Maven) и т.д.?

Всё просто. Оно есть в интернете и когда производиться команда сборки приложения происходит скачивание его из интернета и помещение скачанного ПО в результирующий Docker Image.

Также при сборке приложения Docker выполняет его настройку — например, загружает зависимости с помощью заранее скачанного для этого ПО (например, Maven). То есть Docker сначала скачивает Maven и сразу использует его для загрузки зависимостей в собираемый образ.

А при запуске уже собранного Docker-образа Docker использует то ПО, которое было включено в образ, для запуска самого приложения — например, берёт из скачанного JDK утилиту java и с её помощью запускает Spring Boot-приложение.

То какое ПО будет скачиваться в результирующий Docker Image, то как им будет настраиваться или запускаться приложение внутри Docker Image как раз настраивается в Dockerfile.


ПО для сборки с сайта Docker Hub.

Итак, из всех программ, которые нам понадобятся для сборки и запуска приложения, в Dockerfile нужно указать Mavenдля загрузки зависимостей и сборки JAR-файла, а также JDKдля запуска этого собранного файла.

Их можно найти на сайте Docker Hub. Там можно найти мавен репозиторий

и в тегах выбрать подходящую версию для нашего приложения.

 Таким же образом можем найти репозиторий jdk.

И в его тегах тоже можно поискать подходящую версию jdk.

Очевидно, что ПО скачиваемое в Docker Image нашего спринг приложения тоже является Docker Image.


Настройка Dockerfile.

Сборка Docker Image происходит посредством последовательного выполнения команд прописанных в Dockerfile. Рассмотрим содержимое Dockerfile спринг приложения, которое мы будем собирать в Docker Image.

#Решетка в Dockerfile это комментарий. #Коммандой FROM указывается программа из Docker Hub, #которая будет использоваться для сборки Image. #Maven – программа из Docker Hub, #после двоеточия – ее версия. FROM maven:3.8.5-openjdk-17 as maven_build #WORKDIR – устанавливаем рабочую директорию внутри #нашего будущего Image. То есть внутри image будет #папка application которая будет восприниматься всеми #дальнейшими командами ниже как рабочая директория. WORKDIR /application #COPY – производится копирование всего из директории #на компьютере(точка значит из директории в которой #находиться Dockerfile, а он находиться в корневой #папке спринг проекта) в /application директорию #внутри Docker Image COPY . /application #То есть данном этапе содержимое нашего #проекта simpleSpringBootApp находиться #в папке /application внутри Docker Image. # Командой mvn clean package запускаем сборку # приложения в jar. Результирующий jar будет # в папке /application/target внутри image. RUN mvn -f /application/pom.xml clean package # RUN mvn -f /application/pom.xml clean package # После run у нас создался первый Docker Image # co spring boot проектом внутри него и jar файлом # в папке target. Это промежуточный Docker Image, #он будет использован для создания результирующего #Docker Image, который будет запускаться командой #docker container … и загружаться на Docker Hub. #Указываем jdk из Docker Hub с помощью которого будет #запускаться созданный jar. FROM openjdk:17 #Указываем порт на котором будет запускаться #результирующий Image в Docker Hub. #И да это порт докер контейнера, а не того что на компьютере. #Порт на нашем компьютере с помощью которого происходить #подключение к докер контейнеру по порту указанному #в команде ниже мы будем указывать в команде #запуска результирующего Docker Image. EXPOSE 8080 #Первому Docker Image было дано имя maven build (сверху #можно увидеть as maven_build). Теперь мы можем #использовать первый Docker Image для создания #результирующего Docker Image. С помощью #--from=maven_build мы обращаемся к предыдущему #промежуточному Docker Image таким образом мы копируем #из него jar в папке /application/target #в результирующий Docker Image с новым именем app.jar COPY --from=maven_build /application/target/*.jar app.jar #Как уже было сказано знакомыми нам командами пакета jdk, #который будет скачан из интернета при сборке, #запускаеться итоговый app.jar внутри результирующего #Docker Image. То есть видим ниже команду #java -jar app.jar для запуска #нашего spring boot приложения. ENTRYPOINT [“java”,”-jar”,”app.jar”] #Данная команда будет выполняться внутри #сформированного результирующего Docker Image, а запуск #самого Docker Image извне будет выполняться одной #и той же ранее упомянутой командой docker container…

Сборка приложения в Docker Image

Для начала в консоли перейдем в корневую папку спринг проекта, который мы собираемся собирать в Docker Image и собственно соберем его в Docker Image с помощью комманды:

docker build -t michaelshadrin/simple-spring-boot-app:version1

Здесь michaelshadrin/simple-spring-boot-app это название репозитория, который будет создан на сайте Docker Hub если мы захотим загрузить туда получившийся в результате этой команды Docker образ. Важно уточнить, что перед слешем в названии репозитория всегда указывается username аккаунта Docker Hub.

version1 это имя версии приложения собираемого в Docker Image и если загрузить получившийся Docker Image на Docker Hub, то в разделе Tags в репозитории michaelshadrin/simple-spring-boot-app можно будет увидеть Docker Image с названием version1.

Search Icon

Процесс сборки может быть довольно долгим, так как в процессе сборки скачиваются jdk, maven, зависимости и другое.


Запуск Docker Image.

Давайте запустим получившийся Docker Image. Важно отметить, что перед этим необходимо включить на компьютере приложение Docker Desktop ссылка для скачивания которого давалась ранее.

Образ запускается уже знакомой нам командой — docker container run, с помощью которой можно запустить любой Docker Image.

Выполним ее для запуска нашего Docker Image:

В команде указывается пара портов -p 8081:8080. Первый порт – это порт компьютера на котором мы запускаем Docker Image, второй – это порт внутри контейнера, указанный в Dockerfile, на котором работает наше приложение.

Docker-контейнер можно представить как лёгкую виртуальную машину. У него, как и у обычного компьютера, есть свои порты. Команда -p пробрасывает порт с компьютера на нужный порт внутри контейнера, чтобы мы могли получить доступ к приложению извне. Подробнее о Docker-контейнере будет ниже.

-d пишется в команде просто чтобы запущенный Docker Image не был привязан к консоли в которой он запускается. То есть, чтобы мы могли дальше использовать эту консоль после запуска Docker Image.

После указания портов очевидно указывается название репозитория и имя конкретного Docker Image в нем, то есть имя версии.

Итак, Docker Image запущен, а значит и Spring boot приложение в нем тоже. Давайте обратимся к обработчику helloWorld этого приложения через порт компьютера 8081, который мы указывали в команде запуска Docker Image.

Как видим, обработчик успешно вернул Hello World!!!, что значит, что сборка приложения в Docker Image и запуск этого Docker Image прошли успешно.


Запуск Image. Контейнеры

Работающий, запущенный Docker Image называется контейнером. Запущенных, одновременно работающих докер контейнеров может быть много. Чтобы несколько Docker Image работали одновременно их нужно запускать на разных портах компьютера.

Давайте запустим три контейнера одного и того же Docker Image на порте 8081, на порте 8082 и на порте 8083.

И ко всем можно будет иметь доступ через localhost по соответствующему порту.

С помощью команды docker container list можно просмотреть все работающие контейнеры.

В результате этой команды можно увидеть разную информацию о контейнере, в том числе id запущенного контейнера. С помощью команды docker container stop можно остановить контейнер по этому id. Не обязательно писать id контейнера полностью, можно написать лишь его первые несколько символов.

Как видим, теперь работают только два контейнера, один мы остановили.


Загрузка Image на Docker Hub

Можно загрузить Docker Image, который мы собрали в Docker Hub. Сделать это можно через консоль посредством команды или через графический интерфейс приложения Docker Desktop. Сделаем это через Docker Desktop.

В приложении в разделе Images можно увидеть Image, который мы собирали. Чтобы загрузить его на Docker Hub достаточно лишь нажать на три точки и выбрать push. Начнется загрузка.

После загрузки в аккаунте с именем michaelshadrin (то что указывается до слеша в названии репозитория) должен появиться репозиторий michaelshadrin/simple-spring-boot-app, который будет содержать Image, который загружался.

В Tags этого репозитория видим image с названием version1.


О пользе Docker

Search Icon

Еще раз о преимуществах докера.

Представим себе ситуацию когда, например, вебсайт создают два человекафронтенд часть один человек, а бэкенд часть другой. Фронтендер написал свою фронтенд часть и хочет протестировать ее как часть полноценного вебсайта. Для этого ему нужна вторая часть вебсайта – бэкенд. Тогда фронтендер может попросить у бэкендера image бэкенда, чтобы запускать этот image у себя на компьютере командой docker container run для тестирования взаимодействия разработанного фронта с беком. При этом фронтендер не занимаеться муторным развертыванием этого бэкенда у себя на компьютере, он может даже не знать на каком языке бэкендер его написал.

Как видим, докер очень удобен при разработке приложений в команде.

Вообще всё может быть намного круче, приложение может состоять не из двух частей, а из множества микросервисов и каждый из них можно упаковать в отдельный image и всех их запускать одинаковой командой docker container run.

Docker Hub: назначение и использование

Для начала работы с Docker необходимо его скачать и установить на компьютер по ссылке:

https://www.docker.com/products/docker-desktop/

Также необходимо создать аккаунт на сайте Docker Hub по ссылке:

https://hub.docker.com/signup.

Там всё довольно стандартно и интуитивно, поэтому не будем заострять внимание.


Что такое Docker Hub, Repository, Tag?

Итак, как мы поняли, Docker image – это итоговый файл, который получается в результате сборки приложения с помощью Docker.

Этот файл содержит в себе приложение и все необходимые технологии для его запуска.

Когда разработчик собрал докером приложение, он может загрузить получившийся Docker image в интернет на сайт Docker Hub.

Допустим два приложения было собрано в Docker Image и загружено на Docker Hub. Тогда их можно увидеть на этом сайте:

Теперь если какой-либо человек захочет запустить один из этих Docker image со своего докер аккаунта на своем компьютере командой:

docker container run -d -p 8081:8080 michaelshadrin/ecommerce-backend-app-docker:version1

то даже если на его компьютере нет запускаемого этой командой Docker Image, произойдет скачивание этого Docker Image из интернета

и после этого произойдет запуск этого Docker Image. Можно увидеть, что происходит Pulling from michaelshadrin/ecommerce-backend-app-docker, то есть скачивание ecommerce-backend-app-docker. Согласитесь довольно удобный механизм.


Репозитории, теги на сайте Docker Hub

Выше на картинке можно было увидеть два загруженных на сайт приложения собранных в Docker Image.

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

Называются они так, потому что каждый репозиторий хранит в себе не один Docker Image приложения, а много Docker Image разных версий одного и того же приложения.

То есть если мы нажмем на один из репозиториев на сайте:

то в разделе теги можно увидеть много Docker Image, которые являются разными версиями одного и того же приложения. То есть, ясное дело, разработчик по ходу разработки приложения будет собирать его в Docker Image не один раз, приложение будет изменяться и собирать новую его версию в Docker Image и загружать ее на сайт нужно будет не раз и все версии одного и того же приложения будут храниться в конкретном Docker Repositiry.

Еще раз, конкретное приложение связано с конкретным Docker Repository для него. У нас было два репозитория для двух разных приложений.

Что такое Docker и зачем он нужен?

С помощью докер можно запускать любые приложения реализованные любыми технологиями на любых языках программирования одной и той же командой.

Вот такой:

docker container run -d -p 8081:8080 <путь_к_docker_image>

Ее содержимое разберем позже.

Для того чтобы запустить приложение этой командой его нужно собрать в Docker Image.

Один раз собранное докером приложение в Docker image можно запустить даже на компьютере где не установлены различные необходимые программы, зависимости проекта, языки программирования на которых реализовано приложение.

То есть раньше приложение необходимо было развертывать. Нужно было установить все необходимые технологии для работы приложения.

Example

Например, для запуска spring boot приложения необходимы:

  • jdk
  • maven
  • зависимости прописанные в pom.

Приходилось отдельно скачивать jdk, потом отдельно скачивать maven вместе со spring boot приложением, потом отдельно скачивать зависимости проекта через этот maven. Это и называется развертыванием приложения на компьютере.

Теперь с докером, как мы уже поняли, всё намного проще.

Внутри Docker Image получившимся в результате сборки приложения УЖЕ ЕСТЬ ВСЁ, что нужно для запуска этого приложения и любой человек может запустить приложение очень просто ранее упомянутой командой без муторного его развертывания.

Докер имеет стандартизированный способ сборки проекта.

То есть любые приложения использующие какие-угодно технологии имеют подобный способ сборки и в результате сборки имеют одинаковый итоговый формат.

Также Docker Image можно запускать одинаковым образом где угодно – на компьютере, в облаке или где-либо еще.

Создание Spring Boot Data Rest CRUD-приложения

Создадим crud приложение, только теперь с использованием Spring Data Rest.

Создадим новое Spring Boot приложение с такими папками и файлами.

Как видите, в этом проекте, по сравнению с предыдущими crud приложениями которые мы разрабатывали, стало крайне мало файлов.

Нет дао класса, сервис класса и даже контроллера. А если вы пролистаете данный урок далее, то увидите, что в файлах вообще практически нет кода.

Search Icon

Но при всем этом, это полностью рабочее crud приложение. Что? Как такое возможно?

Давайте же разберемся.

Для начала, давайте добавим зависимость для поддержки Spring Data Rest.

4.0.0 org.springframework.boot spring-boot-starter-parent 3.1.5 com.SpringBootApps firstSpringBootDataRestCRUDApp 0.0.1-SNAPSHOT firstSpringBootDataRestCRUDApp Demo project for Spring Boot 17 org.springframework.boot spring-boot-starter-data-jpa org.springframework.boot spring-boot-starter-web com.mysql mysql-connector-j runtime org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-devtools test org.springframework.boot spring-boot-starter-data-rest org.springframework.boot spring-boot-maven-plugin

В прошлом уроке мы задавали вопрос о DAO имплементациях. Нужно ли их так много?

Но тот же вопрос можно применить и к контроллерам! Нужно ли для каждой сущности писать маппинги(ну там GetMapping, PostMapping и т.д.) для CRUD операций?

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

Search Icon

Spring может сам создать эти методы за кулисами!

Всё что нам нужно, это создать интерфейсы расширяющие JpaRepository для сущностей, а спринг сам предоставит нам адреса, которые мы писали в GetMapping, PostMapping и т.д. по которым мы можем совершать CRUD операции. Адреса эти формируются автоматически по имени класса, который мы передали в JpaRepository<[По имени этого класса],Integer>.

Например, для JpaRepository<Actor, Integer> Spring Data Rest сгенерирует адреса /actors, /actors/{actorId} и т.д. для разных HTTP методов.

Также важно упомянуть что в JSON ответах от таких сгенерированных REST методов можно увидеть некоторые метаданные. Например, по какой ссылке был сделан запрос или количество переданных объектов и т.д.

package com.SpringBootApps.firstSpringBootDataRestCRUDApp.dao; import org.springframework.data.jpa.repository.JpaRepository; //Как уже было сказано адреса формируются автоматически по имени класса, //который мы передали в JpaRepository<[По имени этого класса],Integer>. public interface ActorRepository extends JpaRepository { }

Давайте же протестируем созданные обработчики.

В таблице actor сейчас три актера:

Давайте выберем из БД всех актеров GET запросом с помощью автоматически сгенерированного с помощью Spring Data Rest адреса /actors.

Как видим, все актеры успешно были получены.

При этом, как уже упоминалось, были присланы также некоторые метаданные. Например, можно увидеть, что JSON каждого актера содержит ссылку по которой можно получить доступ к нему.

Также на скриншоте снизу можно увидеть метаданные о страницах – количество актеров на странице (три), номер страницы (нулевая страница), размер страницы (на странице может быть всего 20 актеров). То есть актеры разбиваются на страницы. Пока сильно не будем углубляться, что такое страницы и как с ними взаимодействовать.

Давайте теперь выберем из БД одного конкретного актера GET запросом с помощью автоматически сгенерированного с помощью Spring Data Rest адреса /actors/{actorId}.

Как видим, актер с id два был успешно выбран из БД и прислан в ответ на GET запрос. Также с метаданными.

Давайте теперь добавим в БД нового актера POST запросом с помощью автоматически сгенерированного с помощью Spring Data Rest адреса /actors. Для этого пишем в body данные в формате JSON, которые мы хотим добавить в БД.

Выше видим, что в результате выполнения POST запроса был получен код успешного выполнения операции201. Ниже можно увидеть, что актер успешно добавился в БД.

Давайте теперь обновим в БД актера PUT запросом с помощью автоматически сгенерированного с помощью Spring Data Rest адреса /actors/{actorId}. Для этого пишем в body данные в формате JSON, которыми мы хотим обновить актера с id, который указывается в конце адреса.

Выше видим, что в результате выполнения PUT запроса был получен код успешного выполнения операции 201. Ниже можно увидеть, что актер с id четыре успешно обновился в БД.

Давайте теперь удалим из БД актера DELETE запросом с помощью автоматически сгенерированного с помощью Spring Data Rest адреса /actors/{actorId}. Для этого пишем в конце адреса id актера, которого мы хотим удалить.

Выше видим, что в результате выполнения DELETE запроса был получен код успешного выполнения операции201. Ниже можно увидеть, что актер с id четыре успешно удалился из БД.

Search Icon

Еще раз заметим. Все эти ссылки были фактически ПОДАРЕНЫ нам средствами Spring Data Rest. Это может серьезно сэкономить время если нам нужны простейшие crud операции.


Доп. конфигурация Spring Boot Data Rest приложения

А что если нам нужно чтобы Spring Data Rest не генерировал автоматически некоторые адреса для CRUD?

Что если мы хотим написать их код вручную в контроллере (например, для POST и DELETE)? То есть нам не нужны простейшие POST и DELETE операции по адресу /actors, мы хотим написать чуть более сложные обработчики для этого адреса вручную в контроллере.

Для этого можно просто отключить генерацию адресов для POST и DELETE в классе, который реализует RepositoryRestConfigurer переопределяя метод configureRepositoryRestConfiguration.

package com.SpringBootApps.firstSpringBootDataCRUDApp.config; import org.springframework.context.annotation.Configuration; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer; import org.springframework.http.HttpMethod; @Configuration public class MyDataRestConfig implements RepositoryRestConfigurer { @Override public void configureRepositoryRestConfiguration( RepositoryRestConfiguration config, CorsRegistry cors) { //здесь в массив передаем не нужные для генерации HTTP методы HttpMethod[] theUnsupportedActions={HttpMethod.POST, HttpMethod.DELETE}; config.getExposureConfiguration() //Ниже пишем класс, который мы передавали //в JpaRepository на основе //которого генерировались методы для CRUD. .forDomainType(Actor.class) //Передаем в методы ниже созданный массив. .withItemExposure((metadata, httpMethods) -> httpMethods.disable(theUnsupportedActions)) .withCollectionExposure((metadata, httpMethods) -> httpMethods.disable(theUnsupportedActions)); //Все. Автоматическая генерация адресов для совершения //Post и Delete запросов над сущностью actor отключена. } }

Создание Spring Boot Data CRUD-приложения

Создадим CRUD приложение, только теперь с использованием Spring Boot Data.

Создадим новое Spring Boot приложение с такими папками и файлами.

Содержимое файлов в этом crud приложении ничем не отличается от содержимого предыдущего crud приложения, которое мы создавали в прошлом уроке за исключением файла класса сервиса.

Также можно заметить, что DAO интерфейс и класс исчезли, а вместо них появился файл ActorRepository. Давайте же разберемся почему.

До сего момента мы делали Crud операции, только над актером.

А что если в нашем приложении нам придется совершать CRUD операции не только с ним, а и с другими сущностями?

Получается придется для каждой сущности писать DAO интерфейс и его реализацию с почти одинаковыми CRUD методами.

Spring Data избавит нас от этой головной боли!

Благодаря Spring Data, дао классы вообще не пишутся. Вместо этого для каждой сущности пишется один единственный интерфейс, наследующий от другого интерфейса, он называется JpaRepository<[сюда передаем имя класса сущности], [сюда тип ID сущности]> (ниже пример), а методы для простых CRUD операций уже реализованы за нас и мы просто используем их в сервисе. То есть Spring Data фактически дарит нам методы для работы с БД и вручную, как раньше, их писать не нужно.

Также JpaRepository предоставляет нам и другие методы, здесь посмотрим только CRUD.

package com.SpringBootApps.firstSpringBootCRUDApp.dao; import java.util.List; //Вот интерфейс о котором мы говорили. //Для каждой другой сущности пишется подобный. //Класс таблицы actor это Actor, а тип id в этой таблице как мы //помним это Integer. Поэтому передаем эти данные в JpaRepository. public interface ActorRepository extends JpaRepository { //А здесь, если для сущности актер нужны только самые //обычные Crud операции, можно даже ничего не писать!!! //Но обычно здесь пишутся методы со специальным именем //чтобы делать особенные запросы к таблице с актерами. }

Теперь давайте же воспользуемся этими подаренными Spring Data методами для совершения crud операций в сервисном классе.

package com.SpringBootApps.firstSpringBootDataCRUDApp.service; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.SpringBootApps.firstSpringBootDataCRUDApp.models.Actor; import com.SpringBootApps.firstSpringBootDataCRUDApp.repositories.ActorRepository; import java.util.Optional; @Service public class ServiceInterfaceImpl implements ServiceInterface { //Внедряем созданный интерфейс (вернее бин, который будет //создан на основе этого интерфейса за кулисами средствами //Spring Boot). Где-то там за кулисами ясное дело спринг //берет созданный нами интерфейс ActorRepository, реализует //его (то есть реализует методы, которые наш интерфейс //наследовал от интерфейса JpaRepository) и внедряет //объект(бин) реализованного класса сюда. @Autowired private ActorRepository actorRepository; @Transactional public Actor getActor(int actorID) { //.findById() – дается нам БЕСПЛАТНО! //То есть нам не нужно создавать метод для взаимодействия //с БД вручную как мы это делали раньше. Optional actor = actorRepository.findById(actorID); //Немного об Optional. Здесь вам просто нужно знать что //в Optional объекте может оказаться либо пустота либо //извлеченный по Id актер. Не будем углубляться. Optional //это просто дополнительная страховка на случай того //если actorRepository.findById(actorID) вернет null. if(actor.isPresent()) { //Если не пустая возвращаем актера. return actor.get(); } return null;//иначе null } @Transactional public void saveActor(Actor actor) { //.save() – БЕСПЛАТНО! actorRepository.save(actor); } @Transactional public List getListOfActors() { //.findAll() – БЕСПЛАТНО! return actorRepository.findAll(); } @Transactional public void deleteActor(int actorid) { //.deleteById() – БЕСПЛАТНО! actorRepository.deleteById(actorid); } }

Давайте протестируем данное Crud приложение.

В таблице actor сейчас три актера:

Для примера давайте протестируем обработчик getActors, который выбирает всех актеров из таблицы.

Как видим, все актеры были выбраны обработчиком getActors из БД успешно. Это значит, что подаренный Spring Data метод findAll() работает.

Создание Spring Boot CRUD-приложения

Создадим Spring Boot Crud приложение. Это приложение будет предоставлять ту же функциональность, что и предыдущее CRUD приложение. То есть между клиентом и сервером будет происходить обмен JSON файлами с помощью REST.

Создадим новое Spring Boot приложение с такими папками и файлами.

Здесь содержимое всех файлов идентично предыдущему CRUD приложению, которое разбиралось в разделе “Spring MVC CRUD”, за исключением pom, содержимое которого идентично файлу pom предыдущего урока в этом разделе, DAO класса и файла настроек application.propreties.

В application.propreties находятся самые основные настройки, а именно данные для подключения к БД и настройка корневого пути.

server.servlet.context-path=/firstSpringBootCRUDApp spring.datasource.url=jdbc:mysql://localhost/storage spring.datasource.username=root spring.datasource.password=07998MSD spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

Помните в MVC приложении с использованием Hibernate мы в applicationContext.xml определяли бины DataSource и SessionFactory?

Spring Boot создает эти бины автоматически и нам не приходиться писать весь код этих бинов, где легко допустить ошибку. Нам просто нужно написать данные бд (database-url, login, password) в application.properties.

Также важно упомянуть, что теперь вместо бина SessionFactory создается и внедряется в ДАО класс бин EntityManager.

EntityManagerэто тот же самый SessionFactory, только как-бы обертка над ним (то есть добавляет доп функциональность SessionFactory).

Чтобы узнать какая это функциональность копнем чуть глубже.

Что такое JPA? Это стандарт для реализации фреймворков по типу Hibernate, то есть фреймворков для связывания таблиц БД и объектов. Да, существует не только Hibernate.

JPA это набор интерфейсов (под интерфейсом имеется ввиду ООП интерфейс, то есть тот интерфейс, который реализуют классы).

Фреймворки по типу Hibernate реализуют эти интерфейсы (то есть классы фреймворка Hibernate реализуют набор JPA интерфейсов).

И благодаря EntityManager можно легко переходить с одной реализации этих JPA интерфейсов на другую почти ничего не меняя в программе.

То есть без удобных средств Spring Boot нам пришлось бы вручную переконфигурировать весь проект, чтобы, например, перейти с Hibernate на, например, стандартную реализацию JPA

Взглянем как через EntityManger можно преходить между реализациями ничего не меняя в проекте.

Ниже в одном и том же DAO классе используется и Hibernate и стандартная JPA реализация.

И это благодаря EntityManager, в обычном не Spring Boot приложении такое не было бы возможно. Ну или правильнее сказать, такое очень сложно было бы настроить вручную.

package com.SpringbootApps.firstSpringBootCRUDApp.Dao; import java.util.List; import org.springframework.stereotype.Repository; @Repository public class ActorDAOImpl implements ActorDAO { // Внедряем EntityManager @Autowired private EntityManager entityManager; public Actor getActor(int actorId) { // Извлекаем новую Hibernate session также // как это делали через sessionFactory // в прошлых Crud приложениях, // только в данном случае // из entityManager и другим методом. Session session = entityManager.unwrap(Session.class); return session.get(Actor.class, actorId); } public List getListOfActors() { // А здесь давайте воспользуемся стандартной // реализацией JPA. То есть это не Hibernate. // Как видим сессии нет. Со стандартной реализацией // работаем прямо через entityManager. return entityManager.createQuery(“from Actor”).getResultList(); } public void saveActor(Actor actor) { Session session = entityManager.unwrap(Session.class); session.saveOrUpdate(actor); } public void deleteActor(int actorId) { Session session = entityManager.unwrap(Session.class); session.createQuery(“delete from Actor ” + “where id=:actorid”).setParameter( “actorid”, actorId).executeUpdate(); } }

В таблице actor сейчас три актера:

Давайте получим их данные всех актеров в формате JSON через обработчик getActors. Этот обработчик извлекает актеров из БД помещает их в List и отправляет этот List с актерами в формате JSON. Отметим, что актеры, как мы только что видели в классе, извлекаются с помощью стандартной реализации JPA.

Обратимся к этому обработчику через Rest Client по ссылке /actors запросом GET. RestController распознает, что был сделан GET запрос и вызывает обработчик помеченный аннотацией GetMapping.

Как видим, List с объектами актеров из БД успешно пришел в формате JSON в ответ на GET запрос.

То есть мы успешно воспользовались стандартными реализациями JPA.

Теперь давайте протестируем какой-нибудь метод, который использует Hibernate реализации, чтобы убедиться, что разные реализации JPA могут быть использованы в одном проекте.

Давайте извлечем третьего актера с помощью обработчика getActor.

Как видим, всё прошло успешно. То есть обе реализации JPA могут одновременно использоваться в одном проекте благодаря EntityManager и конечно же Spring Boot.

Spring Boot: в чём его сила и удобство?

Что такое Spring Boot приложение?

Spring Boot приложение это почти то же самое Spring приложение, что мы изучали раньше, но с дополнительными средствами упрощающими разработку. Эти средства помогают легче конфигурировать наше приложение и взаимодействовать с ним.

Отметим основные моменты:

1. Во первых, в Spring Boot приложении сокращено количество файлов (нет файла настроек бинов applicationContext, файла web.xml и других), которые нужно было конфигурировать вручную и где легко ошибиться и потом долго искать ошибку.

В Spring Boot приложении нам вообще не придется лезть в папку WEB-INF (которой собственно и нет в Spring Boot приложении). Спринг Бут сам за кулисами конфигурирует приложение. А нужные нам настройки пишутся в отдельном файле настроек application.properties в очень коротком и простом виде.

2. Второй важный момент, что не нужно беспокоиться о совместимости версий зависимостей в pom, ведь часто приходиться тратить много времени на поиск совместимых. Спринг Бут предоставляет специальные зависимости, которые представляют собой наборы всем нам известных ранее зависимостей, которые уже сразу между собой совместимы.

3. В третьих, стартовый Спринг Бут проект можно настроить (настроить стартовые зависимости, тип сборщика Maven или Gradle и т.д.) удобным интуитивным интерфейсом в интернете на сайте Spring Initializer и скачать этот настроенный проект с этого сайта. Что упрощает стартовую конфигурацию проекта.

Раньше, чтобы создать новый Spring проект, приходилось копировать старый проект и редактировать его, потому что создавать Spring проект с нуля морока серьезная. Спринг Бут же серьезно упрощает создание нового Спринг проекта.

Давайте зайдем на сайт https://start.spring.io/ , сконфигурируем и скачаем наше первое Spring Boot приложение:

Видим интерфейс для настройки стартового Spring Boot приложения. Здесь, как видим, можно выбрать сборщик проекта (они бывают разные, но мы выберем Maven), язык Spring Boot проекта, его версию, и также можно сразу настроить метаданные проекта (это то что, как мы помним, сверху в pom файле).

Также можно сразу добавить зависимости в проект. Для этого нажимаем на Add Dependencies…

В результате откроется окно в котором можно найти и добавить в проект набор зависимостей(как уже упоминалось, в сприг бут наборы, а не отдельные зависимости):

Добавим такие наборы зависимостей: Web(добавляет поддержку Spring MVC, сервлетов и т.д.), jpa(поддержка Hibernate и др.) и MySql Driver(для возможности подключения к базе).

Всё мы сконфигурировали наш стартовый проект.

Spring Initializer настолько удобен, что перед скачиванием настроенного проекта его даже можно просмотреть прямо на сайте. То есть просмотреть структуру проекта и его файлы.

Нажимаем Download. Наше первое Spring Boot приложение должно скачаться на компьютер.

4. Ну и наверное самый важный момент из выше перечисленных, это то что скачиваемый Spring Boot проект уже сразу в себе содержит Tomcat сервер на котором мы запускаем скачанное Spring Boot приложение. То есть нам не нужно скачивать Tomcat отдельно и запускать через него спринг приложения, сервер уже СРАЗУ встроен в приложение, которое скачано с сайта Spring Initializer.

Также Spring Boot имеет различную другую функциональность, которая будет рассмотрена далее.


Spring Boot приложение.

Давайте добавим скачанное стартовое приложение в Eclipse. Для этого переходим по пути File -> Open Projects from File System и там указываем путь к папке скачанного проекта. Взглянем на структуру и файлы Spring Boot приложения.

Как видим, оно отличается от тех Спринг приложений, которые были раньше. Видим, что папка WEB-INF в src отсутствует и присутствуют некоторые новые папки и файлы.

mvnwдля запуска мавен проекта из командной строки. Автоматически подгружает мавен при запуске проекта если он не установлен или обновляет его.

То есть запустить приложение можно из консоли не открывая IDE. Давайте сделаем это.

Вводим в папке приложения mvnw package, что загружает мавен если его нет и создает jar файл:

В папке target должен появиться jar файлик нашего приложения. Его теперь можно запустить.

Далее вводим java -jar (имя приложения).jar и оно запускается

properties – здесь все настройки Spring Boot приложения. Настройки совершаются в формате свойств типа some.prop=”value” (то есть имя свойства и его значение). В примере ниже можно увидеть, что таким образом можно настраивать приложение в очень лаконичной форме. Можно с помощью свойства server.port изменить порт на котором запускается наше приложение, а например с помощью server.servlet.context-path можно изменить корневой элемент в адресе. Также можно увидеть, что здесь настраиваются уровни логирования, данные для подключения к БД, время жизни сессии и вообще все основные настройки Спринг Бут приложения происходят здесь в этом файле.

Можно также добавлять сюда кастомные свойства типа some.custom.prop="value" и можно внедрить их в строку.

Кто помнит из прошлых уроков их можно извлекать в классах приложения с помощью аннотации @Value("\${some.custom.prop}") и внедрять в String поле.

#Lets watch on some of important settings. There are lots of them #we can change port of app. Now it will be 7070 instead of 8080 server.port=7070 #we can change context path – root element of addresses in project server.servlet.context-path=/firstSpringBootApp #can set lifetime of session server.servlet.session.timeout=120s #can set logging levels to folders of project logging.level.com.springboot=TRACE logging.level.org.hibernate=DEBUG #database info. spring.datasource.url=jdbc:mysql://localhost/storage spring.datasource.username=root spring.datasource.password=07998MSD spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

В папке static проекта хранятся все статические ресурсы, типа обычных html страниц,css файлов,картинок и т.д

В template хранятся динамические страницы типа JSP,Thyseleaf(аналог JSP)

pom файлик в Spring Boot приложении отличается некоторыми элементами от обычного веб приложения.

Здесь можно увидеть настройки, которые мы делали через интерфейс сайта Spring Initializr. То есть метаданные, версию спринг бута и также зависимости, которые мы включали в проект. Также здесь мы включим еще одну нужную зависимость devtools вручную, о который написано ниже.

4.0.0 org.springframework.boot spring-boot-starter-parent 3.1.5 com.SpringBootApps firstSpringBootApp 0.0.1-SNAPSHOT firstSpringBootApp Demo project for Spring Boot 17 org.springframework.boot spring-boot-starter-data-jpa org.springframework.boot spring-boot-starter-web com.mysql mysql-connector-j runtime org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-devtools test org.springframework.boot spring-boot-maven-plugin

Аннотация @SpringBootApplication в файле ниже состоит из трех аннотаций@EnableAutoConfiguration (нужна для включения автоматической конфигурации Spring Boot) и знакомых нам @ComponentScan и @Configuration (кто помнит использовались в Java версии файла applicationContext.xml для конфигурации бинов).

То есть через этот класс происходит конфигурация приложения, только она происходит за кулисами средствами Spring Boot, а нам ничего конфигурировать не нужно.

Создание бинов на основе классов, помеченных специальными аннотациями (@Component, @Controller, @Repository, @Service), происходит с помощью аннотации @ComponentScan.

Эта аннотация автоматически сканирует все подпапки той директории, в которой находится класс с методом main() (обычно это Application.java).

Вернитесь немного назад и посмотрите на структуру проекта ещё раз: файл с main находится на уровень выше папки controller, в которой лежит контроллер.

То же самое должно быть и с остальными директориями — service, dao и другими.
Все они должны быть вложены в ту же корневую папку или быть её поддиректориями, чтобы @ComponentScan смог обнаружить и зарегистрировать все нужные бины, и приложение корректно работало.

Запуск приложения, автоматическая конфигурация бинов, сканирование поддеректорий и другие настройки происходят методом run в main методе.

Запускать Spring Boot приложение можно как обычное Java приложение, то есть через метод main, что удобно (можно воспользоваться кнопкой Run в Eclipse IDE).

package com.SpringBootApps.firstSpringBootApp; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class FirstSpringBootAppApplication { public static void main(String[] args) { SpringApplication.run(FirstSpringBootAppApplication.class, args); } }

Давайте в классе контроллере создадим простенький обработчик для теста первого Спринг Бут приложения.

package com.SpringBootApps.firstSpringBootApp.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; public class MainController { @GetMapping(“/helloworld”) public String helloWorld() { return “Hello world!!!”; } }

Запустим приложение через main файл и перейдем к обработчику по определенному адресу /helloworld.

Как видим, обработчик успешно прислал строку в ответ на запрос.

При этом можно увидеть, что обращение к обработчику идет через порт 7070 и через корневой путь firstSpringBootApp. Это было настроено, как мы помним, в файле application.properties

Создание REST API CRUD приложения

Создадим crud приложение только теперь с использованием REST.

Создадим новое Maven приложение с такими папками и файлами.

Содержимое файлов в этом crud приложении ничем не отличается от содержимого предыдущего crud приложения, которое мы создавали, за исключением содержимого файла контроллера и pom файла, в который просто нужно добавить зависимость для поддержки jackson.

Рассмотрим содержимое файла контроллера, в котором собственно пишется api.

Заметьте что в Mapping-ах внизу повторяющиеся адреса /actors.

Это стандартная практика в spring rest приложениях, что для разных операций с тем или иным конкретным классом (в данном случае с Actor) используется один и тот же адрес, а получатель запроса (получатель это RestController) будет распознавать какой из обработчиков вызывать основываясь на HTTP методе присланного запроса

То есть крайне не желательно называть адреса по разному. Например: /getallactors, /getactorfromdb, /savesingleactor/{actorID} и т.д.

Лучше пусть контроллер сам распознает какой из обработчиков вызывать по HTTP методу запроса.

  • Get запрос будет вызывать обработчик помеченный аннотацией @GetMapping,
  • Post запрос будет вызывать обработчик помеченный аннотацией @PostMapping,
  • Put запрос будет вызывать обработчик помеченный аннотацией @PutMapping,
  • Delete запрос будет вызывать обработчик помеченный аннотацией @DeleteMapping)
package com.MavenWebApps.RestWebApplicationCRUD.controller; import java.util.List; @RestController public class MainController { @Autowired private ServiceInterface serviceclass; //READ. Всех актеров @GetMapping(“/actors”) public List getActors() { return serviceclass.getListOfActors(); } //READ. Одного актера @GetMapping(“/actors/{actorID}”) public Actor getActor( @PathVariable int actorID){ Actor actor = serviceclass.getActor(actorID); return actor; } //CREATE @PostMapping(“/actors”) public Actor addActor( @RequestBody Actor actor) { //Из нового здесь можно увидеть //аннотацию @RequestBody, //но здесь ничего сложного. //Когда клиентом посылается JSON //методом POST в наш контроллер //средства Jackson конвертируют //за кулисами этот JSON в объект actor //и мы можем использовать этот объект. //В этом обработчике мы добавляем //присланного актера в базу. serviceclass.saveActor(actor); return actor; } //UPDATE @PutMapping(“/actors”) public Actor updateActor( @RequestBody Actor actor) { serviceclass.saveActor(actor); return actor; } //DELETE @DeleteMapping(“/actors/{actorID}”) public String deleteActor( @PathVariable int actorID) { serviceclass.deleteActor(actorID); return “Deleted customer id – ” +actorID; } }

Давайте же протестируем созданные обработчики.

В таблице actor сейчас три актера:

Давайте получим данные всех актеров в формате JSON через обработчик getActors. Этот обработчик извлекает актеров из БД помещает их в List и отправляет этот List с актерами в формате JSON.

Обратимся к этому обработчику через Rest Client по ссылке /actors запросом GET. RestController распознает, что был сделан GET запрос и вызывает обработчик помеченный аннотацией GetMapping.

Как видим, List с объектами актеров из БД успешно пришел в формате JSON в ответ на GET запрос.

Давайте получим данные одного конкретного актера в формате JSON через обработчик getActor. Этот обработчик извлекает конкретного актера из БД по присланному в ссылке id этого актера и отправляет его в формате JSON.

Обратимся к этому обработчику через Rest Client по ссылке /actors/2 запросом GET чтобы получить JSON актера с id два. RestController распознает, что был сделан GET запрос и вызывает обработчик помеченный аннотацией GetMapping. Но в этот раз уже тот где ссылка /actors/{actorID}, а не просто /actors

Как видим обработчик успешно извлек актера с id два и отправил его в ответ на GET запрос в формате JSON.

Теперь давайте добавим нового актера в БД через обработчик addActor. Этот обработчик получает информацию нового актера в формате JSON, конвертирует этот JSON в объект класса Actor и добавляет этот объект в БД. Также он возвращает данные добавленного актера в формате JSON в ответ на POST запрос.

Обратимся к этому обработчику через Rest Client по ссылке /actors запросом POST, при этом во вкладке BODY пишем в формате JSON данные актера, которого мы хотим отправить в контроллер и добавить БД. RestController распознает, что был сделан POST запрос и вызывает обработчик помеченный аннотацией PostMapping.

Выше видим, что обработчик успешно отправил в ответ на POST запрос данные добавленного в БД актера в формате JSON. Ниже можно увидеть, что актер успешно добавился в БД.

Теперь давайте обновим присутствующего актера в БД через обработчик updateActor. Этот обработчик получает новую информацию для обновления существующего в БД актера в формате JSON, конвертирует этот JSON в объект класса Actor и обновляет актера в БД исходя из id, который был передан в JSON. Также он возвращает данные обновленного актера в формате JSON в ответ на PUT запрос.

Обратимся к этому обработчику через Rest Client по ссылке /actors запросом PUT, при этом во вкладке BODY пишем в формате JSON данные актера, которого мы хотим отправить в контроллер и обновить в БД, при этом в отправляемом JSON обязательно нужно указать id актера, которого мы хотим обновить. RestController распознает, что был сделан PUT запрос и вызывает обработчик помеченный аннотацией PutMapping.

Выше видим, что Обработчик успешно отправил в ответ на PUT запрос данные обновленного в БД актера в формате JSON. Ниже можно увидеть, что актер с id четыре успешно обновился в БД. Naomi Watts изменилось на Michael Pitt.

Давайте удалим конкретного актера из БД через обработчик deleteActor. Этот обработчик удаляет конкретного актера из БД по присланному в ссылке id этого актера и отправляет сообщение, что актер с таким-то id удален из БД.

Обратимся к этому обработчику через Rest Client по ссылке /actors/4 запросом DELETE чтобы удалить из БД актера с id четыре. RestController распознает, что был сделан DELETE запрос и вызывает обработчик помеченный аннотацией DeleteMapping.

Выше видим, что Обработчик успешно отправил в ответ на DELETE запрос сообщение о том, что удален актер с id четыре. Ниже можно увидеть, что актер с id четыре успешно удалился из БД.

Первое Spring REST API-приложение

Создадим свое собственное Rest Api. То есть мы создадим набор методов, к которым можно будет обращаться http запросом по ссылке. Эти методы будут возвращать java объекты конвертированные в JSON тому кто сделал запрос.

Создадим Мавен приложение с такой структурой и файлами:

В файле MainController будут находиться обработчики, которые будут принимать http запрос, конвертировать java объекты в JSON и отправлять этот JSON обратно тому кто сделал запрос.

В classes находятся классы, объекты которых будут конвертироваться в JSON.

Теперь давайте добавим необходимые зависимости в pom файл чтобы приложение работало. А именно Jackson для конвертации из JSON в объект и наоборот.

4.0.0 com.MavenWebApp RestWEBapp jar 0.0.1-SNAPSHOT hibernateCRUDapp Maven Webapp http://maven.apache.org org.springframework spring-webmvc 5.3.0 org.springframework spring-tx 5.3.0 org.springframework spring-orm 5.3.0 org.hibernate hibernate-core 5.3.26.Final mysql mysql-connector-java 8.0.32 com.mchange c3p0 0.9.5.5 javax.servlet javax.servlet-api 3.1.0 javax.servlet jstl 1.2 junit junit 3.8.1 test com.fasterxml.jackson.core jackson-databind 2.15.2 org.apache.maven.plugins maven-war-plugin 3.4.0 false

Обработчики в нашем приложении будут конвертировать объект в JSON подобный тому, что приведен ниже.

{ “musician_name”: “Behoven”, “isclassic”: true, “numofoperas”: 150, “gratest_works”: [“Archduke Trio”, “Missa Solemnis”, “Symphony No. 5”], “theMostFamousWork”: { “work_name”: “Symphony No. 3 ‘Eroica”, “dedicatedTo”: “to Napoleon”, “ComposedIn”: 1802 }, “somePropToIgnore”: “ignoreMe” }

Этот JSON хранит данные о музыканте Bethoven.

Здесь можно увидеть все типы информации, которые можно хранить в JSON.

В первых трех записях можно увидеть, что можно хранить разные типы данных, в нашем случае – string, boolean и int.

В четвертой записи можно увидеть, что в JSON также можно хранить массивы. Его элементы перечисляются в квадратных скобочках.

В пятой записи можно увидеть, что в JSON также можно хранить вложенный JSON. В нем хранятся данные, которые описывают самую известную работу автора Bethoven.

Зачем шестая запись дальше будет объеснено.

Давайте создадим класс музыканта объект которого будет конвертироваться в JSON подобный тому, что приведен выше и наоборот должна быть возможность конвертации JSON выше в объект класса ниже. Как можно увидеть он содержит такие же типы информации как и json, который был выше – три поля, массив и вложенный объект. Как уже говорилось для успешной конвертации имена сеттеров и геттеров в этом классе должны содержать имена записей из JSON.

package com.MavenWebAps.RestWebApp.convertFromJsonjson; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; //Аннотация @JsonIgnoreProperties нужна просто //чтобы приложение не выдало ошибку если в классе //нет каких-то полей, которые есть в JSON, который //конвертируется в объект на основе этого класса. //В нашем случае это “somePropToIgnore”: “ignoreMe”. //Как можно увидеть в классе ниже нет поля //соответствующего записи //”somePropToIgnore”: “ignoreMe” в JSON. @JsonIgnoreProperties(ignoreUnknown=true) public class Musician { private String musician_name; private boolean isclassic; private int numofoperas; private String[] gratest_works; //В JSON файле приведенном ранее можно //увидеть вложенный JSON: “theMostFamousWork”: //{“work_name”: “Symphony No. 3 ‘Eroica'”, …} //Этот вложенный JSON сконвертируется как //внутренний объект объекта класса Musician. //Класс этого внутреннего объекта нам тоже нужно //создать. Поэтому ниже в следующем примере //кода после текущего создается //класс TheMostFamousWork. private TheMostFamousWork theMostFamousWork; public Musician() { } public Musician(String musician_name, boolean isclassic, int numofoperas) { this.musician_name = musician_name; this.isclassic = isclassic; this.numofoperas = numofoperas; } //Заметьте что названия геттерам и сеттерам даны //в соответствии с именами данных в JSON, то есть //левой части записей. Например сеттер и геттер //ниже для записи “musician_name” : “Behoven” //содержит имя левой части этой записи. //Имена очень важно давать именно такие //чтобы конвертация происходила успешно. public String getMusician_name() { return musician_name; } public void setMusician_name( String musician_name) { this.musician_name = musician_name; } public boolean isIsclassic() { return isclassic; } public void setIsclassic(boolean isclassic) { this.isclassic = isclassic; } public int getNumofoperas() { return numofoperas; } public void setNumofoperas(int numofoperas) { this.numofoperas = numofoperas; } public String[] getGratest_works() { return gratest_works; } public void setGratest_works( String[] gratest_works) { this.gratest_works = gratest_works; } public TheMostFamousWork getTheMostFamousWork() { return theMostFamousWork; } public void setTheMostFamousWork( TheMostFamousWork theMostFamousWork) { this.theMostFamousWork = theMostFamousWork; } }

Создадим теперь класс вложенного в музыканта объекта.

*TheMostFamousWork.java package com.MavenWebApps.RestWEBApp.convertfromjsoncl; public class TheMostFamousWork { private String work_name; private String dedicatedTo; private int ComposedIn; public TheMostFamousWork() { } public String getWork_name() { return work_name; } public void setWork_name(String work_name) { this.work_name = work_name; } public String getDedicatedTo() { return dedicatedTo; } public void setDedicatedTo(String dedicatedTo) { this.dedicatedTo = dedicatedTo; } public int getComposedIn() { return ComposedIn; } public void setComposedIn(int composedIn) { ComposedIn = composedIn; } }

Давайте же создадим Api, то есть набор методов, которые можно вызывать по ссылке используя http запросы. Делается это конечно же в контроллере, который представлен ниже.

package com.MavenWebAps.RestWEBApp.controller; import java.util.ArrayList; //Напишем свое REST Api! //В Spring MVC для обработки http методов и автоматической //конвертации из JSON в java объекты используеться //@RestController вместо @Controller, который был раньше. @RestController public class MainController { //Этим обработчиком просто отправляем в ответ //тому кто сделал запрос по адресу /getMusician //объект класса Musician в JSON формате. //Конвертация Java объектов в JSON и наоборот //происходит “за кулисами” средствами Jackson, //ничего специального для конвертации делать //не нужно. Просто возвращаем объект из метода //и он автоматически отправляеться тому кто //вызвал метод по ссылке /getMusician //в JSON формате. @RequestMapping(“/getMusician”) public Musician getMusician() { return new Musician(“Bethoven”,true,150); } //Помним что результат запроса SELECT для извлечения //нескольких записей из таблицы, который совершаеться //средствами hibernate, записываеться в List. //Соответственно удобно будет сразу отправлять клиенту //этот List с извлеченными данными таблицы как ответ. //С помощью Jackson можно отправить List в JSON формате. //Обработчик ниже возвращает клиенту List JAVA объектов //в формате JSON. Конвертация Java объектов в JSON //происходит “за кулисами” средствами Jackson. //Нашему API нужно просто вернуть List с обьектами //и клиент получит объекты списка в формате JSON. @RequestMapping(“/getMusicians”) public List getMusicians() { List musicians = new ArrayList(); musicians.add(new Musician(“Bethoven”,true,150)); musicians.add(new Musician(“Mozart”,true,250)); musicians.add(new Musician(“Bach”,true,120)); //Возвращаем List return musicians; //То есть можно отправить не один объект //как это делает прошлый обработчик, а сразу много. } //Чтобы клиент мог получить конкретного музыканта из списка //он может передать число в путь как показано ниже. //Это число это индекс в списке и по этому индексу извлекается //конкретный музыкант в списке. @RequestMapping(“/getSingleMusicianFromList/{musicianID}”) //Аннотация @PathVariable позволяет получить этот //индекс из пути запроса. Имя аргумента метода должно //совпадать с именем переменной в URL. public Musician getSingleMusicianFromList( @PathVariable int musicianID) { List musicians = new ArrayList(); musicians.add(new Musician(“Bethoven”,true,150)); musicians.add(new Musician(“Mozart”,true,250)); musicians.add(new Musician(“Bach”,true,120)); //С помощью get достаем из списка нужного музыканта //исходя из переданного клиентом числа и отправляем //его клиенту в JSON формате. return musicians.get(musicianID); } }

Давайте проверим первый обработчик. Вернет ли он нам объект музыканта Bethoven в формате JSON.

Вообще тест можно было бы совершить через адресную строку браузера, как раньше, поскольку в нашем приложении пока только Get запросы, но будем привыкать к Rest Client, так как скоро появятся и post запросы с помощью которых клиентом будет отправляться JSON в обработчик и другие виды запросов.

Перейдем по ссылке getMusician чтобы сделать запрос к первому обработчику:

Как видим обработчик успешно создал объект, конвертировал его в JSON и отправил этот JSON как ответ на запрос.

Кстати, как можно было заметить, вложенный объект мы не создавали, он null. Это было сделано просто чтобы не было слишком много JSON и чтобы показать, что можно конвертировать и отправлять не полностью заполненный объект.

Далее протестируем второй обработчик. Как мы помним, он должен возвращать много объектов конвертированных в JSON.

Как видим, обработчик успешно конвертировал объекты в списке в JSON и отправил эти JSON как ответ на запрос.

Теперь давайте с помощью третьего обработчика извлечем из списка объектов второй объект. Как мы помним счет начинается с нуля, поэтому чтобы обратиться ко второму элементу в списке пишем в конце ссылки 1.

Как мы помним, второй музыкант в списке был моцарт и как мы видим обработчик успешно его выбрал из списка и прислал в формате JSON.

Что такое REST API?

REST API – простой способ коммуникации между приложениями основанный на http методах передачи данных. Помним эти методы – GET, POST и др.

Приложения могут обмениваться данными в различных форматах. То есть обмен данными между приложениями может происходить например в формате обмена XML файлами. Также обмен может происходить с помощью JSON, HTML или др.

Приложения между которыми происходит обмен могут быть написаны на разных языках, главное чтобы эти приложения были способны пропарсить (это значит считать данные из файла) передаваемые файлы с данными (XML, JSON или HTML файлы).

Что же такое API?

API — это механизмы (или проще говоря, способы), с помощью которых клиентское приложение может обратиться к серверному приложению. Точнее, это конкретные методы (функции) на сервере, к которым можно обратиться по URL через HTTP.

Кроме самих методов, API включает правила, как правильно к ним обращаться: какой HTTP-метод использовать (GET, POST и т.д.), какие данные передавать и что можно получить в ответ.

Если говорить ещё точнее, API — это интерфейс серверного приложения, то есть набор доступных снаружи способов взаимодействия с сервером без знания его внутреннего устройства.

То есть под API-интерфейсом понимается то, что мы описали выше: серверные HTTP-обработчики и правила, как к ним обращатьсябез знания внутренней реализации обработчиков.

У каждого специализированного веб-приложения может быть свое собственное особенное API.

Example

Например, API приложения для покупки билетов.

У сервиса для покупки билетов есть API, который позволяет клиентскому приложению отправить запрос на бронирование или оплату. Сервер обрабатывает этот запрос и возвращает результат.

У многих известных веб-сервисов, как правило, есть свое API. У Facebook, GitHub и т.д. API, как уже говорилось, это методы(функции) для обмена данными определенные в программах этих веб-сервисов. Вышеупомянутые веб-ресурсы предоставляют ссылки для обращения к своим методам. То есть кто угодно может сделать запрос к какому-либо методу веб-сервиса по этим ссылкам и получить от этого веб-сервиса какие-то данные.

Давайте для примера посмотрим на страницу с перечислением Api методов сервиса FaceBook предназначенных для сбора статистики FaceBook страницы:

Как видим, нам предлагается ссылка по которой мы можем вызвать конкретный метод сервиса FaceBook, который вернет нам JSON с данными о том сколько человек взаимодействовало с конкретной указанной в ссылке FaceBook страницей.

Таким образом через ссылки можно взаимодействовать с Api Фейсбука.

Есть не один архитектурный стиль построения APIRPC, SOAP, REST и другие. Мы будем рассматривать REST стиль, так как он самый популярный.

Отличительной особенностью REST API от других стилей является то, что этот стиль лучше подходит для взаимодействия клиентского приложения с ресурсом (например с БД), который находиться на серверном приложении. То есть это как раз хорошо подходит для того чтобы клиент совершал CRUD операции с ресурсом на сервере.

Как уже говорилось, обменивающиеся данными приложения могут быть написаны на разных языках, ГЛАВНОЕ чтобы они оба использовали REST стиль и были в состоянии прочитать отправляемые друг другу данные.


JSON, Jackson

Самый популярный формат обмена данными сегодня это конечно же JSON.

JSON – просто строка с данными.

Простой пример:

{ “musician_name”:”Behoven”, “isclassic”: true, “numofoperas”: 150 }

Как видим данные хранятся в JSON в формате пар (то есть например “musician_name”:”Behoven” это одна пара) и их может прочитать и обработать любой язык программирования.

JSON строку можно превратить в Java объект и наоборот (это называется Data binding).

При конвертации JSON в Java объект пара превращается в поле в этом объекте. Левое значение в паре это имя поля объекта – правое это значение поля с таким именем.

То есть в обеъкте, который получиться по итогу конвертации JSON файла выше будет содержаться строковое поле с именем musician_name и значением “Behoven”, будет содержать boolean поле с именем isclassic и значением true и будет содержать числовое поле с именем numofoperas и значением 150.

Jacksonсамое популярное средства для таких конвертаций.

При конвертации ИЗ JSON в объект вызываются нужные setter-ы объекта в который происходит конвертация, при конвертации А когда В JSON из объекта нужные getter-ы.

При создании класса объекта, который будет конвертироваться в JSON и наоборот, для успешной конвертации нужно называть геттеры/сеттеры в нем в соответствии с теми элементами пар JSON, что слева (например musician_name – setMusician_name(), isclassic – setIsclassic()…) иначе jackson не найдет getter/setter методы чтобы совершить конвертацию.


Содержимое запроса/ответа, коды статуса

HTTP запрос/ответэто набор данных.

Example

HTTP запрос состоит из 3 частей:

  • В Request line храниться название метода (POST,GET,…),
  • Header Variablesдоп. инф., например формат отправляемых данных (JSON,xml,…), размер данных и т.д.,
  • Message Body – собственно отправляемый JSON или xml или др.

HTTP ответ отличается от запроса только первым элементом:

В HTTP ответе в Response line храниться код статуса ответа сервера (типа 404 – сервер сообщает этим кодом, что запрашиваемые клиентом данные не найдены на сервере (виноват клиент, что запросил несуществующие данные, поэтому это клиентская ошибка), или 200 – сервер сообщает в ответе этим кодом, что всё прошло успешно).

Вообще коды статуса ответа бывают такие:

  • 100-199 инф-ые,
  • 200-299удача,
  • 300-399перенаправление,
  • 400-499ошибки на клиенте,
  • 500-599ошибки на сервере.

REST клиент

Чтобы сделать небольшой тест отправки HTTP запроса и получения HTTP ответа необходимо установить REST клиент.

Скачаем программу Advanced REST Client по ссылке: https://www.advancedrestclient.com/.

REST клиент это такое средство для тестирования REST приложения.

Допустим мы создали серверное REST приложение, которое принимает JSON от клиентского приложения и отправляет ему в ответ на этот JSON другой JSON.

И нам нужно протестировать этот механизм. То есть нам нужно какое-то средство для того чтобы отправить JSON в наше серверное REST приложение и прочитать ответ, который серверное REST приложение отсылает. Это средство и есть REST клиент.

Их есть много разных в интернете, но мы воспользуемся Advance REST Client.


Тестирование с помощью Rest Client

Отправим GET запрос по ссылке https://api.myip.com (это такой REST API для получения своего IP адреса в формате JSON).

В ARC выберем GET запрос, вобьем ссылку куда будем делать запрос и нажмем send the request.

На картинке ниже можно увидеть все три элемента HTTP ответа – статус 200, headers и сам response c нашим ip.

Это был пример обмена данными в формате JSON между двумя приложениями.

Одно клиентское – Advanced REST Client, другое серверное – https://api.myip.com