Декларативный HTTP-клиент
Микросервисы постоянно обращаются друг к другу по HTTP. В Micronaut для этого не нужно вручную собирать запрос и разбирать JSON: достаточно описать интерфейс с аннотациями, как у контроллера, — реализацию сгенерирует обработчик аннотаций. Такой клиент называется декларативным: вы объявляете, что нужно, а не пишете, как это сделать.
Для примера блог будет импортировать статьи из учебного API jsonplaceholder.typicode.com — у него есть адрес /posts/{id}, который отдаёт пост в JSON.
Зависимость
Генератор уже добавил micronaut-http-client, но с <scope>test</scope> — только для тестов. Чтобы клиент работал в самом приложении, поменяйте область видимости в pom.xml:
<!-- pom.xml -->
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-http-client</artifactId>
<scope>compile</scope>
</dependency>Модель и клиент
Запись для ответа чужого сервиса:
// src/main/java/com/example/ExternalPost.java
package com.example;
import io.micronaut.serde.annotation.Serdeable;
@Serdeable
public record ExternalPost(Long id, Long userId, String title, String body) {
}Интерфейс клиента:
// src/main/java/com/example/PlaceholderClient.java
package com.example;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.client.annotation.Client;
import java.util.List;
import java.util.Optional;
@Client(id = "placeholder")
public interface PlaceholderClient {
@Get("/posts/{id}")
Optional<ExternalPost> fetchPost(Long id);
@Get("/posts?userId={userId}")
List<ExternalPost> fetchByUser(Long userId);
}id = "placeholder" — имя сервиса. Его адрес задаётся в настройках, а не в коде:
# src/main/resources/application.properties
micronaut.http.services.placeholder.url=https://jsonplaceholder.typicode.comТак адрес легко поменять для тестов или сервера. Можно указать адрес и прямо: @Client("https://jsonplaceholder.typicode.com").
Использование
Клиент — обычный бин, его внедряют через конструктор:
// src/main/java/com/example/ImportController.java
package com.example;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.exceptions.HttpStatusException;
import io.micronaut.scheduling.TaskExecutors;
import io.micronaut.scheduling.annotation.ExecuteOn;
@ExecuteOn(TaskExecutors.BLOCKING)
@Controller("/import")
public class ImportController {
private final PlaceholderClient placeholderClient;
private final ArticleService articleService;
public ImportController(PlaceholderClient placeholderClient, ArticleService articleService) {
this.placeholderClient = placeholderClient;
this.articleService = articleService;
}
@Post("/{id}")
public Article importPost(Long id) {
ExternalPost post = placeholderClient.fetchPost(id)
.orElseThrow(() -> new HttpStatusException(HttpStatus.NOT_FOUND, "Поста " + id + " нет на удалённом сервере"));
return articleService.create(new ArticleForm(post.title(), post.body()));
}
}Вызов клиента блокирует поток до ответа, поэтому контроллер снова помечен @ExecuteOn(TaskExecutors.BLOCKING).
Проверка
curl -X POST http://localhost:8080/import/2{"id":1,"title":"qui est esse","body":"est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\n..."}Пост с удалённого сервера сохранился как статья блога. Для несуществующего поста:
curl -X POST http://localhost:8080/import/1000{"_links":{"self":[{"href":"/import/1000","templated":false}]},"_embedded":{"errors":[{"message":"Поста 1000 нет на удалённом сервере"}]},"message":"Not Found"}Если метод клиента возвращает Optional, ответ 404 превращается в пустой Optional. Другие ошибки (500, 401) клиент выбрасывает как HttpClientResponseException.
Частая ошибка
Если оставить зависимость с <scope>test</scope>, основной код не скомпилируется:
[ERROR] PlaceholderClient.java:[4,43] package io.micronaut.http.client.annotation does not existВ тестах клиент работает, а в приложении нет — проверьте область видимости micronaut-http-client в pom.xml.