CoApi Internal Architecture: How AbstractCoApiRegistrar and CoApiFactoryBean Build Spring HTTP Clients

CoApi uses a four-component Spring registration pipeline where AbstractCoApiRegistrar detects client modes and delegates to CoApiRegistrar, which registers CoApiFactoryBean instances that ultimately generate JDK proxies for your annotated interfaces.

The ahoo-wang/coapi library transforms plain Kotlin or Java interfaces into fully managed Spring HTTP clients through a lightweight, annotation-driven registration pipeline. Understanding the CoApi internal architecture—specifically how AbstractCoApiRegistrar coordinates with CoApiFactoryBean—is essential for debugging configuration issues or extending the framework with custom adapters.

The Four Core Components of CoApi

The framework's spring module implements a chain-of-responsibility pattern across four specialized classes. Each component handles a distinct phase of the bean lifecycle, from classpath scanning to proxy instantiation.

AbstractCoApiRegistrar

AbstractCoApiRegistrar serves as the entry point by implementing Spring’s ImportBeanDefinitionRegistrar interface. Located in spring/src/main/kotlin/me/ahoo/coapi/spring/AbstractCoApiRegistrar.kt, this abstract class orchestrates the initial setup phase.

When registerBeanDefinitions() is invoked, the registrar performs three critical tasks:

  1. Infers the ClientMode by calling ClientMode.inferClientMode(environment), which checks properties like coapi.client-mode to determine whether to use synchronous (sync) or reactive (reactive) HTTP clients.
  2. Registers the appropriate HttpExchangeAdapterFactory (SyncHttpExchangeAdapterFactory or ReactiveHttpExchangeAdapterFactory) as a Spring bean.
  3. Delegates definition processing by calling the abstract getCoApiDefinitions() method, which concrete subclasses implement to scan for @CoApi annotated interfaces.

The registrar then passes the resulting set of CoApiDefinition objects to CoApiRegistrar for actual bean registration.

CoApiRegistrar

CoApiRegistrar (defined in spring/src/main/kotlin/me/ahoo/coapi/spring/CoApiRegistrar.kt) acts as the construction coordinator. It accepts a BeanDefinitionRegistry and the resolved ClientMode, then iterates over each CoApiDefinition to register two beans per definition:

  • The HTTP client bean: Either RestClientFactoryBean (sync) or WebClientFactoryBean (reactive)
  • The API client bean: A CoApiFactoryBean that manufactures the final proxy

Using Spring’s BeanDefinitionBuilder, CoApiRegistrar programmatically adds these definitions to the container, logs registration steps, and skips duplicates to prevent bean overriding conflicts.

CoApiFactoryBean

CoApiFactoryBean implements Spring’s FactoryBean interface to produce the actual client instance. When Spring requests the bean via getObject(), the factory (located in spring/src/main/kotlin/me/ahoo/coapi/spring/CoApiFactoryBean.kt) executes a three-step construction process:

  1. Retrieves the HttpExchangeAdapterFactory from the bean factory based on the current ClientMode.
  2. Creates an HttpExchangeAdapter and builds a HttpServiceProxyFactory using Spring’s HTTP interface infrastructure.
  3. Generates the proxy by calling HttpServiceProxyFactory.createClient(apiType), returning a concrete implementation of the annotated interface that routes method calls through the underlying HTTP client.

This approach allows the resulting client to behave like a standard Spring bean, supporting dependency injection, scoping, and AOP proxies.

CoApiDefinition

CoApiDefinition (found in spring/src/main/kotlin/me/ahoo/coapi/spring/CoApiDefinition.kt) is an immutable data class that encapsulates the metadata required to construct a client. It stores the API name, interface type (apiType), base URL, load-balanced flag, and lazily computed bean names (httpClientBeanName, coApiBeanName).

The class provides a static extension function Class<*>.toCoApiDefinition(Environment) that extracts @CoApi and optional @LoadBalanced annotations, resolves property placeholders (e.g., ${github.url}), and produces a fully resolved definition ready for registration.

Step-by-Step Registration Flow

The CoApi internal architecture follows a strict lifecycle from annotation detection to proxy creation:

  1. Interface Discovery: A concrete subclass of AbstractCoApiRegistrar (such as EnableCoApiRegistrar) scans the configured base packages and converts each @CoApi interface into a CoApiDefinition using toCoApiDefinition(env).

  2. Mode Detection: AbstractCoApiRegistrar examines the Spring Environment to determine ClientMode, registering the corresponding HttpExchangeAdapterFactory bean.

  3. Bean Registration: CoApiRegistrar receives the definitions and registers both the low-level HTTP client factory and the high-level CoApiFactoryBean for each API interface.

  4. Proxy Instantiation: When application code requests the client bean, Spring invokes CoApiFactoryBean.getObject(), which assembles the HttpServiceProxyFactory and generates the JDK proxy that implements your interface.

