Расширение и ограничения HTTP-клиента
Собственный Converter.Factory
Аннотация @ConverterFactory связывает интерфейс с фабрикой-создателем:
@Api(name = "search")
@ConverterFactory(factoryClass = SearchConverterFactoryCreator::class)
interface SearchApi
class SearchConverterFactoryCreator : ConverterFactoryCreator {
override fun create(
apiConfig: ApiClientsConfig.ApiConfig,
mapper: ObjectMapper
): Converter.Factory {
val searchMapper = mapper.copy()
.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
return JacksonConverterFactory.create(searchMapper)
}
}
Класс-создатель должен иметь открытый конструктор без аргументов. Он получает
полную конфигурацию API и уже выбранный JSON/XML-маппер. Собственный конвертер
регистрируется раньше стандартного Jackson, поэтому должен возвращать null для
типов и методов, которые не обрабатывает.
Реальный пример такого расширения — ElasticConverterFactoryCreator: он
исключает null и включает SNAKE_CASE только для Elasticsearch API.
Дополнительный OkHttp interceptor
Глобальную фабрику interceptor можно зарегистрировать до создания клиента:
DefaultApiClientFactory.addInterceptorFactory<CatalogApi>(
object : InterceptorFactory {
override fun createInterceptor(
clazz: Class<*>,
config: ApiClientsConfig.ApiConfig
) = Interceptor { chain ->
val request = chain.request().newBuilder()
.header("X-Client", config.name)
.build()
chain.proceed(request)
}
}
)
Регистрация хранится в статической коллекции по точному классу API. Удаления и
защиты от повторной регистрации нет: повторный вызов добавит ещё один
interceptor. Каждый новый getClient() вызывает фабрику и создаёт новый
экземпляр interceptor. Он выполняется после авторизации.
Собственный методный тег
Методная аннотация, помеченная @Tag, переносится в OkHttp request:
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
@Tag
annotation class Audit(val operation: String)
interface CatalogApi {
@Audit("product-read")
@GET("api/products/{id}")
fun getProduct(@Path("id") id: Long): ProductDto
}
В interceptor значение читается так:
val audit = chain.request().tag(Audit::class.java)
Аннотация должна иметь runtime retention и target FUNCTION. @SoapRequest не
является таким тегом: его читает непосредственно SOAP-конвертер.
Ручное ограничение составной операции
RateLimiterWrapper применяется, когда одно разрешение должно охватывать не
отдельный метод Retrofit, а составной участок кода. Текущая реализация принимает
готовый io.github.resilience4j.ratelimiter.RateLimiter:
val wrapper: RateLimiterWrapper = RateLimiterWrapperImpl(rateLimiter)
val result = wrapper.execute {
api.createTask(request)
}
Эта обёртка не использует настройки api.<key>.rate.limiter.* автоматически и
не преобразует RequestNotPermitted в BGMessageException. Такое преобразование
есть только в основном адаптере клиента.
Ограничения модели конфигурации
-
Один
@Api.nameсоответствует одной секции конфигурации. Перегрузка с URL позволяет менять только адрес, но не авторизацию, тайм-ауты или лимиты. -
Интерфейсы с одинаковым
@Api.nameделят пулы, Rate Limiter, Circuit Breaker и ключ внешнего токена. -
Retrofit-прокси не кэшируется и не обновляется после изменения Setup.
-
Вызовы синхронные; асинхронный
Call<T>не поддерживается. -
Успешный ответ без тела может вернуть
nullдля non-null типа. -
Собственный
errorTypeвсегда десериализуется как JSON независимо отresponseType. -
LOCAL-хранилище токенов — общий изменяемый map процесса без синхронизации.
Известные особенности реализации
В текущем коде есть несколько пограничных эффектов, которые важно учитывать при диагностике:
-
после изменения конфигурации первый новый клиент ещё получает прежний кэшированный компонент, хотя новый уже создан; следующий клиент получает обновлённый;
-
вспомогательные функции поиска следующего конвертера проходят диапазон до
factories.sizeвключительно; если ни одна последующая фабрика не подходит, возможенIndexOutOfBoundsException; -
CircuitBreakerServiceпри включённом компоненте оборачивает практически любое исключение вызова в новыйBGException, хотя часть таких ошибок исключена из статистики Circuit Breaker; -
RateLimiterCallAdapterDecoratorостался в пакете, но фабрика клиента используетRateLimiterCallAdapterиRateLimiterService; -
сохранение Redis-токена выполняется отдельными командами
SETиEXPIRE, поэтому между ними есть небольшое окно без TTL.
Что проверяют тесты
Юнит-тесты пакета покрывают базовые метрики MBean, работу Rate Limiter и Circuit
Breaker, а также кэширование компонентов. Полного теста сборки
DefaultApiClientFactory, цепочки interceptor, авторизации, SOAP и разбора HTTP
ошибок сейчас нет. При изменении этих частей нужен регрессионный тест на реальном
MockWebServer или эквивалентном тестовом HTTP-сервере.