Перейти к основному содержимому

Http Requests с Rest Client в Spring Framework 6

Profile picture
Автор
Harpal Singh
Software Engineer
Profile picture
Автор
Namastecode
Оглавление

1. Введение

В этом руководстве мы узнаем, как выполнять HTTP-запросы с помощью HTTP-клиента RestClient, представленного в Spring 6.1 (M2).

Сначала мы разберём API RestClient. Затем посмотрим, как выполнять GET, POST, PUT и DELETE запросы с помощью API RestClient. Наконец, рассмотрим практический пример выполнения HTTP-запросов к OpenAI API и вызова ChatGPT для генерации текста для нас.

2. Понимание RestClient

Некоторое время назад фреймворк Spring представил WebClient для выполнения non-blocking HTTP requests. WeClient использует reactive paradigm и подходит для приложений, которые хорошо работают в asynchronous mode и в streaming scenarios. Поэтому использование его в non-reactive application добавляет ненужную сложность и требует от нас хорошего понимания типов Mono и Flux. Кроме того, WebClient не делает программу более performant, поскольку нам всё равно нужно ждать ответа.

Итак, Spring framework представил API RestClient для выполнения синхронных HTTP-запросов. Более современный подход к классическому RestTemplate. Это не означает, что RestTemplate помечен как deprecated. На самом деле, под капотом они используют одну и ту же инфраструктуру, такую как message converters, request factories и т.д. Тем не менее, нам стоит придерживаться API RestClient, так как он будет в центре внимания будущих версий spring framework и получит больше возможностей.

RestClient использует HTTP libraries, такие как JDK HttpClient, Apache HttpComponents и другие. Он использует библиотеку, которая доступна в classpath. Таким образом, RestClient абстрагирует все детали http client и использует наиболее подходящий из них в зависимости от dependencies, которые есть в нашем проекте.

3. Создаём RestClient

Мы создаём RestClient с помощью метода create(), который возвращает новый экземпляр с настройками по умолчанию:

RestClient simpleRestClient = RestClient.create();

Если мы хотим настроить RestClient, мы можем использовать метод builder() и применить fluent API, чтобы задать необходимые нам настройки:

RestClient customRestClient = RestClient.builder()
 .requestFactory(new HttpComponentsClientHttpRequestFactory())
 .messageConverters(converters -> converters.add(new MappingJackson2HttpMessageConverter()))
 .baseUrl("https://example.com")
 .defaultHeader("Authorization", "Bearer your-token")
 .defaultHeader("Content-Type", "application/json")
 .build();

Наш собственный RestClient использует Apache client для выполнения HTTP requests. Затем мы добавляем собственный message converter, чтобы обрабатывать JSON serialization и deserialization с помощью Jackson. Мы задаём base URL, default headers и content type для всех запросов, сделанных с помощью этого клиента.

Имейте в виду, что большинство этих конфигураций уже настроено по умолчанию. Теперь у нас есть настроенный RestClient, который мы можем использовать для выполнения HTTP-запросов. Мы можем повторно использовать этот экземпляр RestClient во всём приложении без проблем, поскольку он thread-safe.

Мы также можем поэтапно мигрировать с RestTemplate на RestClient и повторно использовать существующие configurations для создания RestClient:

RestTemplate restTemplate = new RestTemplate();
RestClient restClientFromRestTemplate = RestClient.create(restTemplate);

4. Выполнение HTTP-запросов

В этом разделе мы кратко объясним, как выполнять GET, POST, PUT и DELETE запросы с помощью API RestClient. Для демонстрации запросов мы будем использовать тестовый API под названием awesomeapi.com. Для простоты мы будем использовать метод retrieve() для выполнения запросов и получения ответа в виде String.

4.1. Выполнение GET запроса

Самый распространённый тип HTTP-запроса — это GET-запрос, который мы используем для получения данных с сервера. RestClient предоставляет простой способ выполнять GET-запросы с различными опциями для обработки параметров и типов ответа:

String response = simpleRestClient.get()
 .uri("https://awesomeapi.com/string/")
 .retrieve()
 .body(String.class);

record Item(String id, String name, double price) {    
} // JSON: {"id": 123, "name": "Product", "price": 99.99}

Item item = simpleRestClient.get()
 .uri("https://awesomeapi.com/item/{id}", 123)
 .retrieve()
 .body(Item.class);

В этом фрагменте мы выполняем два GET запроса. Первый просто получает необработанный String-ответ. Второй получает JSON-ответ в виде объекта Item с полями name и price. Метод body() автоматически десериализует ответ в валидный экземпляр Item, используя Jackson под капотом. Поэтому, если ответ отличается от структуры Item, будет выброшено исключение.

4.2. Выполнение POST запроса