All components participate in Spring’s standard bean-definition lifecycle, ensuring compatibility with @Autowired, @Qualifier, and custom bean post-processors.

Configuration Examples

Defining a CoApi Interface

Mark an interface with @CoApi to trigger processing. The serviceId attribute enables Spring Cloud LoadBalancer integration when combined with @LoadBalanced.

package me.ahoo.coapi.example

import me.ahoo.coapi.api.CoApi
import me.ahoo.coapi.api.LoadBalanced

@CoApi(serviceId = "github-service")
@LoadBalanced
interface GitHubSyncClient {
    fun getIssue(owner: String, repo: String, number: Long): Issue
}

Internally, CoApiDefinition.toCoApiDefinition(env) converts this to a definition with baseUrl = "lb://github-service" and loadBalanced = true.

Enabling CoApi in Spring

Apply @EnableCoApi to a configuration class. This annotation imports EnableCoApiRegistrar, which extends AbstractCoApiRegistrar to scan the specified packages.

import me.ahoo.coapi.spring.EnableCoApi
import org.springframework.context.annotation.Configuration

@Configuration
@EnableCoApi(
    scanBasePackages = ["me.ahoo.coapi.example"]
)
class CoApiConfig

Injecting the Generated Client

Inject the interface directly. Spring resolves the bean from CoApiFactoryBean and routes calls through the appropriate HTTP client.

import org.springframework.stereotype.Service

@Service
class IssueService(private val gitHubSyncClient: GitHubSyncClient) {
    fun fetchIssue(owner: String, repo: String, number: Long): Issue =
        gitHubSyncClient.getIssue(owner, repo, number)
}

Configuring Client Mode

Control the underlying HTTP client implementation via properties. AbstractCoApiRegistrar reads this during registerBeanDefinitions().

coapi.client-mode=reactive

Valid values are sync (uses RestClient) or reactive (uses WebClient).

Summary

  • AbstractCoApiRegistrar implements ImportBeanDefinitionRegistrar to detect ClientMode from the environment and trigger the registration pipeline.
  • CoApiRegistrar registers two beans per API definition: the HTTP client factory (RestClientFactoryBean or WebClientFactoryBean) and the proxy factory (CoApiFactoryBean).
  • CoApiFactoryBean creates the final proxy by assembling HttpServiceProxyFactory and invoking createClient(apiType).
  • CoApiDefinition captures metadata from @CoApi annotations and resolves property placeholders using toCoApiDefinition(Environment).
  • The entire architecture leverages standard Spring bean lifecycle hooks, allowing CoApi clients to participate in dependency injection and AOP infrastructure.

Frequently Asked Questions

What is the role of AbstractCoApiRegistrar in CoApi?

AbstractCoApiRegistrar acts as the bootstrap component that integrates CoApi with Spring's bean registration phase. It implements ImportBeanDefinitionRegistrar to hook into the @Configuration processing lifecycle, determines whether to use synchronous or reactive HTTP clients by inspecting ClientMode.inferClientMode(environment), and delegates the actual bean registration work to CoApiRegistrar. It also registers the appropriate HttpExchangeAdapterFactory bean before processing individual API definitions.

How does CoApiFactoryBean create the HTTP client proxy?

CoApiFactoryBean implements Spring's FactoryBean interface to produce the client instance lazily. When Spring calls getObject(), the factory retrieves the previously registered HttpExchangeAdapterFactory, creates an HttpExchangeAdapter, and uses it to build a HttpServiceProxyFactory. It then calls createClient(apiType) on the proxy factory to generate a JDK dynamic proxy that implements the annotated interface, routing all method invocations through the configured HTTP client.

How does CoApi determine whether to use sync or reactive mode?

During bean registration, AbstractCoApiRegistrar invokes ClientMode.inferClientMode(environment) to read the coapi.client-mode property from the Spring Environment. If set to reactive, the registrar registers ReactiveHttpExchangeAdapterFactory and WebClientFactoryBean; if set to sync, it registers SyncHttpExchangeAdapterFactory and RestClientFactoryBean. This decision propagates through CoApiRegistrar to CoApiFactoryBean, ensuring consistent client behavior.

What is CoApiDefinition and how is it created?

CoApiDefinition is an immutable data class defined in spring/src/main/kotlin/me/ahoo/coapi/spring/CoApiDefinition.kt that stores metadata about a CoApi interface, including the API type, base URL, service ID, and load-balanced status. It is created through the extension function Class<*>.toCoApiDefinition(Environment), which scans the class for @CoApi and @LoadBalanced annotations, resolves placeholder expressions like ${service.url}, and constructs the definition object passed to CoApiRegistrar for bean creation.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →