Универсальный API-клиент
Особенности клиентов
-
Минимальное взаимодействие с retrofit (Отсутствие взаимодействий с getResponse(Call<*>) конструкциями)
-
Клиенты кэшируются
-
Поддержка изменений конфигурации клиентов во время исполнения
Quick Start
-
Создаем интерфейс под API-клиент
-
Отмечаем методы необходимыми аннотациями
-
Настраиваем конфигурацию клиента
-
Вызываем созданный API-клиент с помощью фабрики
-
Profit
1. Заведение интерфейса
В примере будет использоваться несуществующий сервис DummyService
.
Сервис DummyService является REST API приложением |
Заведём интерфейс DummyServiceAPI
.
// Интерфейс сервиса API-клиента
@Api(name = "dummy")
interface DummyServiceAPI
Аннотация @Api
Данная аннотация обязательна для объявления API-клиента.
Параметры
-
name - название (обязательное поле)
-
responseType - формат общения, JSON (по-умолчанию), XML
-
errorType - класс типа ошибки, используется для формирования сообщения об ошибки
Обратите внимание, что @Api.name задает имя клиенту, оно должно быть уникальным, это имя должно использоваться при указании конфигурации клиента (см. 3. Конфигурация сервиса)
|
Дополнительные аннотации интерфейса указаны в разделе Дополнительные возможности
2. Создание методов
Определим метод для получения объекта из сервиса, и сам получаемый объект.
Эндпоинт /dummy/api/v0/get-wrap/{value} сервиса DummyService возвращает введённую строку как объект {value: String}
|
// Интерфейс сервиса API-клиента
@Api(name = "dummy")
interface DummyServiceAPI {
@GET("/dummy/api/v0/get-wrap/{value}")
fun getWrappedString(@Path("value") value: String): WrappedStringResponse
}
// Модель возвращаемого объекта
data class WrappedStringResponse(
var value: String
)
Для большего понимания как работать с аннотациями библиотеки retrofit (@GET , @Path , … ) рекомендуется изучить документацию retrofit
|
Дополнительные аннотации методов и их использование указаны в разделе Дополнительные возможности
3. Конфигурация сервиса
Прежде чем, проверять наш API-клиент, для начала нужно определить конфигурацию. Конфигурация клиента берётся из глобальной конфигурации ERP.
Шаблон настройки клиента
api.{apiname}.{parameter}={value}
# Где:
# apiname - уникальное имя клиента, которое указано в `@Api.name`
# parameter - параметр конфигурации api
# value - значение параметра
Стандартные настройки API
# Название API (по-умолчанию: "")
api.dummy.name=Dummy
# URL API (по-умолчанию: null}
api.dummy.url=http://localhost:8083/
# Игнорирование SSL (по-умолчанию: false)
api.dummy.ignoreSSL=1
Настройки аутентификации API
В данном подразделе, будут поочередно использоваться связки конфигураций для каждого типа аутентификации
NONE (по-умолчанию)
# Авторизация отсутствует
api.dummy.auth.type=NONE
BASIC
# Простая аутентификация
api.dummy.auth.type=BASIC
# Логин сервиса
api.dummy.auth.username=admin
# Пароль сервиса
api.dummy.auth.password=admin
# Header для отправки данных аутентификации (по-умолчанию Authorization)
api.dummy.auth.header=Authorization
STATIC_TOKEN
# Авторизация по токену
api.dummy.auth.type=STATIC_TOKEN
# Токен сервиса
api.dummy.auth.token=oKwWNJQ2fdFcspWNiZVRno3oqOrbuIwK
# Header для отправки данных аутентификации (по-умолчанию Authorization)
api.dummy.auth.header=Authorization
EXTERNAL_TOKEN
# Авторизация по токен генератору из класса
api.dummy.auth.type=EXTERNAL_TOKEN
# Тип токена {JWT, BEARER} (по-умолчанию JWT)
api.dummy.auth.tokenType=JWT
# Место сохранения {LOCAL, REDIS} (по-умолчанию: LOCAL)
api.dummy.auth.tokenRepositoryProvider=LOCAL
# Время истечения токена в секундах (по-умолчанию: 1 день)
api.dummy.auth.tokenExpireTime=86400
# Header для отправки данных аутентификации (по-умолчанию: Authorization)
api.dummy.auth.header=Authorization
CUSTOM
# Авторизация по собственному обработчику запросов
api.dummy.auth.type=CUSTOM
Подробнее об использовании аутентификации EXTERNAL_TOKEN и CUSTOM будет описано в соответствующих разделах. |
Настройки запросов API
# Максимальное время обработки отправки запроса (по-умолчанию: 10000)
api.dummy.http.connectTimeout=10000
# Максимальное время обработки получения запроса (по-умолчанию: 10000)
api.dummy.http.readTimeout=10000
Конфигурация пула потоков
# Количество одновременно отправляемых запросов
# Принимает целое положительное число
# По-умолчанию 64
# Если указано некорректное значение, то используется значение по-умолчанию
api.dummy.worker.pool.maxRequests=
# Количество одновременно отправляемых запросов в рамках хоста
# Принимает целое положительное число
# По-умолчанию 5
# Если указано некорректное значение, то используется значение по-умолчанию
api.dummy.worker.pool.maxRequestsPerHost=
# Максимальное количество неиспользуемых активных соединений
# Принимает целое положительное число
# По-умолчанию 5
# Если указано некорректное значение, то используется значение по-умолчанию
api.dummy.http.pool.idle=
# Количество времени поддержания неиспользумого активного соединения
# Принимает целое положительное число
# По-умолчанию 5
# Если указано некорректное значение, то используется значение по-умолчанию
api.dummy.http.pool.keepAlive=
# Единица времени
# Принимает одно из значений 'java.util.concurrent.TimeUnit'
# По-умолчанию MINUTES
# Если указано некорректное значение, то используется значение по-умолчанию
api.dummy.http.pool.timeUnit=
Конфигурация выключателя (Circuit Breaker)
Circuit Breaker (далее - CB)
Может принимать несколько статусов:
-
CLOSE: Нормальное состояние, когда автоматический выключатель позволяет совершать вызовы внешних сервисов.
-
OPEN: Состояние указывает на то, что автоматический выключатель сработал и предотвращает вызовы внешних сервисов. Это делается для того, чтобы предотвратить дальнейшие сбои и дать внешнему сервису время для восстановления.
-
HALF_OPEN: В этом состоянии автоматический выключатель позволяет несколько вызовов внешних сервисов, чтобы проверить, восстановился ли он. Если эти вызовы увенчаются успехом, автоматический выключатель возвращается в состояние CLOSE. Если они терпят неудачу, они возвращаются в OPEN состояние.
# Включен ли CB
# Принимает true/false
# По-умолчанию false
# Если указано некорректное значение, то используется значение по-умолчанию
api.dummy.circuit.breaker.enabled=
# Параметр определяет порог процента неудачных вызовов, после которого CB переключится в состояние "OPEN".
# По умолчанию, если более 50% вызовов за последние 10 вызовов завершились неудачно, то CB перейдет в состояние "OPEN".
# Принимает целое число от 1 до 100
# По-умолчанию 50
# Если указано некорректное значение, то используется значение по-умолчанию
api.dummy.circuit.breaker.failureRateThreshold=
# Длительность времени, в течение которой CB будет находиться в состоянии "OPEN", прежде чем перейти в состояние "HALF_OPEN".
# По умолчанию, CB будет оставаться в состоянии "OPEN" в течение 10000 (10 секунд).
# Принимает целое положительное число
# По-умолчанию 10000
# Если указано некорректное значение, то используется значение по-умолчанию
api.dummy.circuit.breaker.waitDurationInOpenState=
# Порог процента медленных вызовов, при достижении которого CB может перейти в состояние "OPEN".
# По умолчанию, если более 50% вызовов за последние 10 вызовов считаются медленными, то CB может перейти в состояние "OPEN".
# Принимает целое число от 1 до 100
# По-умолчанию 50
# Если указано некорректное значение, то используется значение по-умолчанию
api.dummy.circuit.breaker.slowCallRateThreshold=
# Длительность времени, после которой вызов считается медленным.
# По умолчанию, вызов будет считаться медленным, если его длительность превышает 60000 (60 секунд).
# Принимает целое положительное число (в мс)
# По-умолчанию 60000
# Если указано некорректное значение, то используется значение по-умолчанию
api.dummy.circuit.breaker.slowCallDurationThreshold=
# Количество разрешенных вызовов в состоянии "HALF_OPEN".
# По умолчанию, CB разрешает выполнение до 3 вызовов в состоянии "HALF_OPEN".
# Принимает целое положительное число
# По-умолчанию 3
# Если указано некорректное значение, то используется значение по-умолчанию
api.dummy.circuit.breaker.permittedNumberOfCallsInHalfOpenState=
# Размер окна скользящего среднего, используемого для вычисления процента неудачных вызовов.
# По умолчанию, CB учитывает последние 10 вызовов для вычисления процента неудачных вызовов.
# Принимает целое положительное число
# По-умолчанию 10
# Если указано некорректное значение, то используется значение по-умолчанию
api.dummy.circuit.breaker.slidingWindowSize=
# Минимальное количество вызовов, которые должны быть выполнены, прежде чем CB начнет отслеживать процент неудачных вызовов.
# По умолчанию, CB начнет отслеживать процент неудачных вызовов только после выполнения минимум 10 вызовов.
# Принимает целое положительное число
# По-умолчанию 10
# Если указано некорректное значение, то используется значение по-умолчанию
api.dummy.circuit.breaker.minimumNumberOfCalls=
Конфигурация частоты запросов (Rate Limiter)
RATE LIMITER (далее - RL)
# Включен ли RL
# Принимает true/false
# По-умолчанию false
# Если указано некорректное значение, то используется значение по-умолчанию
api.dummy.rate.limiter.enabled=
# Параметр определяет длительность ожидания перед тем как запрос будет считаться неудачным из-за превышения лимита.
# По умолчанию, если запрос не может быть выполнен из-за превышения лимита,
# он будет ждать до 10000 (10 секунд) перед тем как считаться неудачным.
# Принимает целое неотрицательное число
# По-умолчанию 10000
# Если указано некорректное значение, то используется значение по-умолчанию
api.dummy.rate.limiter.timeoutDuration=
# Параметр определяет период времени, через который лимиты будут обновляться.
# По умолчанию, лимиты будут обновляться каждые 500 (0,5 секунды).
# Это означает, что после каждого такого периода времени, количество доступных запросов будет восстанавливаться.
# Принимает целое положительное число
# По-умолчанию 500
# Если указано некорректное значение, то используется значение по-умолчанию
api.dummy.rate.limiter.limitRefreshPeriod=
# Параметр устанавливает количество доступных запросов за каждый период обновления.
# По умолчанию, установлен лимит в 50 запросов за каждый период обновления.
# Это означает, что клиент может сделать до 50 запросов за каждые 0,5 секунды, после чего лимиты будут восстановлены.
# Принимает целое положительное число
# По-умолчанию 50
# Если указано некорректное значение, то используется значение по-умолчанию
api.dummy.rate.limiter.limitForPeriod=
Конфигурация логирования запросов
# Уровень логирования запросов (по-умолчанию: NONE)
# * NONE - логирование отсутствует
#
# * BASIC - простое логирование
# Пример:
# --> POST /greeting http/1.1 (3-byte body)
# <-- 200 OK (22ms, 6-byte body)
#
# * HEADERS - логирование заголовков
# Пример:
# --> POST /greeting http/1.1
# Host: example.com
# Content-Type: plain/text
# Content-Length: 3
# --> END POST
#
# <-- 200 OK (22ms)
# Content-Type: plain/text
# Content-Length: 6
# <-- END HTTP
#
# * BODY - полное логирование с содержимым запроса и ответа
# Пример:
# --> POST /greeting http/1.1
# Host: example.com
# Content-Type: plain/text
# Content-Length: 3
#
# {"Object": 123}
# --> END POST
#
# <-- 200 OK (22ms)
# Content-Type: plain/text
# Content-Length: 6
#
# {"error": 12, "message": "Why u sent me 123?"}
# <-- END HTTP
api.dummy.http.logLevel=NONE
4. Фабрика клиентов
Для получения клиента, доступна фабрика ApiClientFactory
, с функцией для получения клиента getClient
, принимающий класс API
// Получения клиента
DummyServiceAPI dummyService = context.getService(ApiClientFactory.class).getClient(DummyServiceAPI.class);
// Отправка запроса на получение строки Hello, World! из сервиса Dummy
String text = dummyService.getString("Hello, World!").value;
// Вывод результата
System.out.println(text);
Логи выполнения
[DummyServiceAPI] - --> GET http://localhost:8083/dummy/api/v0/get-wrap/Hello,%20World!
[DummyServiceAPI] - --> END GET
[DummyServiceAPI] - <-- 200 http://localhost:8083/dummy/api/v0/get-wrap/Hello,%20World! (5ms)
[DummyServiceAPI] - X-Content-Type-Options: nosniff
[DummyServiceAPI] - X-XSS-Protection: 1; mode=block
[DummyServiceAPI] - Cache-Control: no-cache, no-store, max-age=0, must-revalidate
[DummyServiceAPI] - Pragma: no-cache
[DummyServiceAPI] - Expires: 0
[DummyServiceAPI] - X-Frame-Options: DENY
[DummyServiceAPI] - Content-Type: application/json
[DummyServiceAPI] - Transfer-Encoding: chunked
[DummyServiceAPI] - Date: Tue, 31 Jan 2023 07:48:53 GMT
[DummyServiceAPI] - Keep-Alive: timeout=60
[DummyServiceAPI] - Connection: keep-alive
[DummyServiceAPI] -
[DummyServiceAPI] - {"value":"Hello, World!"}
[DummyServiceAPI] - <-- END HTTP (25-byte body)
Hello, World!
Дополнительные возможности
Использование конверторов
Для удобного общения между сервисами, поддерживаются конвертеры. На данный момент доступны JsonRpcConverterFactory и SoapConverterFactory позволяющие упростить взаимодействие с биллингом.
Пример API с использованием JsonRpcConverterFactory.
@Api(name = "billRB")
@ConverterFactory(factoryClass = JsonRpcConverterFactoryCreator::class)
interface BillExampleAPI {
@BillingJsonRequest(method = "state")
fun getContractState(scid: Int): ContractState
}
Данные конвертеры, преобразуют отправляемые объекты в структурированные по протоколу общения данные и наоборот. |
Дополнительные методы аутентификации
Дополнительные методы аутентификации доступны при использовании EXTERNAL_TOKEN, CUSTOM значениях типа аутентификации в конфигурации API.
Работа с EXTERNAL_TOKEN
При работе с типом аутентификации EXTERNAL_TOKEN, класс должен быть "снаряжён" дополнительным классом, который наследует TokenProvider.
Задачей данного класса является получение актуального токена по "нестандартной" схеме получения токена.
// Пример токен провайдера
class DummyServiceTokenProvider(
apiClass: Class<DummyServiceAPI>,
apiConfig: ApiClientsConfig.ApiConfig
) : TokenProvider(apiClass, apiConfig) {
override fun generateToken(): String {
return "1234-${UUID.randomUUID()}"
}
}
// Использование аннотации TokenProvider
@Api(name = "dummy")
@TokenProvider(providerClass = DummyServiceTokenProvider::class)
interface DummyServiceAPI {
@GET("/dummy/api/v0/get-wrap/{value}")
fun getWrappedString(@Path("value") value: String): WrappedStringResponse
}
Метод generateToken() будет обновлять токен, как только будет приходить ошибка 401 (UNAUTHORIZED) и повторять предыдущий запрос после смены токена.
|
Работа с CUSTOM
Аутентификация с типом CUSTOM позволяет встроить собственную аутентификацию, которая отличается от token-based сервисов. Используя данный тип аутентификации, можно встроить собственный интерцептор и систему аутентификации.
На официальном сайте okhttp можно прочитать подробнее об интерцепторах и аутентификаторах
При регистрации апи с CUSTOM типом аутентификации, требуется указать класс провайдера аутентификации @AuthenticationProvider.
Пример ниже показывает, как можно сделать собственный интерцептор, если аутентификация происходит по двум токенам в заголовке запроса.
// Пример CUSTOM токен провайдера
class DummyServiceTokenProvider(
apiClass: Class<DummyServiceAPI>,
apiConfig: ApiClientsConfig.ApiConfig
) : AuthenticationProvider(apiClass, apiConfig) {
override fun createInterceptor(): Interceptor = Interceptor { chain ->
val original = chain.request()
val request = original
.newBuilder()
.addHeader("token1", "91uIVdUIrkfczgfhYxdWxnJ594zm1Jd3lB1wCgMObm4XCVicmKT6MFibRAeFbrw5")
.addHeader("token2", "4aOiMnXudIzJcG3V30QggS60uDtTUkGk5ZmSlcmvV9kokrJ4xrATz686s0mCDUnA")
.build()
chain.proceed(request)
}
override fun createAuthenticator() = createDefaultAuthenticator()
}
// Использование аннотации AuthenticationProvider
@Api(name = "dummy")
@AuthenticationProvider(providerClass = DummyServiceTokenProvider::class)
interface DummyServiceAPI {
@GET("/dummy/api/v0/get-wrap/{value}")
fun getWrappedString(@Path("value") value: String): WrappedStringResponse
}
Тэги
Существуют аннотации-тэги, которые добавляются к методам для более гибкой настройки запросов.
@NoAuth
Использование тэга NoAuth позволяет пропустить авторизацию. Таким образом, можно обойти рекурсию по авторизации через тот же ресурс.
Пример использования:
import ru.bgcrm.context.core.ServerContext
import ru.bgcrm.util.api.factory.ApiClientFactory
class DummyServiceTokenProvider(
apiClass: Class<DummyServiceAPI>,
apiConfig: ApiClientsConfig.ApiConfig
) : TokenProvider(apiClass, apiConfig) {
override fun generateToken(): String {
val api = ServerContext.getService<ApiClientFactory>().getClient(DummyServiceAPI::class.java)
val result = api.auth(Crendtials("admin", "12345"))
return result.accessToken
}
}
import retrofit2.http.Body
@Api(name = "dummy")
@TokenProvider(providerClass = DummyServiceTokenProvider::class)
interface DummyServiceAPI {
@NoAuth
@POST("/dummy/api/v0/auth")
fun auth(@Body credentials): AccessData
@GET("/dummy/api/v0/get-wrap/{value}")
fun getWrappedString(@Path("value") value: String): WrappedStringResponse
}
Создание собственного Тэга
Для создания собственного тэга, достаточно создать класс-аннотацию и пометить его аннотацией @Tag.
package ru.bgcrm.util.api.tag
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
@Tag
annotation class NoAuth
Получение тэга
Тэг можно получить в перехватичике запроса, при помощи метода okhttp3.Request::tag
на выходе будет доступен объект аннотации метода
Interceptor { chain ->
val originalRequest = chain.request()
val noAuth = originalRequest.tag(NoAuth::class.java)
if (noAuth != null) return@Interceptor chain.proceed(originalRequest)
// Логика по авторизации
return@Interceptor chain.proceed(newRequest)
}