Kora фреймворк для написания Java / Kotlin приложений с упором на производительность, эффективность, прозрачность сделанный разработчиками Т-Банк / Тинькофф

Kora is a framework for writing Java / Kotlin applications with a focus on performance, efficiency, transparency made by T-Bank / Tinkoff developers

Перейти к содержанию

HTTP клиент

Модуль предоставляет тонкий слой абстракции для создания HTTP-клиентов с помощью аннотаций в декларативном стиле, либо использование клиентов в императивном стиле.

AsyncHttpClient

Реализация HTTP клиента основанная на библиотеке Async HTTP Client.

Подключение

Зависимость build.gradle:

annotationProcessor "ru.tinkoff.kora:annotation-processors"
implementation "ru.tinkoff.kora:http-client-async"

Модуль:

@KoraApp
public interface Application extends AsyncHttpClientModule { }

Зависимость build.gradle.kts:

ksp("ru.tinkoff.kora:symbol-processors")
implementation("ru.tinkoff.kora:http-client-async")

Модуль:

@KoraApp
interface Application : AsyncHttpClientModule

Конфигурация

Пример полной конфигурации, описанной в классе AsyncHttpClientConfig и HttpClientConfig (указаны примеры значений или значения по умолчанию):

httpClient {
    async {
        followRedirects = true //(1)!
    }
    connectTimeout = "5s" //(2)!
    readTimeout = "2m" //(3)!
    useEnvProxy = false //(4)!
    proxy {
        host = "localhost"  //(5)!
        port = 8090  //(6)!
        user = "user"  //(7)!
        password = "password"  //(8)!
        nonProxyHosts = [ "host1", "host2" ]  //(9)!
    }
}
  1. Следовать ли по перенаправлениям в HTTP
  2. Максимальное время на установление соединения
  3. Максимальное время на чтение ответа
  4. Использовать ли переменные окружения для настройки прокси
  5. Адрес прокси (по умолчанию отсутвует)
  6. Порт прокси (по умолчанию отсутвует)
  7. Пользователь для прокси (по умолчанию отсутвует)
  8. Пароль для прокси (по умолчанию отсутвует)
  9. Хосты которые следует исключить из проксирования (по умолчанию отсутвует)
httpClient:
  async:
    followRedirects: true #(1)!
  connectTimeout: "5s" #(2)!
  readTimeout: "2m" #(3)!
  useEnvProxy: false #(4)!
  proxy:
    host: "localhost"  #(5)!
    port: 8090  #(6)!
    user: "user"  #(7)!
    password: "password"  #(8)!
    nonProxyHosts: [ "host1", "host2" ]  #(9)!
  1. Следовать ли по перенаправлениям в HTTP
  2. Максимальное время на установление соединения
  3. Максимальное время на чтение ответа
  4. Использовать ли переменные окружения для настройки прокси
  5. Адрес прокси (по умолчанию отсутвует)
  6. Порт прокси (по умолчанию отсутвует)
  7. Пользователь для прокси (по умолчанию отсутвует)
  8. Пароль для прокси (по умолчанию отсутвует)
  9. Хосты которые следует исключить из проксирования (по умолчанию отсутвует)

Можно также настроить Netty транспорт.

OkHttp

Реализация HTTP клиента основанная на библиотеке OkHttp. Учитывайте что реализация написана на Kotlin и использует соответствующие зависимости.

Подключение

Зависимость build.gradle:

annotationProcessor "ru.tinkoff.kora:annotation-processors"
implementation "ru.tinkoff.kora:http-client-ok"

Модуль:

@KoraApp
public interface Application extends OkHttpClientModule { }

Зависимость build.gradle.kts:

ksp("ru.tinkoff.kora:symbol-processors")
implementation("ru.tinkoff.kora:http-client-ok")

Модуль:

@KoraApp
interface Application : OkHttpClientModule

Конфигурация

Пример полной конфигурации, описанной в классе OkHttpClientConfig и HttpClientConfig (указаны примеры значений или значения по умолчанию):

