Http client¶
Kora предоставляет инструментарий для создания и исполнения http-запросов.
Реализации клиента¶
AsyncHttpClient¶
implementation 'ru.tinkoff.kora:http-client-async'
Для работы через AsyncHttpClient необходимо добавить модуль AsyncHttpClientModule
к своему @KoraApp
.
Нативный JDK client¶
implementation 'ru.tinkoff.kora:http-client-jdk'
Для работы через нативный JDK клиент необходимо добавить модуль JdkHttpClientModule
к своему @KoraApp
Клиент¶
Базовый клиент представляет собой интерфейс HttpClient
public interface HttpClient {
/**
* Result Mono can throw wrapped {@link HttpClientException}
*/
Mono<HttpClientResponse> execute(HttpClientRequest request);
default HttpClient with(HttpClientInterceptor interceptor) {
return request -> interceptor.processRequest(this::execute, request);
}
}
execute
— метод исполнения запросаwith
— метод, позволяющий добавлять различные интерцепторы
На данный момент предоставляется одна реализация клиента на базе org.asynchttpclient.AsyncHttpClient
: ru.tinkoff.kora.http.client.async.AsyncHttpClient
Request builder¶
Для построения запросов вручную можно использовать HttpClientRequestBuilder
, конструктор которого выглядит следующим образом:
public HttpClientRequestBuilder(String method, String uriTemplate) {
this.method = method;
this.uriTemplate = uriTemplate;
}
Генерация клиента¶
Аннотация HttpClient
помечает интерфейс как http client. HttpRoute
, в случае клиента, отмечает маршрут, на который нужно отправить запрос.
@HttpClient
public interface Hello {
@HttpRoute(method = HttpMethod.GET, path = "/hello/{name}")
HttpClientResponse getGreetings(@Path("name") String name, @Query("includeOutdated") String includeOutdated);
@HttpRoute(method = HttpMethod.POST, path = "/hello/")
Greeting addGreeting(Greeting greeting);
}
В этом примере показана как работа с чистым HttpClientResponse
, так и с маппингом тела запроса и ответа. Рассмотрим подробнее:
* @Query
— аннотация, помечающая параметр метода как query-параметр
* @Path
— аннотация, помечающая параметр метода как часть пути
* @Header
— аннотация, позволяющая добавить к запросу заголовок
* @Mapping
— аннотация, позволяющая указать кастомный маппер для преобразования в тело запроса
По умолчанию маппер будет применяться только для 2хх статусов, для всех остальных будет выбрасываться исключение HttpClientResponseException
, выглядящее следующим образом:
public class HttpClientResponseException extends HttpClientException {
private final int code;
private final String httpMessage;
private final HttpHeaders headers;
private final byte[] bytes;
}
Это поведение, не применяется если работать с HttpClientResponse
. Кроме того, можно задать собственные мапперы для любых статусов с помощью аннотации @ResponseCodeMapper
.
Пример:
@HttpRoute(method = HttpMethod.POST, path = "/repos/{owner}/{repo}/issues")
@ResponseCodeMapper(code = 418, mapper = CustomVoidMapper.class)
@ResponseCodeMapper(code = 201, type = Void.class)
void createIssue(Issue issue, @Path("owner") String owner, @Path("repo") String repo);
Код маппера:
class CustomVoidMapper implements HttpClientResponseMapper<Void, Mono<Void>> {
@Override
public Mono<Void> apply(HttpClientResponse response) {
return Mono.empty();
}
}
Можно заметить, что в методе addGreeting
не указаны аннотации для параметра. В таком случае кодогенератор будет считать, что в контейнере можно получить соответствующий экземпляр HttpClientRequestMapper
.
Посмотрим на сгенерированный код клиента:
@Generated("ru.tinkoff.kora.http.client.annotation.processor.ClientClassGenerator")
public class HelloClient implements Hello {
private final HttpClientRequestMapper<Greeting> addGreetingRequestMapper;
private final HttpClientResponseMapper<Greeting, Mono<Greeting>> addGreetingResponseMapper;
private final HttpClient addGreetingClient;
private final int addGreetingRequestTimeout;
private final String addGreetingUrl;
public HelloClient(HttpClient httpClient,
HelloConfig config,
HttpClientRequestMapper<Greeting> addGreetingRequestMapper,
HttpClientResponseMapper<Greeting, Mono<Greeting>> addGreetingResponseMapper) {
this.addGreetingRequestMapper = addGreetingRequestMapper;
this.addGreetingResponseMapper = addGreetingResponseMapper;
var addGreeting = config.apply(httpClient, Hello.class, "addGreeting", config.addGreetingConfig(), "/hello/");
this.addGreetingUrl = addGreeting.url();
this.addGreetingClient = addGreeting.client();
this.addGreetingRequestTimeout = addGreeting.requestTimeout();
}
@Override
public Greeting addGreeting(Greeting greeting) throws HttpClientException {
//здесь реализация
}
}
Для наглядности было убрано всё, связанное с обработкой HttpClientResponse
. Здесь следует остановиться на параметрах конструктора сгенерированного клиента:
httpClient
- http client, через который будут выполняться все запросыconfig
- конфигурация клиента, о ней поговорим чуть нижеaddGreetingRequestMapper
- маппер запросаaddGreetingResponseMapper
- маппер ответа
При использовании аннотации @Mapping
вместо сгенерированного маппера будет использоваться указанный, кроме того, не будет проводиться проверка статусов, пример:
@HttpClient
public interface ClientWithMappers {
@HttpRoute(method = HttpMethod.GET, path = "/repos/{owner}/{repo}/contributors")
@Mapping(value = ContributorListMapper.class)
@Tag(ClientWithMappers.class)
List<Contributor> contributors(@Path("owner") String owner, @Path("repo") String repo);
}
Код маппера:
class ContributorListMapper implements HttpClientResponseMapper<List<Contributor>, Mono<List<Contributor>>> {
private final JsonReader<List<Contributor>> reader;
public ContributorListMapper(JsonReader<List<Contributor>> reader) {this.reader = reader;}
@Override
public Mono<List<Contributor>> apply(HttpClientResponse response) {
return ReactorUtils.toByteArrayMono(response.body())
.handle((bytes, sink) -> {
try {
sink.next(reader.read(bytes));
} catch (IOException e) {
sink.error(new HttpClientDecoderException(e));
}
});
}
}
Конфигурация клиента¶
Кроме клиента, кодогенератор создаёт класс конфигурации для него:
public record HelloConfig(
String url,
@Nullable Integer requestTimeout,
@Nullable Boolean tracingEnabled,
@Nullable Boolean loggingEnabled,
@Nullable HttpClientOperationConfig addGreetingConfig) implements ru.tinkoff.kora.http.client.common.declarative.DeclarativeHttpClientConfig {
}
addGreetingConfig
позволяет переопределить конфигурацию для запроса addGreeting
, а именно requestTimeout
, tracingEnabled
и loggingEnabled
.
По умолчанию для поиска конфигурации будет использован следующий путь httpClient.{lower case class name}
.
В таком случае файл конфигурации будет выглядеть следующим образом:
httpClient.hello {
"url" = "http://localhost:8080"
"requestTimeout" = 10s
"tracingEnabled" = false
"loggingEnabled" = true
'addGreetingConfig" {
"requestTimeout" = 20s
}
}
Интерцепторы¶
Kora предоставляет интерфейс HttpClientInterceptor
для создания кастомных интерцепторов:
public interface HttpClientInterceptor {
Mono<HttpClientResponse> processRequest(Function1<HttpClientRequest, Mono<HttpClientResponse>> chain, HttpClientRequest request);
}
Пример существующей имплементации:
public class RootUriInterceptor implements HttpClientInterceptor {
private final String root;
public RootUriInterceptor(String root) {
this.root = root.endsWith("/")
? root.substring(0, root.length() - 1)
: root;
}
@Override
public Mono<HttpClientResponse> processRequest(Function1<HttpClientRequest, Mono<HttpClientResponse>> chain, HttpClientRequest request) {
var template = request.uriTemplate().startsWith("/")
? request.uriTemplate()
: "/" + request.uriTemplate();
var r = request.toBuilder()
.uriTemplate(this.root + template)
.build();
return chain.invoke(r);
}
}
Для декларативного клиента можно отмечать классы и методы аннотацией @InterceptWith
с указанием интерцептора.
Пример использования в сервисе¶
public final class HelloService {
private final Hello client;
public HelloService(Hello client) {
this.client = client;
}
int getHelloResponseCode(String name) {
return client.getGreetings(name, "true").code();
}
}
Поддержка корутин и Reactor¶
HttpClient Kora поддерживает Kotlin coroutines и Project Reactor из коробки. Примеры:
- Kotlin coroutines:
@HttpClient
interface GithubClientKotlin {
@HttpRoute(method = HttpMethod.GET, path = "/repos/{owner}/{repo}/contributors")
suspend fun contributors(@Path("owner") owner: String, @Path("repo") repo: String): List<GithubClient.Contributor>
@HttpRoute(method = HttpMethod.POST, path = "/repos/{owner}/{repo}/issues")
suspend fun createIssue(issue: GithubClient.Issue, @Path("owner") owner: String, @Path("repo") repo: String)
}
- Mono:
@HttpClient
public interface GithubClientReactive {
@HttpRoute(method = HttpMethod.GET, path = "/repos/{owner}/{repo}/contributors")
Mono<List<Contributor>> contributors(@Path("owner") String owner, @Path("repo") String repo);
@HttpRoute(method = HttpMethod.POST, path = "/repos/{owner}/{repo}/issues")
Mono<Void> createIssue(Issue issue, @Path("owner") String owner, @Path("repo") String repo);
}