JavaRush/Java блог/Java Developer/Обзор REST. Часть 3: создание RESTful сервиса на Spring B...

Обзор REST. Часть 3: создание RESTful сервиса на Spring Boot

Статья из группы Java Developer
участников
Это заключительная часть разбора REST. В предыдущих частях: Обзор REST. Часть 3: создание RESTful сервиса на Spring Boot - 1

Создание проекта

В данном разделе мы создадим небольшое RESTful приложение на Spring Boot. В нашем приложении будут реализованы CRUD (Create, Read, Update, Delete) операции над клиентами из примера из прошлой части разбора. Для начала создадим новое Spring Boot приложение через меню File -> New -> Project... В открывшимся окне выбираем Spring Initializr и указываем Project SDK: Обзор REST. Часть 3: создание RESTful сервиса на Spring Boot - 2Нажимаем кнопку Next. В следующем окне указываем тип проекта Maven, указываем Group и Artifact: Обзор REST. Часть 3: создание RESTful сервиса на Spring Boot - 3Нажимаем кнопку Next. В следующем окне нам необходимо выбрать нужные для проекта компоненты Spring Framework. Нам будет достаточно Spring Web: Обзор REST. Часть 3: создание RESTful сервиса на Spring Boot - 4Нажимаем кнопку Next. Далее осталось указать только наименование проекта и его расположение в файловой системе: Обзор REST. Часть 3: создание RESTful сервиса на Spring Boot - 5Нажимаем кнопку Finish. Проект создан, теперь мы можем увидеть его структуру: Обзор REST. Часть 3: создание RESTful сервиса на Spring Boot - 6IDEA сгенерировала за нас дескриптор развертывания системы сборки Maven — pom.xml и главный класс приложения: RestExampleApplication. Приведем их код:
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <parent>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-parent</artifactId>
       <version>2.2.2.RELEASE</version>
       <relativePath/> <!-- lookup parent from repository -->
   </parent>
   <groupId>com.javarush.lectures</groupId>
   <artifactId>rest_example</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <name>rest_example</name>
   <description>REST example project</description>

   <properties>
       <java.version>1.8</java.version>
   </properties>

   <dependencies>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
       </dependency>

       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-test</artifactId>
           <scope>test</scope>
           <exclusions>
               <exclusion>
                   <groupId>org.junit.vintage</groupId>
                   <artifactId>junit-vintage-engine</artifactId>
               </exclusion>
           </exclusions>
       </dependency>
   </dependencies>

   <build>
       <plugins>
           <plugin>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-maven-plugin</artifactId>
           </plugin>
       </plugins>
   </build>

</project>
RestExampleApplication:
@SpringBootApplication
public class RestExampleApplication {

   public static void main(String[] args) {
       SpringApplication.run(RestExampleApplication.class, args);
   }

}

Создание REST функционала

Наше приложение управляет клиентами. Поэтому первым делом нам необходимо создать сущность клиента. Это будет POJO класс. Создадим пакет model внутри пакета com.javarush.lectures.rest_example. Внутри пакета model создадим класс Client:
public class Client {

   private Integer id;
   private String name;
   private String email;
   private String phone;

   public Integer getId() {
       return id;
   }

   public void setId(Integer id) {
       this.id = id;
   }

   public String getName() {
       return name;
   }

   public void setName(String name) {
       this.name = name;
   }

   public String getEmail() {
       return email;
   }

   public void setEmail(String email) {
       this.email = email;
   }

   public String getPhone() {
       return phone;
   }

   public void setPhone(String phone) {
       this.phone = phone;
   }
}
В сервисе будут реализованы CRUD операции над клиентом. Следующим шагом мы должны создать сервис, в котором будут реализованы эти операции. В пакете com.javarush.lectures.rest_example создадим пакет service, внутри которого создадим интерфейс ClientService. Приведем код интерфейса с комментариями:
public interface ClientService {

   /**
    * Создает нового клиента
    * @param client - клиент для создания
    */
   void create(Client client);

   /**
    * Возвращает список всех имеющихся клиентов
    * @return список клиентов
    */
   List<client> readAll();

   /**
    * Возвращает клиента по его ID
    * @param id - ID клиента
    * @return - объект клиента с заданным ID
    */
   Client read(int id);