httpClient {
    ok {
        followRedirects = true //(1)!
        httpVersion = "HTTP_1_1" //(2)!
    }
    connectTimeout = "5s" //(3)!
    readTimeout = "2m" //(4)!
    useEnvProxy = false //(5)!
    proxy {
        host = "localhost" //(6)!
        port = 8090 //(7)!
        user = "user" //(8)!
        password = "password" //(9)!
        nonProxyHosts = [ "host1", "host2" ] //(10)!
    }
}
  1. Следовать ли по перенаправлениям в HTTP
  2. Максимальная используемая версия HTTP протокола (доступные значения: HTTP_1_1 / HTTP_2 / HTTP_3)
  3. Максимальное время на установление соединения
  4. Максимальное время на чтение ответа
  5. Использовать ли переменные окружения для настройки прокси
  6. Адрес прокси (по умолчанию отсутвует)
  7. Порт прокси (по умолчанию отсутвует)
  8. Пользователь для прокси (по умолчанию отсутвует)
  9. Пароль для прокси (по умолчанию отсутвует)
  10. Хосты которые следует исключить из проксирования (по умолчанию отсутвует)
httpClient:
  ok:
    followRedirects: true #(1)!
    httpVersion: "HTTP_1_1" #(2)!
  connectTimeout: "5s" #(3)!
  readTimeout: "2m" #(4)!
  useEnvProxy: false #(5)!
  proxy:
    host: "localhost" #(6)!
    port: 8090  #(7)!
    user: "user"  #(8)!
    password: "password" #(9)!
    nonProxyHosts: [ "host1", "host2" ] #(10)!
  1. Следовать ли по перенаправлениям в HTTP
  2. Максимальная используемая версия HTTP протокола (доступные значения: HTTP_1_1 / HTTP_2 / HTTP_3)
  3. Максимальное время на установление соединения
  4. Максимальное время на чтение ответа
  5. Использовать ли переменные окружения для настройки прокси
  6. Адрес прокси (по умолчанию отсутвует)
  7. Порт прокси (по умолчанию отсутвует)
  8. Пользователь для прокси (по умолчанию отсутвует)
  9. Пароль для прокси (по умолчанию отсутвует)
  10. Хосты которые следует исключить из проксирования (по умолчанию отсутвует)

Конфигуратор

Пример настройки построителя OkHttp клиента, OkHttpConfigurer должен быть доступен как компонент:

@Component
public class SomeConfigurer implements OkHttpConfigurer {

    @Override
    public OkHttpClient.Builder configure(OkHttpClient.Builder builder) {
        return builder;
    }
}
@Component
class SomeConfigurer : OkHttpConfigurer {
    fun configure(builder: Builder): Builder {
        return builder
    }
}

Нативный клиент

Реализация HTTP клиента на основании нативного клиента поставляемого в JDK.

Подключение

Зависимость build.gradle:

annotationProcessor "ru.tinkoff.kora:annotation-processors"
implementation "ru.tinkoff.kora:http-client-jdk"

Модуль:

@KoraApp
public interface Application extends JdkHttpClientModule { }

Зависимость build.gradle.kts:

ksp("ru.tinkoff.kora:symbol-processors")
implementation("ru.tinkoff.kora:http-client-jdk")

Модуль:

@KoraApp
interface Application : JdkHttpClientModule

Конфигурация

Пример полной конфигурации, описанной в классе JdkHttpClientConfig и HttpClientConfig (указаны примеры значений или значения по умолчанию):

httpClient {
    jdk {
        threads = 2 //(1)!
        httpVersion = "HTTP_1_1" //(2)!
    }
    connectTimeout = "5s" //(3)!
    useEnvProxy = false //(4)!
    proxy {
        host = "localhost" //(5)!
        port = 8090 //(6)!
        user = "user" //(7)!
        password = "password" //(8)!
        nonProxyHosts = [ "host1", "host2" ] //(9)!
    }
}
  1. Количество потоков для HTTP клиента, по умолчанию равен кол-во ядер процессора умноженных на 2
  2. Какую версию HTTP протокола использовать (доступные значения: HTTP_1_1 / HTTP_2)
  3. Максимальное время на установление соединения
  4. Использовать ли переменные окружения для настройки прокси
  5. Адрес прокси (по умолчанию отсутвует)
  6. Порт прокси (по умолчанию отсутвует)
  7. Пользователь для прокси (по умолчанию отсутвует)
  8. Пароль для прокси (по умолчанию отсутвует)
  9. Хосты которые следует исключить из проксирования (по умолчанию отсутвует)
