Расширение и ограничения 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-сервере.