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: коммуникация между клиентом и сервером
Комментарии (90)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
DeanCage Уровень 51
20 января 2024
Если кто-то сталкивается с ситуации что идея подчеркивает List<Client> в классе ClientController Посмотрите скриншот и вы поймете что автор накосячил в своей статье! там должно анатация быть не @GetMapping а должна быть анатация @PutMapping. на эту ошибку я потратил 3 дня рвя волосы на жопе и думая почему не запускается.
Максим Li Уровень 36
20 ноября 2023
Отлично!
Кирилл Уровень 35
26 июля 2023
Это очень круто!!! Спасибо!!
Anonymous #3293848 Уровень 19
17 апреля 2023
Благодарю за урок! Всё просто и понятно, а, самое главное, работает!
TemaCode Уровень 51
21 февраля 2023
я правильно понял, что запуск RestExampleApplication даст создания серверного сокета на локальной машине на порту 8080?
Thespawn007 Уровень 19 Expert
2 февраля 2023
Для тех кто не понимает почему аннотации @RestController и другие светятся красным, хотя все верно настроили - Перепишите их руками, возможно баг Идеи, но у меня заработали👍 За статью спасибо, все работает!
Svetlana Vydrina Уровень 41
8 июля 2022
Прописала в Контролёре не ту аннотацию к методу update: GetMapping вместо PutMapping. В итоге приложение не запускалось, я часа три искала баг. Полезные поиски, в общем-то
Никита Уровень 28
16 апреля 2022
где ссылка на 4 часть?
Veryprosto Уровень 35
9 апреля 2022
Спасибо, ничего лишнего!
Anonymous #311541 Уровень 32
8 апреля 2022
по хорошему нужно было не пихать мапу в слой сервиса а сделать отдельный интерфейс для репозитория с соответствующими методами - save, getAll, get(int id) и т.д. Вынести это в отдельный пакет - repo, dao, ... А уже в реализации репозитория в этих методах дергать мапу. Тогда если захочется сделать репозиторий работающий через бд то можно его будет рядом запилить а потом вместо реализации с мапой подсунуть сервису и всё будет работать без переделывания кода сервиса.