httpClient:
  jdk:
    threads: 2 #(1)!
    httpVersion: "HTTP_1_1" #(2)!
  connectTimeout: "2s" #(3)!
  useEnvProxy: false #(4)!
  proxy:
    host: "localhost" #(5)!
    port: 8090 #(6)!
    user: "user" #(7)!
    password: "password" #(8)!
    nonProxyHosts: [ "host1", "host2" ] #(9)!
  1. Количество потоков для HTTP клиента, по умолчанию равен кол-во ядер процессора умноженных на 2
  2. Какую версию HTTP протокола использовать (доступные значения: HTTP_1_1 / HTTP_2)
  3. Максимальное время на установление соединения
  4. Использовать ли переменные окружения для настройки прокси
  5. Адрес прокси (по умолчанию отсутвует)
  6. Порт прокси (по умолчанию отсутвует)
  7. Пользователь для прокси (по умолчанию отсутвует)
  8. Пароль для прокси (по умолчанию отсутвует)
  9. Хосты которые следует исключить из проксирования (по умолчанию отсутвует)

Клиент декларативный

Предлагается использовать специальные аннотации для создания декларативного клиента:

  • @HttpClient — указывает что интерфейс является декларативным HTTP клиентом
  • @HttpRoute — указывает тип HTTP запроса и путь запроса
@HttpClient
public interface SomeClient {

    @HttpRoute(method = HttpMethod.GET, path = "/hello/world") 
    void hello(); 
}
@HttpClient
interface SomeClient {

    @HttpRoute(method = HttpMethod.GET, path = "/hello/world")
    fun hello()
}

Конфигурация клиента

Конфигурация конкретной реализации @HttpClient по умолчанию для поиска конфигурации использует следующий путь httpClient.{имя класса в нижнем регистре}, либо указывается в параметре configPath в аннотации:

@HttpClient(configPath = "path.to.config") //(1)!
public interface SomeClient {

    @HttpRoute(method = HttpMethod.GET, path = "/hello/world")
    void hello();
}
  1. Путь до конфигурации конкретно этого клиента
@HttpClient(configPath = "path.to.config") //(1)!
interface SomeClient {

    @HttpRoute(method = HttpMethod.GET, path = "/hello/world")
    fun hello()
}
  1. Путь до конфигурации конкретно этого клиента

Пример конфигурации в случае пути path.to.config описанной в классе DeclarativeHttpClientConfig:

path {
    to {
        config {
            url = "https://localhost:8090" //(1)!
            requestTimeout = "10s" //(2)!
            telemetry {
                logging {
                    enabled = false //(3)!
                }
                metrics {
                    enabled = true //(4)!
                    slo = [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] //(5)!
                }
                tracing {
                    enabled = true //(6)!
                }
            }
        }
    }
}
  1. URL сервиса куда будут отправляться запросы
  2. Максимальное время запроса
  3. Включает логгирование модуля (по умолчанию false)
  4. Включает метрики модуля (по умолчанию true)
  5. Настройка SLO для DistributionSummary метрики
  6. Включает трассировку модуля (по умолчанию true)
path:
  to:
    config:
      url: "https://localhost:8090" #(1)!
      requestTimeout: "10s" #(2)!
      telemetry:
        logging:
          enabled: false #(3)!
        metrics:
          enabled: true #(4)!
          slo: [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] #(5)!
        telemetry:
          enabled: true #(6)!
  1. URL сервиса куда будут отправляться запросы
  2. Максимальное время запроса
  3. Включает логгирование модуля (по умолчанию false)
  4. Включает метрики модуля (по умолчанию true)
  5. Настройка SLO для DistributionSummary метрики
  6. Включает трассировку модуля (по умолчанию true)

Конфигурация метода

На примере выше рассмотренного HTTP клиента, можно настроить отдельно часть параметров для определенного метода, путь к конфигурации определяется путем к клиенту и именем метода, в примере выше конфигурация path.to.config и метода hello финальный путь будет path.to.config.getHello

path {
    to {
        config {
            hello {
                requestTimeout = "10s" //(1)!
                telemetry {
                    logging {
                        enabled = false //(2)!
                    }
                    metrics {
                        enabled = true //(3)!
                        slo = [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] //(4)!
                    }
                    tracing {
                        enabled = true //(5)!
                    }
                }
            }
        }
    }
}
  1. Максимальное время запроса метода
  2. Включает логгирование модуля (по умолчанию false)
  3. Включает метрики модуля (по умолчанию true)
  4. Настройка SLO для DistributionSummary метрики
  5. Включает трассировку модуля (по умолчанию true)