   /**
    * Обновляет клиента с заданным ID,
    * в соответствии с переданным клиентом
    * @param client - клиент в соответсвии с которым нужно обновить данные
    * @param id - id клиента которого нужно обновить
    * @return - true если данные были обновлены, иначе false
    */
   boolean update(Client client, int id);

   /**
    * Удаляет клиента с заданным ID
    * @param id - id клиента, которого нужно удалить
    * @return - true если клиент был удален, иначе false
    */
   boolean delete(int id);
}
Далее нам необходимо создать реализацию этого интерфейса. Сейчас в роли хранилища клиентов будет выступать Map<Integer, Client>. Ключом карты будет id клиента, а значением — сам клиент. Сделано это для того, чтобы не перегружать пример спецификой работы с БД. Однако в будущем мы сможем написать другую реализацию интерфейса, в которой можно будет подключить реальную базу данных. В пакете service создадим реализацию интерфейса ClientService:
@Service
public class ClientServiceImpl implements ClientService {

   // Хранилище клиентов
   private static final Map<Integer, Client> CLIENT_REPOSITORY_MAP = new HashMap<>();

   // Переменная для генерации ID клиента
   private static final AtomicInteger CLIENT_ID_HOLDER = new AtomicInteger();

   @Override
   public void create(Client client) {
       final int clientId = CLIENT_ID_HOLDER.incrementAndGet();
       client.setId(clientId);
       CLIENT_REPOSITORY_MAP.put(clientId, client);
   }

   @Override
   public List<Client> readAll() {
       return new ArrayList<>(CLIENT_REPOSITORY_MAP.values());
   }

   @Override
   public Client read(int id) {
       return CLIENT_REPOSITORY_MAP.get(id);
   }

   @Override
   public boolean update(Client client, int id) {
       if (CLIENT_REPOSITORY_MAP.containsKey(id)) {
           client.setId(id);
           CLIENT_REPOSITORY_MAP.put(id, client);
           return true;
       }

       return false;
   }

   @Override
   public boolean delete(int id) {
       return CLIENT_REPOSITORY_MAP.remove(id) != null;
   }
}
Аннотация @Service говорит спрингу, что данный класс является сервисом. Это специальный тип классов, в котором реализуется некоторая бизнес логика приложения. Впоследствии, благодаря этой аннотации Spring будет предоставлять нам экземпляр данного класса в местах, где это, нужно с помощью Dependency Injection. Теперь пришло время для создания контроллера. Специального класса, в котором мы реализуем логику обработки клиентских запросов на эндпоинты (URI). Чтобы было понятней, будем создавать данный класс по частям. Сначала создадим сам класс и внедрим в него зависимость от ClientService:
@RestController
public class ClientController {

   private final ClientService clientService;