Чтобы отправить данные на сервер, мы используем запрос POST. Более формально, запросы POST создают новые ресурсы на сервере. Как мы видели с запросами GET, RestClient делает простым отправку JSON-encoded objects в теле запроса:

record ItemRequest(String name, double price) {
} // JSON: {"name": "Product", "price": 99.99}

ItemRequest newItem = new ItemRequest("Product", 99.99);

Item createdItem = simpleRestClient.post()
 .uri("https://awesomeapi.com/item/")
 .contentType(MediaType.APPLICATION_JSON)
 .body(newItem)
 .retrieve()
 .body(Item.class);

В этом примере мы создаём новый объект ItemRequest и отправляем его на сервер. Метод contentType() указывает, что мы отправляем данные в формате JSON, а метод body() автоматически сериализует объект Item в корректный JSON. Обычно сервер отвечает ресурсом, который он создал, и мы можем десериализовать его в объект Item. Разница между Item и ItemRequest заключается в том, что у Item есть поле id, генерируемое сервером.

4.3. Отправка PUT запроса

Предположим, мы хотим обновить существующий resource на server. Для этого мы можем использовать PUT request. Аналогично POST requests, PUT requests включают request body с resource, который мы хотим обновить:

ItemRequest updatedItem = new ItemRequest("Updated Product", 129.99);

Item updated = simpleRestClient.put()
 .uri("https://awesomeapi.com/item/{id}", 123)
 .contentType(MediaType.APPLICATION_JSON)
 .body(updatedItem)
 .retrieve()
 .body(Item.class);

Пример PUT-запроса показывает, как обновить существующий продукт. Мы указываем id продукта в методе uri() и передаём обновлённые данные продукта в request body. response содержит обновлённую информацию о продукте, подтверждённую server.

Обратите внимание, что метод uri() использует синтаксис path variables — удобный способ указать динамические части URI. Некоторые API используют query parameters вместо path variables. Чтобы добавить query parameters, мы можем использовать UriBuilder, задать путь, параметры с помощью метода queryParam(), и, в конце, построить Uri, чтобы передать его в метод uri():    

Item updatedWithQuery = simpleRestClient.put()
 .uri(uriBuilder ->
            uriBuilder.path("https://awesomeapi.com/item/")
 .queryParam("id", 123)
 .build())
 .contentType(MediaType.APPLICATION_JSON)
 .body(updatedItem)
 .retrieve()
 .body(Item.class);

Query parameters добавляются в URI после символа ?. Таким образом, результирующий uri будет https://awesomeapi.com/item/?id=123.

4.4. Выполнение запроса DELETE

Наконец, давайте посмотрим, как удалить ресурс с сервера с помощью запроса DELETE:

simpleRestClient.delete()
 .uri("https://awesomeapi.com/item/{id}", 123)
 .retrieve()
 .toBodilessEntity();

В примере с DELETE мы используем toBodilessEntity(), так как DELETE-запросы обычно не возвращают тело ответа. Синтаксис path variable указывает, какой ресурс удалить.

Если удаление прошло успешно, никакое exception не выбрасывается. Это верно для всех запросов; если сервер отвечает error status code, будет выброшено exception. Мы можем использовать try-catch blocks для обработки этих exceptions, но существуют более разумные способы обработки этих errors. Мы увидим это в следующем разделе.

5. Обработка ошибок с помощью onStatus()

При работе с REST API нам нужно грамотно обрабатывать ошибки. Мы не можем использовать блоки try-catch для каждого запроса, так как это приводит к шаблонному и нечитаемому коду. RestClient предоставляет очень чистый способ обработки ошибок на основе кода статуса ответа. Мы используем метод onStatus() для обработки разных кодов статуса и для более читабельной обработки сбоев:

String id = "123";
item = simpleRestClient.get()
 .uri("https://awesomeapi.com/item/{id}", id)
 .retrieve()
 .onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {
        throw new RuntimeException("Item not found with id: " + id);
 })
 .onStatus(HttpStatusCode::is5xxServerError, (request, response) -> {
        throw new RuntimeException("Service is currently unavailable");
 })
 .body(Item.class);

В этом примере мы выполняем GET запрос, чтобы получить объект Item с конкретным id. Если сервер отвечает с 4xx client error, мы выбрасываем исключение с сообщением о том, что элемент не найден на server. Аналогично, если мы получаем 5xx server error, мы выбрасываем другой RuntimeException, указывающий, что service недоступен. Это показывает, как различать разные типы ошибок; в реальных сценариях нам придётся реализовать логику обработки ошибок в соответствии с требованиями. Например, у нас могут быть временные ошибки, которые требуют логики retry.

6. Понимание retrieve() vs exchange()