path:
  to:
    config:
      hello:  
        requestTimeout: "10s" #(1)!
        telemetry:
          logging:
            enabled: false #(2)!
          metrics:
            enabled: true #(3)!
            slo: [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] #(4)!
          telemetry:
            enabled: true #(5)!
  1. Максимальное время запроса метода
  2. Включает логгирование модуля (по умолчанию false)
  3. Включает метрики модуля (по умолчанию true)
  4. Настройка SLO для DistributionSummary метрики
  5. Включает трассировку модуля (по умолчанию true)

Запрос

Секция описывает преобразования HTTP запроса у декларативного HTTP клиента. Предлагается использовать специальные аннотации для указания параметров запроса.

Параметр пути

@Path — обозначает значение части пути запроса, сам параметр указывается в {кавычках} в пути и имя параметра указывается в value либо по умолчанию равно имени аргумента метода.

@HttpClient
public interface SomeClient {

    @HttpRoute(method = HttpMethod.GET, path = "/hello/{pathName}") 
    void hello(@Path("pathName") String pathValue); 
}
@HttpClient
interface SomeClient {

    @HttpRoute(method = HttpMethod.GET, path = "/hello/{pathName}")
    fun hello(@Path("pathName") pathValue: String)
}

Параметр запроса

@Query — значение параметра запроса, имя параметра указывается в value либо по умолчанию равно имени аргумента метода.

@HttpClient
public interface SomeClient {

    @HttpRoute(method = HttpMethod.GET, path = "/hello/world") 
    void hello(@Query("queryName") String queryValue,
               @Query("queryNameList") List<String> queryValues); 
}
@HttpClient
interface SomeClient {

    @HttpRoute(method = HttpMethod.GET, path = "/hello/world")
    fun hello(@Query("queryName") queryValue: String, 
              @Query("queryNameList") queryValues: List<String>)
}

Можно отправлять параметры запроса в формате ключ и значение, для этого предполагается использовать тип Map, где ключом является имя параметра и обязано иметь тип String, а значение параметра может быть любым типом и будет обработано через String.valueOf():

@HttpClient
public interface SomeClient {

    @HttpRoute(method = HttpMethod.GET, path = "/hello/world") 
    void hello(@Query Map<String, String> queryValues); 
}
@HttpClient
interface SomeClient {

    @HttpRoute(method = HttpMethod.GET, path = "/hello/world")
    fun hello(@Query queryValues: Map<String, String>)
}

Заголовок

@Header — значение заголовка запроса, имя параметра указывается в value либо по умолчанию равно имени аргумента метода.

@HttpClient
public interface SomeClient {

    @HttpRoute(method = HttpMethod.GET, path = "/hello/world") 
    void hello(@Header("headerName") String headerValue, 
               @Header("headerNameList") List<String> headerValues); 
}
@HttpClient
interface SomeClient {

    @HttpRoute(method = HttpMethod.GET, path = "/hello/world")
    fun hello(@Header("headerName") headerValue: String, 
              @Header("headerNameList") headerValues: List<String>)
}

Можно отправлять параметры запроса в формате ключ и значение, для этого предполагается использовать HttpHeaders тип либо тип Map, где ключом является имя параметра и обязано иметь тип String, а значение параметра может быть любым типом и будет обработано через String.valueOf():

@HttpClient
public interface SomeClient {

    @HttpRoute(method = HttpMethod.GET, path = "/hello/world") 
    void hello(@Header HttpHeaders headers); 
}
@HttpClient
interface SomeClient {

    @HttpRoute(method = HttpMethod.GET, path = "/hello/world")
    fun hello(@Header headers: HttpHeaders)
}

Тело запроса

Для указания тела запроса требуется использовать аргумент метода без специальных аннотации, по умолчанию поддерживаются такие типы как byte[], ByteBuffer или String.

Json

Для указания, что тело является Json и ему требуется автоматически создать такого писателя и внедрить его, требуется использовать аннотацию @Json:

@HttpClient
public interface SomeClient {

