Skip to content

Customization & Extensibility

Overview

CoApi's HTTP clients are not black boxes. The library exposes a layered customization SPI that lets you intercept and modify the client builder at three points: (1) per-client YAML configuration for filters and interceptors, (2) per-type builder customizers for load balancing and protocol-specific tweaks, and (3) global customizer beans applied to all clients in order. This design means common concerns (connection pooling, metrics, tracing) can be applied globally while client-specific overrides (auth headers, timeouts) target individual interfaces.

At a Glance

Customization PointInterfaceScopeKey FileSource
Base SPIHttpClientBuilderCustomizer<Builder>All clientsHttpClientBuilderCustomizer.ktHttpClientBuilderCustomizer.kt
Reactive customizerWebClientBuilderCustomizerWebClient clientsWebClientBuilderCustomizer.ktWebClientBuilderCustomizer.kt
Sync customizerRestClientBuilderCustomizerRestClient clientsRestClientBuilderCustomizer.ktRestClientBuilderCustomizer.kt
Per-client configClientPropertiesIndividual clientsClientProperties.ktClientProperties.kt
Per-client filtersFilterDefinition / InterceptorDefinitionIndividual clientsClientProperties.ktClientProperties.kt

Customizer Class Hierarchy

mermaid
classDiagram
    class HttpClientBuilderCustomiabler~Builder~ {
        <<fun interface>>
        +customize(CoApiDefinition, Builder)
    }
    class WebClientBuilderCustomizer {
        <<fun interface>>
        +customize(CoApiDefinition, WebClient.Builder)
        +NoOp
    }
    class RestClientBuilderCustomizer {
        <<fun interface>>
        +customize(CoApiDefinition, RestClient.Builder)
        +NoOp
    }
    class ClientProperties {
        <<interface>>
        +getBaseUri(String) String
        +getLoadBalanced(String) Boolean?
        +getFilter(String) FilterDefinition
        +getInterceptor(String) InterceptorDefinition
    }
    class FilterDefinition {
        +names: List~String~
        +types: List~Class~
    }
    class InterceptorDefinition {
        +names: List~String~
        +types: List~Class~
    }

    HttpClientBuilderCustomizer <|-- WebClientBuilderCustomizer
    HttpClientBuilderCustomizer <|-- RestClientBuilderCustomizer
    ClientProperties --> FilterDefinition
    ClientProperties --> InterceptorDefinition

Customizer Invocation Order

When a WebClient or RestClient bean is created, customizers are applied in a strict order:

mermaid
sequenceDiagram
    autonumber
    participant FB as AbstractWebClientFactoryBean
    participant CTX as ApplicationContext
    participant Builder as WebClient.Builder
    participant Props as ClientProperties
    participant LB as builderCustomizer
    participant Global as Global Customizers

    FB->>CTX: getBean(WebClient.Builder)
    CTX-->>Builder: builder instance
    FB->>FB: getBaseUrl() → set baseUrl
    FB->>Props: getFilter(definition.name)
    Props-->>FB: FilterDefinition
    FB->>Builder: apply filters (names + types)
    FB->>LB: builderCustomizer.customize(definition, builder)
    LB->>Builder: add LoadBalancedExchangeFilterFunction if load-balanced
    FB->>CTX: getBeanProvider(WebClientBuilderCustomizer).orderedStream()
    loop For each global customizer (ordered)
        Global->>Builder: customize(definition, builder)
    end
    FB->>Builder: build()
    Builder-->>FB: WebClient instance

The invocation order in AbstractWebClientFactoryBean.getObject():

OrderStepWhatConfigurable?
1Get builderWebClient.Builder from ApplicationContextNo
2Set base URLgetBaseUrl() — properties override annotationVia coapi.clients.<name>.base-url
3Apply filtersFilterDefinition from ClientPropertiesVia YAML
4Per-type customizerLoad balancing filter or NoOpAutomatic
5Global customizersAll WebClientBuilderCustomizer beans, orderedRegister as Spring bean

Customizer Decision Flow

