How do you turn an @HttpExchange interface into a usable client using HttpServiceProxyFactory and an adapter?
answer
- client -> adapter -> factory -> createClient
- builderFor(adapter) is the 6.1+ API
- RestClientAdapter.create / WebClientAdapter.create
- config lives on RestClient, not the interface
- return proxy from an @Bean
basics
~10 sWrap 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 sThe 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@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
Know that a factory + adapter produces the client and it must be a bean.
Reproduce the four-step build fluently and know config belongs on the client.
Discuss adapter choice, the 6.1 API change, and reflection-based proxy internals.
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.