    record MyBody(String name) { }

    @HttpRoute(method = HttpMethod.POST, path = "/hello/world")
    void hello(@Json MyBody body); //(1)!
}
  1. Указывает что тело должно быть записано как Json
@HttpClient
interface SomeClient {

    data class MyBody(val name: String) { }

    @HttpRoute(method = HttpMethod.POST, path = "/hello/world")
    fun hello(@Json body: MyBody) //(1)!
}
  1. Указывает что тело должно быть записано как Json

Требуется подключить модуль Json.

Текстовая форма

Можно использовать FormUrlEncoded как тип аргумента тела форма данных.

@HttpClient
public interface SomeClient {

    @HttpRoute(method = HttpMethod.POST, path = "/hello/world")
    void hello(FormUrlEncoded body);
}
@HttpClient
interface SomeClient {

    @HttpRoute(method = HttpMethod.POST, path = "/hello/world")
    fun hello(body: FormUrlEncoded): 
}

Пример вызова метода с такой формой будет выглядеть так:

var response = httpClient.formEncoded(new FormUrlEncoded(
        new FormUrlEncoded.FormPart("name", "Bob"),
        new FormUrlEncoded.FormPart("password", "12345")
));
val response = httpClient.formEncoded(
    FormUrlEncoded(
        FormUrlEncoded.FormPart("name", "Bob"),
        FormUrlEncoded.FormPart("password", "12345")
    )
)
Бинарная форма

Можно использовать FormMultipart как тип аргумента тела бинарная форма.

@HttpClient
public interface SomeClient {

    @HttpRoute(method = HttpMethod.POST, path = "/hello/world")
    void hello(FormMultipart body);
}
@HttpClient
interface SomeClient {

    @HttpRoute(method = HttpMethod.POST, path = "/hello/world")
    fun hello(body: FormMultipart): 
}

Пример вызова метода с такой формой будет выглядеть так:

var response = httpClient.formMultipart(new FormMultipart(List.of(
        FormMultipart.data("field1", "some data content"),
        FormMultipart.file("field2", "example1.txt", "text/plain", "some file content".getBytes(StandardCharsets.UTF_8))
)));
val response = httpClient.formMultipart(
    FormMultipart(
        listOf<FormMultipart.FormPart>(
            FormMultipart.data("field1", "some data content"),
            FormMultipart.file(
                "field2",
                "example1.txt",
                "text/plain",
                "some file content".toByteArray(StandardCharsets.UTF_8)
            )
        )
    )
)
Самописное

Если тело требуется записывать отличным от стандартных механизмов способом, то можно использовать специальный интерфейс HttpClientRequestMapper для реализации собственной логики:

@HttpClient
public interface SomeClient {

    record UserBody(String id) {}

    final class UserRequestMapper implements HttpClientRequestMapper<UserBody> {

        @Override
        public HttpBodyOutput apply(Context ctx, UserBody value) {
            return HttpBody.plaintext(value.id());
        }
    }

    @HttpRoute(method = HttpMethod.POST, path = "/hello/world")
    void hello(@Mapping(UserRequestMapper.class) UserBody body);
}
@HttpClient
interface SomeClient {

    data class UserBody(val id: String)

    class UserRequestMapper : HttpClientRequestMapper<UserBody> {
        override fun apply(ctx: Context, value: UserBody): HttpBodyOutput {
            return HttpBody.plaintext(value.id)
        }
    }

    @HttpRoute(method = HttpMethod.POST, path = "/hello/world")
    fun hello(@Mapping(UserRequestMapper::class) body: UserBody)
}

Куки

@Cookie — значение Cookie, имя параметра указывается в value либо по умолчанию равно имени аргумента метода.

@HttpClient
public interface SomeClient {

    @HttpRoute(method = HttpMethod.GET, path = "/hello/world") 
    void hello(@Cookie("cookieName") String cookieValue); 
}
@HttpClient
interface SomeClient {

    @HttpRoute(method = HttpMethod.GET, path = "/hello/world")
    fun hello(@Cookie("cookieName") cookieValue: String)
}

Обязательные параметры

По умолчанию все аргументы объявленные в методе являются обязательными (NotNull).

По умолчанию все аргументы объявленные в методе которые не используют Kotlin Nullability синтаксис считаются обязательными (NotNull).