mermaid
flowchart TD
    A["FactoryBean.getObject()"] --> B["Get Builder from Context"]
    B --> C[Set baseUrl]
    C --> D[Apply per-client filters/interceptors]
    D --> E{Load balanced?}
    E -->|Yes| F[Add LB filter/interceptor]
    E -->|No| G[NoOp customizer]
    F --> H[Apply global customizers]
    G --> H
    H --> I[Build client]

Per-Client Filter Configuration

Filters and interceptors are configured per client via YAML properties. The ClientProperties interface provides typed access:

Reactive (WebClient) filters:

yaml
coapi:
  clients:
    MyApiClient:
      reactive:
        filter:
          names:
            - myAuthFilter
          types:
            - com.example.LoggingExchangeFilterFunction

Sync (RestClient) interceptors:

yaml
coapi:
  clients:
    MyApiClient:
      sync:
        interceptor:
          names:
            - myAuthInterceptor
          types:
            - com.example.LoggingInterceptor

Filter resolution in AbstractWebClientFactoryBean:

  • names → resolved as beans from ApplicationContext by name
  • types → resolved as beans from ApplicationContext by class type

Example: Connection Pool Customizer

A real-world example from the consumer server demonstrates per-client connection pooling:

kotlin
@Service
class ConsumerWebClientBuilderCustomizer : WebClientBuilderCustomizer {
    override fun customize(
        coApiDefinition: CoApiDefinition,
        builder: WebClient.Builder
    ) {
        val connectionProvider = ConnectionProvider.builder(coApiDefinition.name)
            .maxConnections(500)
            .maxIdleTime(Duration.ofSeconds(20))
            .maxLifeTime(Duration.ofSeconds(60))
            .pendingAcquireTimeout(Duration.ofSeconds(60))
            .evictInBackground(Duration.ofSeconds(120))
            .build()
        val httpClient = HttpClient.create(connectionProvider)
        builder.clientConnector(ReactorClientHttpConnector(httpClient))
    }
}

Key points:

  • Registered as @Service so Spring discovers it as a global customizer
  • Uses coApiDefinition.name to create a named connection pool per client
  • Applied to all @CoApi clients via getBeanProvider().orderedStream()

Example: Per-Client Auth Filter

Configure a filter for a specific client without affecting others:

yaml
coapi:
  clients:
    SecureApiClient:
      base-url: https://api.example.com
      reactive:
        filter:
          types:
            - com.example.BearerTokenFilter

Or register the filter by bean name:

yaml
coapi:
  clients:
    SecureApiClient:
      reactive:
        filter:
          names:
            - bearerTokenFilter

YAML Configuration Reference

PropertyTypeDefaultDescription
coapi.clients.<name>.base-urlString""Override annotation's baseUrl
coapi.clients.<name>.load-balancedBooleannullOverride load balancing
coapi.clients.<name>.reactive.filter.namesList[]Filter bean names
coapi.clients.<name>.reactive.filter.typesList[]Filter class types
coapi.clients.<name>.sync.interceptor.namesList[]Interceptor bean names
coapi.clients.<name>.sync.interceptor.typesList[]Interceptor class types

References

  1. HttpClientBuilderCustomizer.ktspring/src/main/kotlin/me/ahoo/coapi/spring/client/HttpClientBuilderCustomizer.kt
  2. WebClientBuilderCustomizer.ktspring/src/main/kotlin/me/ahoo/coapi/spring/client/reactive/WebClientBuilderCustomizer.kt
  3. RestClientBuilderCustomizer.ktspring/src/main/kotlin/me/ahoo/coapi/spring/client/sync/RestClientBuilderCustomizer.kt
  4. ClientProperties.ktspring/src/main/kotlin/me/ahoo/coapi/spring/client/ClientProperties.kt
  5. AbstractWebClientFactoryBean.ktspring/src/main/kotlin/me/ahoo/coapi/spring/client/reactive/AbstractWebClientFactoryBean.kt
  6. AbstractRestClientFactoryBean.ktspring/src/main/kotlin/me/ahoo/coapi/spring/client/sync/AbstractRestClientFactoryBean.kt
  7. ConsumerWebClientBuilderCustomizer.ktexample/example-consumer-server/src/main/kotlin/.../ConsumerWebClientBuilderCustomizer.kt