retrieve() упрощает HTTP-запросы, при этом покрывая большинство сценариев использования. Однако иногда нам нужен больший контроль над HTTP request и response. Например, может потребоваться прочитать заголовки response, которые не подходят для метода onStatus(). В таких случаях мы можем использовать метод exchange(). Обратите внимание: использование exchange() не будет задействовать status handlers, поскольку он уже возвращает полный response, позволяя нам выполнять любую необходимую обработку ошибок.

Например, мы можем реализовать ту же обработку ошибки 404, что и в предыдущем разделе, используя exchange():

Item itemResponse = simpleRestClient.get()
 .uri("https://awesomeapi.com/item/{id}", id)
 .exchange((request, response) -> {

        HttpHeaders headers = response.getHeaders();
        String etag = headers.getETag();

        if (response.getStatusCode().is4xxClientError()) {
            throw new RuntimeException("Item not found with id: " + id);
 } else if(response.getStatusCode().is5xxServerError()){
            throw new RuntimeException("Service is currently unavailable");
 }

        log.info("Got request with ETAG: " + etag);
        return response.bodyTo(Item.class);
 });

В этом примере мы вручную создали логику обработки ошибок, используя if-else statements. Использование handlers снижает читаемость кода, но позволяет нам получить доступ к headers из response, например к ETag, что в противном случае невозможно при использовании retrieve().

Давайте попробуем более практичный пример вызова реального API.

7. Выполнение HTTP Calls к OpenAI API

Rest API — отличный способ взаимодействовать со сторонним сервисом. В этом разделе мы будем использовать OpenAI API для генерации текста с помощью ChatGPT model. Мы воспользуемся RestClient, чтобы сделать POST запрос к OpenAI API и получить ответ.

Сначала нам нужно получить API key с OpenAI website. Хорошей практикой является не включать API key в codebase и использовать environment variables или безопасный способ его хранения. В этом примере мы предположим, что у нас есть environment variable с именем OPENAI_API_KEY, содержащая API key. Чтобы прочитать её, мы просто используем метод getenv() класса System.

String apiKey = System.getenv("OPENAI_API_KEY");

Далее мы создаём несколько вспомогательных классов record, чтобы представлять структуры запроса и ответа, требуемые OpenAI API:


record ChatRequest(String model, List<Message> messages) {} 
record Message(String role, String content) {}
record ChatResponse(List<Choice> choices) {}
record Choice(Message message) {}

Теперь мы создаём экземпляр RestClient с необходимыми headers для authentication и content type:

RestClient openAiClient = RestClient.builder()
 .baseUrl("https://api.openai.com/v1")
 .defaultHeader("Authorization", "Bearer " + apiKey)
 .defaultHeader("Content-Type", "application/json")
 .build();

Наконец, мы выполняем POST запрос к OpenAI API, чтобы сгенерировать текст с помощью ChatGPT model:

ChatRequest request = new ChatRequest(
    "gpt-4o-mini",
    List.of(new Message("user", "What is the brightest star in our galaxy?"))
);

ChatResponse response = openAiClient.post()
 .uri("/chat/completions")
 .body(request)
 .retrieve()
 .body(ChatResponse.class);

log.info("ChatGPT: " + response.choices().get(0).message().content());

Если всё пойдёт хорошо, мы увидим ответ от ChatGPT:

INFO: ChatGPT: The brightest star in our galaxy, the Milky Way, is typically considered to be **Sagittarius A***. However, strictly speaking, Sagittarius A* is actually the supermassive black hole at the center of the Milky Way. The title of the brightest visible star from Earth is generally given to **Sirius**, which is part of the constellation Canis Major. Sirius is approximately 8.6 light-years away and is about twice as massive as the Sun. It is often referred to as the "Dog Star."

В этом примере показано, как мы можем использовать RestClient для взаимодействия с реальным API, таким как предоставляемое OpenAI, чтобы аккуратно и типобезопасно обрабатывать сложные структуры JSON и требования к аутентификации.

8. Заключение

В этой статье мы познакомились с новым RestClient, введённым в Spring Framework 6.1 в составе нового RestClient API для выполнения HTTP-запросов. Мы узнали, как создать и настроить RestClient, выполнять HTTP-запросы (GET, POST, PUT, DELETE), обрабатывать ошибки и работать с реальными API, такими как OpenAI.

Мы можем посмотреть полный code on Github.

Related

Понимание синтаксиса Cron для @Scheduled в Spring
Давайте узнаем, как использовать аннотацию @Scheduled в Spring с cron expressions.
Testcontainers в Spring Boot Integration Tests
Узнаем, как использовать testcontainers в приложении Spring boot при создании integration tests.
Получить значения, определенные в properties file в Spring
Давайте узнаем, как получить доступ к значениям, определённым в properties file в приложении Spring Boot. Определим свойства в properties file и получим к ним доступ в Spring service.