Необязательные параметры

Если аргумент метода является необязательным, то есть может отсутствовать то, можно использовать аннотацию @Nullable:

@HttpClient
public interface SomeClient {

    @HttpRoute(method = HttpMethod.GET, path = "/hello/world")
    void hello(@Nullable @Query("queryValue") String queryValue); //(1)!
}
  1. Подойдет любая аннотация @Nullable, такие как javax.annotation.Nullable / jakarta.annotation.Nullable / org.jetbrains.annotations.Nullable / и т.д.

Предполагается использовать Kotlin Nullability синтаксис и помечать такой параметр как Nullable:

@HttpClient
interface SomeClient {

    @HttpRoute(method = HttpMethod.GET, path = "/hello/world")
    fun hello(@Query("queryValue") queryValue: String?)
}

Ответ

Секция описывает преобразование HTTP ответа от декларативного HTTP клиента.

Тело ответа

По умолчанию можно использовать стандартные типы возвращаемых значений тела ответа, такие как void, byte[], ByteBuffer либо String.

Json

Если предполагается читать тело как Json, то требуется использовать аннотацию @Json над методом.

@HttpClient
public interface SomeClient {

    record MyResponse(String name) { }

    @Json //(1)!
    @HttpRoute(method = HttpMethod.GET, path = "/hello/world")
    MyResponse hello();
}
  1. Указывает что ответ должен быть прочитан как Json
@HttpClient
interface SomeClient {

    data class MyResponse(val name: String) { }

    @Json //(1)!
    @HttpRoute(method = HttpMethod.GET, path = "/hello/world")
    fun hello(): MyResponse
}
  1. Указывает что ответ должен быть прочитан как Json

Требуется подключить модуль Json.

Сущность ответа

Если предполагается читать тело и получить также заголовки и статус код ответа, то предполагается использовать HttpResponseEntity, это обертка над телом ответа.

Ниже показан пример аналогичный примеру Json вместе с оберткой HttpResponseEntity:

@HttpClient
public interface SomeClient {

    record MyResponse(String name) { }

    @Json
    @HttpRoute(method = HttpMethod.GET, path = "/hello/world")
    HttpResponseEntity<MyResponse> hello();
}
@HttpClient
interface SomeClient {

    data class MyResponse(val name: String) { }

    @Json
    @HttpRoute(method = HttpMethod.GET, path = "/hello/world")
    fun hello(): HttpResponseEntity<MyResponse>
}
Самописное

Если требуется чтение ответа отличным способом, то можно использовать специальный интерфейс HttpClientResponseMapper:

@HttpClient
public interface SomeClient {

    record MyResponse(String name) { }

    final class ResponseMapper implements HttpClientResponseMapper<MyResponse> {

        @Override
        public MyResponse apply(HttpClientResponse response) throws IOException, HttpClientDecoderException {
            try (var is = response.body().asInputStream()) {
                final byte[] bytes = is.readAllBytes();
                var body = new String(bytes, StandardCharsets.UTF_8);
                return new MyResponse(body);
            }
        }
    }

    @Mapping(ResponseMapper.class)
    @HttpRoute(method = HttpMethod.GET, path = "/hello/world")
    MyResponse hello();
}
@HttpClient
interface SomeClient {

    data class MyResponse(val name: String)

    class ResponseMapper : HttpClientResponseMapper<MyResponse> {

        @Throws(IOException::class, HttpClientDecoderException::class)
        override fun apply(response: HttpClientResponse): MyResponse {
            response.body().asInputStream().use {
                val bytes: ByteArray = it.readAllBytes()
                val body = String(bytes, StandardCharsets.UTF_8)
                return MyResponse(body)
            }
        }
    }

    @Mapping(ResponseMapper::class)
    @HttpRoute(method = HttpMethod.GET, path = "/hello/world")
    fun hello(): MyResponse
}

Ошибка ответа

По умолчанию преобразование будет применяться только для 2хх HTTP статусов кодов, для всех остальных будет выбрасываться исключение HttpClientResponseException, которое содержит HTTP статус код, тело ответа и заголовки ответа.

Преобразование по коду

Если требуются специфичные преобразование в зависимости от HTTP статус кода ответа, можно использовать аннотацию @ResponseCodeMapper для указания соответствия HTTP статус кода и преобразователя HttpClientResponseMapper.

Также можно использовать ResponseCodeMapper.DEFAULT как указание поведения по умолчанию для всех не перечисленных статус кодов.

@HttpClient
public interface SomeClient {

    record UserResponse(UserResponse.Payload payload, UserResponse.Error error) {

        public record Error(int code, String message) {}

        public record Payload(String message) {}
    }

    @ResponseCodeMapper(code = ResponseCodeMapper.DEFAULT, mapper = ResponseErrorMapper.class)
    @ResponseCodeMapper(code = 200, mapper = ResponseSuccessMapper.class)
    @HttpRoute(method = HttpMethod.GET, path = "/hello/world")
    UserResponse hello();
}
@HttpClient
interface SomeClient {

    data class UserResponse(val payload: Payload, val error: Error) {

        data class Error(val code: Int, val message: String)

        data class Payload(val message: String)
    }

    @ResponseCodeMapper(code = ResponseCodeMapper.DEFAULT, mapper = ResponseErrorMapper::class)
    @ResponseCodeMapper(code = 200, mapper = ResponseSuccessMapper::class)
    @HttpRoute(method = HttpMethod.GET, path = "/hello/world")
    fun hello(): UserResponse
}

В примере выше для статуса кода 200 будет использовать ResponseSuccessMapper, а для всех остальных статус кодов будет использован ResponseErrorMapper.

Сигнатуры

Доступные сигнатуры для методов декларативного HTTP клиента из коробки:

Под T подразумевается тип возвращаемого значения, либо Void.

Под T подразумевается тип возвращаемого значения, либо Unit.

Перехватчики

Можно создавать перехватчики для изменения поведения либо создания дополнительного поведения используя класс HttpClientInterceptor. Перехватчики можно подключить на определенные методы либо весь @HttpClient класс целиком:

@HttpClient
public interface SomeClient {

    final class MethodInterceptor implements HttpClientInterceptor {

        private final Component1 component1;

        private MethodInterceptor(Component1 component1) {
            this.component1 = component1;
        }

        @Override
        public CompletionStage<HttpClientResponse> processRequest(Context ctx, InterceptChain chain, HttpClientRequest request) throws Exception {
            component1.doSomething();
            return chain.process(ctx, request);
        }
    }

    @InterceptWith(MethodInterceptor.class)
    @HttpRoute(method = HttpMethod.GET, path = "/hello/world")
    void hello();
}
@HttpClient
interface SomeClient {

    class MethodInterceptor(val component1: Component1) : HttpClientInterceptor {

        @Throws(Exception::class)
        override fun processRequest(
            ctx: Context,
            chain: HttpClientInterceptor.InterceptChain,
            request: HttpClientRequest
        ): CompletionStage<HttpClientResponse> {
            component1.doSomething()
            return chain.process(ctx, request)
        }
    }

    @InterceptWith(MethodInterceptor::class)
    @HttpRoute(method = HttpMethod.GET, path = "/hello/world")
    fun hello()
}

Авторизация

Kora предоставляет готовые перехватчики которые можно использовать для авторизации посредствам Basic/ApiKey/Bearer/OAuth

Basic

Требуется сконфигурировать перехватчик и конфигурацию для авторизации Basic:

@Module
public interface BasicAuthModule {

    @ConfigSource("openapiAuth.basicAuth")
    public interface BasicAuthConfig {

        String username();

        String password();
    }

    default BasicAuthHttpClientInterceptor basicAuther(BasicAuthConfig config) {
        return new BasicAuthHttpClientInterceptor(config.username(), config.password());
    }
}
@Module
interface BasicAuthModule {

    @ConfigSource("openapiAuth.basicAuth")
    interface BasicAuthConfig {

        fun username(): String

        fun password(): String
    }

    fun basicAuther(config: BasicAuthConfig): BasicAuthHttpClientInterceptor {
        return BasicAuthHttpClientInterceptor(config.username(), config.password())
    }
}

Также в конструктор можно предоставить собственную реализацию HttpClientTokenProvider если правила получения секретов другие.

Затем подключить перехватчик для всего HTTP клиента либо определенных методов.

@HttpClient
public interface SomeClient {

    @InterceptWith(BasicAuthHttpClientInterceptor.class)
    @HttpRoute(method = HttpMethod.GET, path = "/hello/world")
    void hello();
}
@HttpClient
interface SomeClient {

