Лекция 5

Тема: Устройство сети Интернет. Протокол HTTP\HTTPS. Веб-сервисы. Архитектурный стиль REST. Написание REST-контроллеров. Передача параметров в REST-запросах. Формат JSON.

Веб-сервисы

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

Веб-служба или веб-сервис (web-service) – сетевая технология, обеспечивающая межпрограммное взаимодействие на основе веб-стандартов. W3C определяет веб-службу как «программную систему, разработанную для поддержки интероперабельного межкомпьютерного (machine-to-machine) взаимодействия через сеть».

К моменту появления веб-служб уже существовали технологии, позволяющие приложениям взаимодействовать на расстоянии, где одна программа могла вызвать какой-нибудь другой метод в другой программе, которая при этом могла быть запущена на компьютере, расположенном в другом городе или даже стране. Это сокращенно называется RPC (Remote Procedure Calling – удаленный вызов процедур). В качестве примеров можно привести технологии CORBA, а для Java – RMI (Remote Method Invoking – удаленный вызов методов).

Идея веб-службы заключалась в создании такого RPC, который будет упаковываться в HTTP пакеты. Такой подход стал очень популярным, т.к. HTTP был хорошо известен, прост, понятен и обеспечивал лучшее «прохождение» через различные firewall`ы. Именно с появлением веб-сервисов развилась идея SOA – сервис-ориентированной архитектуры веб-приложений (Service Oriented Architecture).

Протокол HTTP

Протокол HTTP лежит в основе обмена данными в Интернете. HTTP является протоколом клиент-серверного взаимодействия, что означает инициирование запросов к серверу самим получателем (браузером или другим клиентским приложением).

Клиенты и серверы взаимодействуют, обмениваясь одиночными сообщениями (а не потоком данных). Сообщения, отправленные клиентом называются запросами, а сообщения, отправленные сервером, называются ответами.

HTTP - это клиент-серверный протокол, то есть запросы отправляются какой-то одной стороной - участником обмена (user-agent). Чаще всего в качестве участника выступает веб-браузер, но им может быть кто угодно.

Каждый запрос (request) отправляется серверу, который обрабатывает его и возвращает ответ (response).

Участник обмена (user agent) - это любой инструмент или устройство, действующее от лица пользователя.

На другой стороне коммуникационного канала расположен сервер, который обслуживает (serve) пользователя, предоставляя ему документы по запросу. С точки зрения конечного пользователя, сервер всегда является некой одной виртуальной машиной, полностью или частично генерирующий документ, хотя фактически он может быть группой серверов, между которыми балансируется нагрузка, то есть перераспределяются запросы различных пользователей, либо сложным программным обеспечением, опрашивающим другие компьютеры.

HTTP-запросы (HTTP-request)

Пример HTTP запроса

Запросы содержат следующие элементы:

  • HTTP-метод, обычно глагол подобно GET, POST или существительное, как OPTIONS или HEAD, определяющее операцию, которую клиент хочет выполнить. Обычно, клиент хочет получить ресурс (используя GET) или передать значения HTML-формы (используя POST), хотя другие операции могут быть необходимы в других случаях;

  • путь к ресурсу;

  • заголовки (опционально), предоставляющие дополнительную информацию для сервера;

  • для некоторых методов, таких как POST, тело метода, которое содержит отправленный ресурс.

HTTP-ответы

Пример HTTP-ответа

Ответы содержат следующие элементы:

  • версию HTTP-протокола;

  • HTTP код состояния, сообщающий об успешности запроса или причине неудачи;

  • сообщение состояния - краткое описание кода состояния;

  • опционально: тело, содержащее пересылаемый ресурс.

Код состояния - это трехзначное число, которое отдает сервер на запрос клиента и благодаря которому корректируется дальнейшая обработка запрашиваемого документа. За числом всегда идет краткое пояснение кода на английском языке, отделенное пробелом - первичная инструкция клиенту.

Классы состояния - группа кодов, объединенных определенными признаками. На класс состояния указывает первая цифра в коде.

Выделяют пять классов:

  • 1ХХ - информационные кода. Они отвечают за процесс передачи данных. Это временные коды, они информируют о том, что запрос принят и обработка будет продолжаться;

  • 2ХХ - успешная обработка. Запрос был получен и успешно обработан сервером;

  • 3ХХ - перенаправление (редирект). Эти ответы сервера гласят, что нужно предпринять дальнейшие действия для выполнения запроса. Например, сделать запрос по другому адресу;

  • 4ХХ - ошибка клиента. Это значит, что запрос не может быть выполнен на стороне клиента;

  • 5ХХ - ошибка сервера. Эти коды возникают из-за ошибок на стороне сервера. В данном случае клиент сделал все правильно, но сервер не может выполнить запрос. Для кодов этого класса сервер обязательно показывает сообщение, что не может обработать запрос и по какой причине.

Протоколы веб-сервисов

На сегодняшний день наибольшее распространение получили следующие протоколы реализации веб-служб:

  • SOAP (Simple Object Access Protocol) – тройка стандартов SOAP/WSDL/UDDI. Сообщения упаковываются в виде структуры, которая называется конверт (envelope), которая включает идентификатор сообщения, заголовок и тело сообщения;

  • REST (Representational State Transfer) – архитектурны стиль, который использует концепцию ресурсов и определяет операции через методы HTTP-протокола;

  • XML-RPC (XML Remote Procedure Call) – вызов удаленных процедур, использующий XML для кодирования своих сообщений и HTTP в качестве транспортного механизма.

Протокол REST

Передача состояния представления (Representational State Transfer (REST)) является архитектурным стилем, в котором веб-службы рассматриваются, как ресурсы и могут быть идентифицированы Унифицированными идентификаторами ресурсов (Uniform Resource Identifiers (URI)).

Веб-службы, разработанные в стиле REST и с учетом ограничений REST, известны как RESTful веб-службы.

Каждая единица информации в REST называется ресурсом и имеет однозначный URI, который является ее, своего рода, первичным ключом. То есть, например, третья книга с книжной полки будет иметь URI /book/3, а 35ая страница в этой книге – /book/3/page/35/. Отсюда и получается строго заданный формат. Причем совершенно не имеет значения, в каком формате находятся данные по адресу /book/3/page/35/ – это может быть и HTML, и отсканированная копия книги в виде jpeg-файла и документ Microsoft Word.

Над ресурсами выполняется ряд простых четко определенных операций. В качестве протокола передачи данных используется stateless-протокол, обычно HTTP.

При использовании протокола HTTP действия над данными выполняются с помощью HTTP-методов: GET (получить), PUT (добавить, заменить), POST (добавить, изменить, удалить), DELETE (удалить). Таким образом, действия CRUD (Create-Read-Update-Delete) могут выполняться как со всеми 4-мя методами, так и только с помощью GET и POST. Примеры запросов:

  • GET /book/ – получить список всех книг;

  • GET /book/3 – получить книгу номер 3;

  • PUT /book/ – добавить книгу (данные в теле запроса);

  • POST /book/3 – изменить книгу (данные в теле запроса);

  • DELETE /book/3 – удалить книгу.

Как правило, необязательно поддерживать все методы, но, как правило, веб-служба должна поддерживать:

  • GET – используется для получения существующих ресурсов;

  • POST – используется для создания/обновления нового ресурса;

  • PUT – используется для обновления/замены ресурса;

  • DELETE – используется для удаления ресурса.

Кроме этого, служба может поддерживать такие методы как PATCH (обновление части ресурса), HEAD (возвращение заголовка ресурса, т.е. метаданных) и т.д.

Разработка Web Layer

Для обработки запросов и возврата данных необходимо предусмотреть соответствующие контроллеры REST-запросов, которые и будут составлять наш веб-слой.

Контроллер – это java-класс, методы которого призваны обрабатывать HTTP-запросы. Отличие обычного контроллера от REST-контроллера заключается в том, что в REST-контроллере каждый метод класса возвращает данные вместо представления. Рассмотрим пример простого REST-контроллера. Создадим в проекте пакет controllers, внутри которого создадим класс HelloController.

HelloController.java
@RestController
public class HelloController {}

Обратите внимание, что мы пометили класс аннотацией @RestController. Таким образом, мы даем знать Spring, что это не просто класс, а контроллер REST-запросов. В классе создадим метод, который будет возвращать строку.

HelloController.java
@RestController
public class HelloController {
    
    public String hello() {
        return "hello";
    }
}

Говорят, что методы контроллера «отображаются» на HTTP-запросы. Это значит, что при поступлении определенного HTTP-запроса (с определенным URL и HTTP-методом), будет вызван определенный метод контроллера, который вернет некоторые данные. Этим данные будут упакованы в HTTP-ответ и высланы обратно клиенту.

Нам необходимо сделать так, чтобы наш созданный метод был вызван, когда на сервер поступит HTTP-запрос с определенным URL, например http://localhost:8080/hello. Для этого необходимо пометить метод аннотацией @GetMapping c параметром (“/hello”) – часть URL, на который будет отображаться данный метод.

HelloController.java
@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

Для каждого из четырех основных HTTP-метода предусмотрена своя аннотация (@GetMapping, @PostMapping, @PutMapping, @DeleteMapping). Метод, помеченный определенной аннотацией, обрабатывает запросы только с определенным HTTP-методом.

Запустим сервер, заходим на http://localhost:8080/hello и видим строку с ответом.

Что произошло? Строка «hello» была помещена в тело HTTP-ответа, браузер получил text/plain с содержимым «hello» и просто вывел его на экран.

Передача параметров в REST-запросах

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

Параметры запроса можно передать несколькими способами. Рассмотрим следующие способы:

  • указание параметра в URL-пути (localhost:8080/rooms/256);

  • указание параметра в строке запроса, которая идет после URL-пути и отделяется символом ? (localhost:8080/rooms?id=256&param2=value2);

  • передача параметров в теле запроса (часто используется для передачи заполненной пользователем формы или передачи данных в формате JSON).

Рассмотрим, каким образом можно получить и обработать параметры запроса, переданные тем или иным способом.

Указание параметра в URL-пути

При создании endpoint, в аннотации необходимо указать вариативную часть и назначить ей идентификатор

@GetMapping("/room/{id}")
public void getRoomById() {
    
}

Далее необходимо предусмотреть входной аргумент метода, куда Spring запишет значение вариативной части и указать аннотацию @PathVariable для этой переменной. Также необходимо указать идентификатор, который вы указали в аннотации @GetMapping.

@GetMapping("/room/{id}")
public void getRoomById(@PathVariable(value = "id") int roomId) {
    // ...
}

В рамках одного запроса может быть несколько вариативных частей, которые можно считать и обработать

@GetMapping("/room/{id1}/{id2}")
public void getRoomById(@PathVariable(value = "id1") int blockId, @PathVariable(value = "id2") int roomId) {
    // ...
}

Указание параметра в строке запроса

В этом случае, для каждого параметра запроса создается входной аргумент, указывается аннотация @RequestParam, а также указывается имя параметра.

// запрос: http://localhost:8080/room?room_id=250&block_id=10
@GetMapping("/room")
public void getRoomById(@RequestParam(value = "room_id") int roomId, @RequestParam(value = "block_id") int blockId) {
    // ...
}

Передача параметров в теле запроса

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

Форма может иметь следующие MIME-типы:

  • application/x-www-form-urlencoded: значения кодируются в кортежах с ключом, разделенных символом '&', с '=' между ключом и значением. Не буквенно-цифровые символы - percent encoded: это причина, по которой этот тип не подходит для использования с двоичными данными (вместо этого используйте multipart/form-data);

  • multipart/form-data: каждое значение посылается как блок данных ("body part"), с заданными пользовательским клиентом разделителем ("boundary"), разделяющим каждую часть. Эти ключи даются в заголовки Content-Disposition каждой части text/plain.

Для обработки данных формы необходимо создать входной аргумент для каждого параметра, для каждого входного аргумента указать аннотацию @RequestParam, а также имя параметра.

@PostMapping("/book")
public void bookRoom(@RequestParam(value = "room_id") int room_id,
                     @RequestParam(value = "firstname") String firstname,
                     @RequestParam(value = "lastname") String lastname,
                     @RequestParam(value = "days") int days) {
    // ...
}

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

Передача данных между клиентом и сервером.

Так как язык Java является ОО языком, нам было бы удобно работать с входящими и исходящими данными в объектном виде - было бы здорово, если бы REST-контроллер возвращал бы данные в виде объекта некоторого класса, а не в виде набора полей со значениями. Также было бы здорово, чтобы мы могли просто возвращать клиенту объект или коллекцию объектов некоторых классов без необходимости формировать Map из полей и значений.

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

Сериализация - это преобразование объекта в последовательность байтов, так что объект можно легко сохранить в постоянное хранилище или передать по каналу связи. Затем поток байтов можно десериализовать - преобразовать в реплику исходного объекта.

Язык Java предоставляет стандартный механизм Java Serialization API для создания сериализуемых объектов, однако, он нам не подходит, так как ограничивает возможности для использования различных языков и технологий на стороне клиента и сервера.

Мы можем использовать сторонние библиотеки для сериализации объекта с помощью формата XML или JSON.

Использование формата JSON (http://bit.ly/32ZelBq) является более предпочтительным. Для сериализации и десериализации в Spring по-умолчанию используется библиотека Jackson.

Библиотека Jackson позволяет гибко настроить процесс сериализаци и десериализации, однако, в рамках данного курса мы будем использовать стандартные механизмы сериализации и десериализации, чтобы уделять этому процессу как можно меньше внимания.

Рассмотрим ситуацию, когда нам необходимо вернуть клиенту данные в объектном виде. Создадим класс с несколькими полями, создадим объект и вернем его в качестве результата GET-запроса.

    @GetMapping("/roominfo")
    public Room getRoomInfoById(@RequestParam(value = "room_id") int roomId) {
        return new Room(roomId, "Отличная комната!", 2, 200);
    }

Обратите внимание, что в код класса Room не зря были включены геттеры и сеттеры. Их наличие обязательно для сериализации и десериализации!

Используем Postman для эмуляции клиента, сделаем GET-запрос и получим следующий результат

Как мы видим, поля объекта были сериализованы с помощью формата JSON. Теперь клиент, после получения этих данных, сможет с помощью процесса десериализации получить объект и удобно работать с ним.

Теперь рассмотрим обратную ситуацию. Клиент делает POST-запрос и передает в теле запроса данные о новом студенте.

На стороне сервера создаем класс Student с соответствующими полями.

Student.java
public class Student {

    private String firstname;
    private String lastname;
    private String phone;
    private int age;

    public Student(String firstname, String lastname, String phone, int age) {
        this.firstname = firstname;
        this.lastname = lastname;
        this.phone = phone;
        this.age = age;
    }

    public String getFirstname() {
        return firstname;
    }

    public void setFirstname(String firstname) {
        this.firstname = firstname;
    }

    public String getLastname() {
        return lastname;
    }

    public void setLastname(String lastname) {
        this.lastname = lastname;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "firstname='" + firstname + '\'' +
                ", lastname='" + lastname + '\'' +
                ", phone='" + phone + '\'' +
                ", age=" + age +
                '}';
    }
}

Создаем конечную точку для обработки запроса. Обратите внимание, что мы используем аннотацию @RequestBody.

@PostMapping("/student")
public void addStudent(@RequestBody Student student) {
    System.out.println(student);
}

Конфигурация HTTP-response с помощью класса ResponseEntity

Далеко не всегда ответ сервера состоит в возврате какого-то значения или какого-то объекта. Очень часто необходимо вернуть ответ с определенным HTTP-кодом и сообщением об ошибке, указать определенный заголовок и так далее.

В этом случае необходимо использовать класс ResponseEntity. Класс ResponseEntity является оберткой для ответа и дополнительно для HTTP заголовков и кода статуса. Он является обобщенным, что позволяет использовать любой тип в качестве тела ответа.

@GetMapping("/student")
public ResponseEntity<?> getStudentById(@RequestParam(value = "id") int studentId) {
    if (studentId < 1) {
        return ResponseEntity.badRequest().body("Invalid id");
    } else {
        return ResponseEntity.ok(new Student("Ivan", "Ivanov", "223322", 20));
    }
}

Подробную информацию по поводу ResponseEntity читайте здесь - https://www.baeldung.com/spring-response-entity.

Last updated