skip to content

How do you turn an @HttpExchange interface into a usable client using HttpServiceProxyFactory and an adapter?

level: middleimportance: must knowfreq 50%

answer

  1. client -> adapter -> factory -> createClient
  2. builderFor(adapter) is the 6.1+ API
  3. RestClientAdapter.create / WebClientAdapter.create
  4. config lives on RestClient, not the interface
  5. return proxy from an @Bean

basics

~10 s

Wrap a configured RestClient (or WebClient) in an adapter, pass it to HttpServiceProxyFactory.builderFor(adapter).build(), then call factory.createClient(MyInterface.class) to get a proxy you register as a bean.

solid answer

~40 s

The interface is inert until you build a proxy for it. Steps: (1) configure the real HTTP client — e.g. `RestClient.builder().baseUrl(...).build()` — this is where base URL, headers, interceptors, and timeouts live. (2) Wrap it in an exchange adapter: `RestClientAdapter.create(restClient)` (or `WebClientAdapter.create(webClient)`, `RestTemplateAdapter.create(...)`). (3) Build the factory: `HttpServiceProxyFactory.builderFor(adapter).build()`. (4) Create the proxy: `factory.createClient(UserClient.class)`. In a Spring app you do this inside an `@Bean` method so the proxy is injectable. The `builderFor(HttpExchangeAdapter)` form is the current API (Spring 6.1+); the older `builder(HttpClientAdapter)` and `WebClientAdapter.forClient(...)` are deprecated. Recent Spring Boot can auto-register these clients via group configuration, but the manual `@Bean` pattern remains the clearest.

code

java · 15 lines
java
@Configuration
class HttpClientsConfig {

    @Bean
    UserClient userClient(RestClient.Builder builder) {
        RestClient restClient = builder
                .baseUrl("https://api.example.com")
                .requestInterceptor(new AuthInterceptor())
                .build();
        HttpServiceProxyFactory factory = HttpServiceProxyFactory
                .builderFor(RestClientAdapter.create(restClient))
                .build();
        return factory.createClient(UserClient.class);
    }
}

go deeper

for a junior

Know that a factory + adapter produces the client and it must be a bean.

for a middle

Reproduce the four-step build fluently and know config belongs on the client.

for a senior

Discuss adapter choice, the 6.1 API change, and reflection-based proxy internals.

for a principal

Design registration at scale (group config vs manual beans), shared RestClient.Builder, and cross-cutting concerns via interceptors.

## The pipeline An `@HttpExchange` interface is just metadata. To get a working client you assemble three layers: 1. **The real HTTP client** — `RestClient`, `WebClient`, or `RestTemplate`. This is where *all* transport concerns live: `baseUrl`, default headers, request interceptors/filters, connection/read timeouts, message converters/codecs, error handlers. 2. **An exchange adapter** — a thin bridge implementing `HttpExchangeAdapter` (blocking) or `ReactorHttpExchangeAdapter` (reactive) that lets the proxy machinery drive that client: - `RestClientAdapter.create(restClient)` — blocking, modern. - `WebClientAdapter.create(webClient)` — reactive, supports `Mono`/`Flux`. - `RestTemplateAdapter.create(restTemplate)` — blocking, for legacy stacks. 3. **`HttpServiceProxyFactory`** — produces the JDK dynamic proxy that implements your interface. ## Canonical code (Spring 6.1+) ```java @Configuration class ClientConfig { @Bean UserClient userClient() { RestClient restClient = RestClient.builder() .baseUrl("https://api.example.com") .defaultHeader("X-App", "katajob") .build(); HttpServiceProxyFactory factory = HttpServiceProxyFactory .builderFor(RestClientAdapter.create(restClient)) .build(); return factory.createClient(UserClient.class); } } ``` Inject `UserClient` anywhere like any bean. ## API version notes (a common interview trap) - **Spring 6.0**: `HttpServiceProxyFactory.builder(WebClientAdapter.forClient(webClient)).build()`. - **Spring 6.1+**: use `builderFor(HttpExchangeAdapter)` with `RestClientAdapter.create(...)` / `WebClientAdapter.create(...)`. The 6.0 forms are **deprecated**. Interviewers like to hear you know the current API. ## Under the hood `createClient` returns a **JDK dynamic `Proxy`** whose `InvocationHandler` reads the annotations once (reflected into `HttpRequestValues`), then for each call builds an `HttpRequestValues`, hands it to the adapter, and adapts the response to the declared return type via `HttpServiceMethod`. No code generation, no bytecode weaving — pure reflection + proxy. ## Registering many clients - **Manual**: one `@Bean` per interface (explicit, easy to reason about). - **Group configuration** (Spring Framework 6.2 `HttpServiceProxyRegistry` / Spring Boot support via `@ImportHttpServices`-style registration): lets you register a set of interfaces against a named client group and have Boot auto-create the proxies. Useful at scale; the manual pattern is fine for a few clients. ## Gotchas - **Configure on the client, not the interface**: timeouts, retries, auth headers all belong on the RestClient/WebClient builder. - **One adapter kind per proxy**: a proxy over `RestClientAdapter` cannot return `Mono`/`Flux` — only the `WebClientAdapter` (reactive adapter) can. - **Reuse the factory/clients**: they're thread-safe and meant to be singletons; don't rebuild per request. - Don't forget to actually **register the proxy as a bean** — a common "it won't inject" mistake.

  • Where do you set the base URL, timeouts, and an auth header — on the interface or the client?
    On the underlying RestClient/WebClient builder. The interface annotations only describe paths, methods, and parameter binding; transport/config concerns live on the real client the adapter wraps.
  • What changed between Spring 6.0 and 6.1 in this API?
    6.0 used `HttpServiceProxyFactory.builder(HttpClientAdapter)` and `WebClientAdapter.forClient(...)`. 6.1 deprecated those in favor of `builderFor(HttpExchangeAdapter)` plus `RestClientAdapter.create(...)` / `WebClientAdapter.create(...)`.

saying these in an interview costs you the question

  • Trying to instantiate the interface directly or annotating it with @Component and expecting Spring to implement it.
  • Setting base URL / timeouts via annotation attributes.
  • Forgetting to expose the proxy as a bean, then wondering why injection fails.
  • Building a new factory/proxy on every request.

context