    @InterceptWith(BasicAuthHttpClientInterceptor::class)
    @HttpRoute(method = HttpMethod.GET, path = "/hello/world")
    fun hello()
}

ApiKey

Требуется сконфигурировать перехватчик и конфигурацию для авторизации ApiKey:

@Module
public interface ApiKeyAuthModule {

    @ConfigSource("openapiAuth.apiKeyAuth")
    interface ApiKeyAuthConfig {

        String apiKey();
    }

    default ApiKeyHttpClientInterceptor apiKeyAuther(ApiKeyAuthConfig config) {
        return new ApiKeyHttpClientInterceptor(ApiKeyLocation.HEADER, "X-API-KEY", config.apiKey());
    }
}
@Module
interface ApiKeyAuthModule {

    @ConfigSource("openapiAuth.apiKeyAuth")
    interface ApiKeyAuthConfig {

        fun apiKey(): String
    }

    fun apiKeyAuther(config: ApiKeyAuthConfig): ApiKeyHttpClientInterceptor {
        return ApiKeyHttpClientInterceptor(ApiKeyLocation.HEADER, "X-API-KEY", config.apiKey())
    }
}

Затем подключить перехватчик для всего HTTP клиента либо определенных методов.

@HttpClient
public interface SomeClient {

    @InterceptWith(ApiKeyHttpClientInterceptor.class)
    @HttpRoute(method = HttpMethod.GET, path = "/hello/world")
    void hello();
}
@HttpClient
interface SomeClient {

    @InterceptWith(ApiKeyHttpClientInterceptor::class)
    @HttpRoute(method = HttpMethod.GET, path = "/hello/world")
    fun hello()
}

Bearer

Требуется сконфигурировать перехватчик для авторизации Bearer:

@Module
public interface BearerAuthModule {

    default BearerAuthHttpClientInterceptor bearerAuther(HttpClientTokenProvider tokenProvider) {
        return new BearerAuthHttpClientInterceptor(tokenProvider);
    }
}
@Module
interface BasicAuthModule {

    fun bearerAuther(tokenProvider: HttpClientTokenProvider): BearerAuthHttpClientInterceptor {
        return BearerAuthHttpClientInterceptor(tokenProvider)
    }
}

Потребуется самостоятельно реализовать предоставление Bearer токена с помощью собственной реализации HttpClientTokenProvider, либо использовать конструктор который принимает статический Bearer Token.

public interface HttpClientTokenProvider {

    CompletionStage<String> getToken(HttpClientRequest request);
}

Затем подключить перехватчик для всего HTTP клиента либо определенных методов.

@HttpClient
public interface SomeClient {

    @InterceptWith(BearerAuthHttpClientInterceptor.class)
    @HttpRoute(method = HttpMethod.GET, path = "/hello/world")
    void hello();
}
@HttpClient
interface SomeClient {

    @InterceptWith(BearerAuthHttpClientInterceptor::class)
    @HttpRoute(method = HttpMethod.GET, path = "/hello/world")
    fun hello()
}

OAuth

Авторизация посредствам OAuth аналогично Bearer, требуется самостоятельно реализовать HttpClientTokenProvider и подложить его в контейнер зависимостей.

Клиент императивный

Базовый клиент представляет собой интерфейс HttpClient и доступен для внедрения:

public interface HttpClient {

    CompletionStage<HttpClientResponse> execute(HttpClientRequest request); //(1)!

    HttpClient with(HttpClientInterceptor interceptor); //(2)!
}
  1. Метод исполнения запроса
  2. Метод позволяющий добавлять различные перехватчики в ручном режиме

Для построения запросов вручную можно использовать HttpClientRequestBuilder:

HttpClientRequest request = HttpClientRequest.of("POST", "http://localhost:8090/pets/{petId}")
        .templateParam("petId", "1")
        .queryParam("page", 1)
        .header("token", "12345")
        .body(HttpBody.plaintext("refresh"))
        .build();
val request = HttpClientRequest.of("POST", "http://localhost:8090/pets/{petId}")
    .templateParam("petId", "1")
    .queryParam("page", 1)
    .header("token", "12345")
    .body(HttpBody.plaintext("refresh"))
    .build()