   @Autowired
   public ClientController(ClientService clientService) {
       this.clientService = clientService;
   }
}
Поясним аннотации: @RestController — говорит спрингу, что данный класс является REST контроллером. Т.е. в данном классе будет реализована логика обработки клиентских запросов @Autowired — говорит спрингу, что в этом месте необходимо внедрить зависимость. В конструктор мы передаем интерфейс ClientService. Реализацию данного сервиса мы пометили аннотацией @Service ранее, и теперь спринг сможет передать экземпляр этой реализации в конструктор контроллера. Далее мы пошагово будем реализовывать каждый метод контроллера, для обработки CRUD операций. Начнем с операции Create. Для этого напишем метод create:
@PostMapping(value = "/clients")
public ResponseEntity<?> create(@RequestBody Client client) {
   clientService.create(client);
   return new ResponseEntity<>(HttpStatus.CREATED);
}
Разберем данный метод: @PostMapping(value = "/clients") — здесь мы обозначаем, что данный метод обрабатывает POST запросы на адрес /clients Метод возвращает ResponseEntity<?>. ResponseEntity — специальный класс для возврата ответов. С помощью него мы сможем в дальнейшем вернуть клиенту HTTP статус код. Метод принимает параметр @RequestBody Client client, значение этого параметра подставляется из тела запроса. Об этом говорит аннотация @RequestBody. Внутри тела метода мы вызываем метод create у ранее созданного сервиса и передаем ему принятого в параметрах контроллера клиента. После чего возвращаем статус 201 Created, создав новый объект ResponseEntity и передав в него нужное значение енума HttpStatus. Далее реализуем операцию Read: Для начала реализуем операцию получения списка всех имеющихся клиентов:
@GetMapping(value = "/clients")
public ResponseEntity<List<Client>> read() {
   final List<Client> clients = clientService.readAll();

   return clients != null &&  !clients.isEmpty()
           ? new ResponseEntity<>(clients, HttpStatus.OK)
           : new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
Приступим к разбору: @GetMapping(value = "/clients") — все аналогично аннотации @PostMapping, только теперь мы обрабатываем GET запросы. На этот раз мы возвращаем ResponseEntity<List<Client>>, только в этот раз, помимо HTTP статуса, мы вернем еще и тело ответа, которым будет список клиентов. В REST контроллерах спринга все POJO объекты, а также коллекции POJO объектов, которые возвращаются в качестве тел ответов, автоматически сериализуются в JSON, если явно не указано иное. Нас это вполне устраивает. Внутри метода, с помощью нашего сервиса мы получаем список всех клиентов. Далее, в случае если список не null и не пуст, мы возвращаем c помощью класса ResponseEntity сам список клиентов и HTTP статус 200 OK. Иначе мы возвращаем просто HTTP статус 404 Not Found. Далее реализуем возможность получать клиента по его id:
@GetMapping(value = "/clients/{id}")
public ResponseEntity<Client> read(@PathVariable(name = "id") int id) {
   final Client client = clientService.read(id);

   return client != null
           ? new ResponseEntity<>(client, HttpStatus.OK)
           : new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
Из нового, у нас тут появилась переменная пути. Переменная, которая определена в URI. value = "/clients/{id}". Мы указали ее в фигурных скобках. А в параметрах метода принимаем её в качестве int переменной, с помощью аннотации @PathVariable(name = "id"). Данный метод будет принимать запросы на uri вида /clients/{id}, где вместо {id} может быть любое численное значение. Данное значение, впоследствии, передается переменной int id — параметру метода. В теле мы получаем объект Client с помощью нашего сервиса и принятого id. И далее, по аналогии со списком, возвращаем либо статус 200 OK и сам объект Client, либо просто статус 404 Not Found, если клиента с таким id не оказалось в системе. Осталось реализовать две операции — Update и Delete. Приведем код этих методов:
@PutMapping(value = "/clients/{id}")
public ResponseEntity<?> update(@PathVariable(name = "id") int id, @RequestBody Client client) {
   final boolean updated = clientService.update(client, id);

   return updated
           ? new ResponseEntity<>(HttpStatus.OK)
           : new ResponseEntity<>(HttpStatus.NOT_MODIFIED);
}

@DeleteMapping(value = "/clients/{id}")
public ResponseEntity<?> delete(@PathVariable(name = "id") int id) {
   final boolean deleted = clientService.delete(id);

   return deleted
           ? new ResponseEntity<>(HttpStatus.OK)
           : new ResponseEntity<>(HttpStatus.NOT_MODIFIED);
}
Чего-то существенно нового в данных методах нет, поэтому подробное описание пропустим. Единственное, о чем стоит сказать: метод update обрабатывает PUT запросы (аннотация @PutMapping), а метод delete обрабатывает DELETE запросы (аннотация DeleteMapping). Приведем полный код контроллера:
@RestController
public class ClientController {

   private final ClientService clientService;

   @Autowired
   public ClientController(ClientService clientService) {
       this.clientService = clientService;
   }

   @PostMapping(value = "/clients")
   public ResponseEntity<?> create(@RequestBody Client client) {
       clientService.create(client);
       return new ResponseEntity<>(HttpStatus.CREATED);
   }

   @GetMapping(value = "/clients")
   public ResponseEntity<List<Client>> read() {
       final List<client> clients = clientService.readAll();

       return clients != null &&  !clients.isEmpty()
               ? new ResponseEntity<>(clients, HttpStatus.OK)
               : new ResponseEntity<>(HttpStatus.NOT_FOUND);
   }

   @GetMapping(value = "/clients/{id}")
   public ResponseEntity<Client> read(@PathVariable(name = "id") int id) {
       final Client client = clientService.read(id);

       return client != null
               ? new ResponseEntity<>(client, HttpStatus.OK)
               : new ResponseEntity<>(HttpStatus.NOT_FOUND);
   }

   @PutMapping(value = "/clients/{id}")
   public ResponseEntity<?> update(@PathVariable(name = "id") int id, @RequestBody Client client) {
       final boolean updated = clientService.update(client, id);

       return updated
               ? new ResponseEntity<>(HttpStatus.OK)
               : new ResponseEntity<>(HttpStatus.NOT_MODIFIED);
   }

   @DeleteMapping(value = "/clients/{id}")
   public ResponseEntity<?> delete(@PathVariable(name = "id") int id) {
       final boolean deleted = clientService.delete(id);

       return deleted
               ? new ResponseEntity<>(HttpStatus.OK)
               : new ResponseEntity<>(HttpStatus.NOT_MODIFIED);
   }
}
В итоге, структура нашего проекта выглядит следующим образом: Обзор REST. Часть 3: создание RESTful сервиса на Spring Boot - 7

Запуск и тестирование

Чтобы запустить наше приложение, достаточно запустить метод main в классе RestExampleApplication. А для того, чтобы тестировать RESTful веб сервисы, нужно скачать новое ПО ) Дело в том, что GET запросы довольно просто отправлять из обычного браузера, а вот для POST, PUT и DELETE обычным браузером не обойтись. Не переживай: чтобы отправлять любые HTTP запросы, можно воспользоваться программой Postman. Скачать её можно отсюда. После скачивания и установки, приступаем к тестированию нашего приложения. Для этого открываем программу и создаем новый запрос: Обзор REST. Часть 3: создание RESTful сервиса на Spring Boot - 9Нажимаем кнопку New в левом верхнем углу. Далее выбираем Request: Обзор REST. Часть 3: создание RESTful сервиса на Spring Boot - 10Далее задаем ему имя и сохраняем его. Попробуем теперь отправить POST запрос на сервер и создать первого клиента: Обзор REST. Часть 3: создание RESTful сервиса на Spring Boot - 11Создаем таким образом несколько клиентов. Затем меняем тип запроса на GET и отправляем его на сервер: Обзор REST. Часть 3: создание RESTful сервиса на Spring Boot - 12

Общие итоги

Поздравляю: мы рассмотрели довольно тему REST. Весь материал получился объемным, но, надеемся, полезным для тебя:
  1. Мы узнали, что такое REST.

  2. Познакомились с историей возникновения REST.

  3. Поговорили об ограничениях и принципах данного архитектурного стиля:

    • приведение архитектуры к модели клиент-сервер;
    • отсутствие состояния;
    • кэширование;
    • единообразие интерфейса;
    • слои;
    • код по требованию (необязательное ограничение).
  4. Разобрали преимущества которые дает REST

  5. Подробно рассмотрели, как сервер и клиент взаимодействуют друг с другом по HTTP протоколу.

  6. Поближе познакомились с запросами и ответами. Разобрали их составные части.

  7. Наконец, мы перешли к практике и написали свое небольшое RESTful приложение на Spring Boot. И даже научились его тестировать с помощью программы Postman.

Фуух. Вышло объемно, но, тем не менее есть чем заняться, в качестве домашнего задания.

Домашнее задание

Попробуй сделать следующее:
  1. Следуя описанию выше, создай самостоятельно Spring Boot проект и реализуй в нем ту же логику, что и в лекции. Повтори все 1 в 1.
  2. Запусти. приложение.
  3. Скачай и настрой Postman (либо любой другой инструмент для отправки запросов, хоть curl).
  4. Протестируй запросы POST и GET так же, как было указано в лекции.
  5. Протестируй запросы PUT и DELETE самостоятельно.
Часть 1: что такое REST Часть 2: коммуникация между клиентом и сервером
Комментарии (92)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
И. Ж.
Уровень 41
21 апреля, 12:08
Приложение не запускается, пишет ошибку "Parameter 0 of constructor in ClientController required a bean of type 'ClientService' that could not be found." Подчеркивало красным конструктор ClientController(ClientService clientService), в итоге добавил аннотацию @Service перед интерфейсом, подчеркивания не стало, но та же ошибка осталась, не запускается. Хз что надо добавить, не хватает
И. Ж.
Уровень 41
21 апреля, 12:45
В итоге заработало когда в конструкторе вместо интерфейса поставил класс, то есть так: private final ClientServiceImpl clientService; @Autowired public ClientController(ClientServiceImpl clientService) { this.clientService = clientService; } Это уже методом тыка, раз пишет конструктор ноль, то есть бин компонент требует, который якобы не может найти, то всунул реализованный класс. Почему так, как разобраться без прямой подсказки в таких моментах, когда только вникаешь в процесс.. Без стажировки наверное никак, где есть возможность спрашивать у менторов и подобное. Самостоятельно вытянуть, разобраться, заставить работать свои творения дело очень сложное.
DeanCage Backend Developer
20 января, 14:15
Если кто-то сталкивается с ситуации что идея подчеркивает List<Client> в классе ClientController Посмотрите скриншот и вы поймете что автор накосячил в своей статье! там должно анатация быть не @GetMapping а должна быть анатация @PutMapping. на эту ошибку я потратил 3 дня рвя волосы на жопе и думая почему не запускается.
Максим Li Java Developer
20 ноября 2023, 05:46
Отлично!
Максим Li Java Developer
26 ноября 2023, 04:25
Повторил туториал один в один, все работает! Спасибо! Теперь на основе этого мини-приложения можно сделать что-то более интересное!
Кирилл
Уровень 35
26 июля 2023, 17:19
Это очень круто!!! Спасибо!!
Anonymous #3293848 Backend Developer
17 апреля 2023, 08:40
Благодарю за урок! Всё просто и понятно, а, самое главное, работает!
TemaCode
Уровень 51
21 февраля 2023, 17:12
я правильно понял, что запуск RestExampleApplication даст создания серверного сокета на локальной машине на порту 8080?
Thespawn007
Уровень 19
Expert
2 февраля 2023, 19:43
Для тех кто не понимает почему аннотации @RestController и другие светятся красным, хотя все верно настроили - Перепишите их руками, возможно баг Идеи, но у меня заработали👍 За статью спасибо, все работает!
Svetlana Vydrina
Уровень 41
8 июля 2022, 23:53
Прописала в Контролёре не ту аннотацию к методу update: GetMapping вместо PutMapping. В итоге приложение не запускалось, я часа три искала баг. Полезные поиски, в общем-то
Oleg Khilko
Уровень 51
11 августа 2022, 11:00
Наши ошибки делают нас сильнее))) Ты молодец что все-таки добила эту тему! Я вчера помогал другу запустить тестовый проект для стажировки. Это был пазл на три часа почему mySQL не хочет коннектиться и что за Driver ему нужен. Причем себе все поставил элементарно и быстро и даже не заметил никаких сложностей, зато потом прикурил у него на проекте, но все равно это опыт, мать его за ногу)
Svetlana Vydrina
Уровень 41
14 августа 2022, 11:43
А в чём была проблема в итоге?
Oleg Khilko
Уровень 51
14 августа 2022, 13:04
У него уже был mySQL, но требовал Driver. Установка этого драйвера ничего не меняла, как зацикленный круг получался (возможно проблема в том что у него был MySQL Workbench и что-то файлу init.sql не нравилось). По итогу пробовали и так и эдак, но самым простым оказалось все снести и переставить заново. Потом выяснилось что он ставил MySQL Workbench давно и прописывал там какой-то пароль к пользователю root, который он разумеется забыл 10 раз. Пришлось нового пользователя прописывать ему и давать права доступа на все папки (по дефолту этих прав нет). Ну вот, поэтому простая инструкция "скачайте mySQL и запустите такой-то скрипт" превратилась в ад на 3 часа и поиски ответов на SoF и у индусов на ютубе. Мораль: гораздо проще выполнять инструкции с нуля, чем пытаться что-то куда-то приколхозить, реально в разы больше времени тратишь на все эти телодвижения. Причем самый прикол был в том что у меня на маке все получилось с полпинка, а на винде прям кровь и слезы - не знаю, может проблема в пользователе конечно, но больше я на уговоры "а помоги мне запустить такой-то проект" не ведусь))
DeanCage Backend Developer
20 января, 14:12
Боже! Да храни тебя господь!!! я 3 дня себе мозги выносил с ИИ и гугление, потом решил комментарии 3-й раз прочитать и попробовать ваш вариант и все запустилось!
Никита
Уровень 28
16 апреля 2022, 14:54
где ссылка на 4 часть?
Veryprosto
Уровень 35
9 апреля 2022, 16:02
Спасибо, ничего лишнего!