Zero‑Intrusion Dynamic Enhancements for Spring Boot RestClient

The article explains how to eliminate boilerplate when using Spring Boot 3.5.0 RestClient by introducing two custom annotations, @ClientEnhance and @ClientConfig, together with an auto‑configuration class that injects logging interceptors and configurable timeouts into selected RestClient.Builder beans, enabling a non‑intrusive, declarative enhancement.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Zero‑Intrusion Dynamic Enhancements for Spring Boot RestClient

In projects that heavily use RestClient for remote HTTP calls, the native client lacks unified logging and flexible timeout configuration. Writing interceptors and request factories for each call creates repetitive boilerplate, raises maintenance cost, and can cause bean injection conflicts when multiple client instances coexist. Mixing marker and configuration in a single annotation also triggers Spring qualifier matching errors. To solve these issues, the article splits the marker and configuration into two annotations and applies a post‑processing auto‑configuration, achieving declarative configuration, reducing duplicate code, and providing unified control of HTTP logging and timeout.

Custom marker annotation

@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface ClientEnhance {}

This annotation mirrors Spring Cloud’s @LoadBalanced design, serving solely to select and filter RestClient.Builder beans in the container without carrying any business parameters.

Configuration annotation

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ClientConfig {
  // Whether to enable request/response logging
  boolean logEnable() default true;
  // Connection timeout in ms
  int connectTimeout() default -1;
  // Read timeout in ms
  int readTimeout() default -1;
}

Placed on @Bean factory methods, this annotation holds the enhancement parameters for a specific RestClient.Builder bean.

Auto‑configuration class

@AutoConfiguration
public class ClientEnhanceAutoConfiguration {
  @ClientEnhance
  @Autowired(required = false)
  private Map<String, RestClient.Builder> restClientBuilders = Map.of();

  @Bean
  ClientLogInterceptor clientLogInterceptor() {
    return new ClientLogInterceptor();
  }

  @Bean
  SmartInitializingSingleton clientEnhanceRestClientBuilder(DefaultListableBeanFactory factory) {
    return () -> restClientBuilders.forEach((beanName, builder) -> {
      // 1. Retrieve BeanDefinition, skip if not found
      BeanDefinition beanDef;
      try {
        beanDef = factory.getBeanDefinition(beanName);
      } catch (Exception e) {
        return;
      }
      String factoryBeanName = beanDef.getFactoryBeanName();
      String factoryMethodName = beanDef.getFactoryMethodName();
      if (factoryBeanName == null || factoryMethodName == null) {
        return;
      }
      // 2. Reflect the @Bean factory method
      Method factoryMethod = getBeanFactoryMethod(factory, factoryBeanName, factoryMethodName);
      if (factoryMethod == null) {
        return;
      }
      // 3. Obtain @ClientConfig
      ClientConfig clientConfig = AnnotationUtils.findAnnotation(factoryMethod, ClientConfig.class);
      if (clientConfig == null) {
        return;
      }
      // 4. Add logging interceptor if enabled
      if (clientConfig.logEnable()) {
        builder.requestInterceptors(interceptors -> interceptors.add(clientLogInterceptor()));
      }
      // 5. Configure timeouts if specified
      long connectTimeoutMs = clientConfig.connectTimeout();
      long readTimeoutMs = clientConfig.readTimeout();
      boolean needSetTimeout = connectTimeoutMs > 0 || readTimeoutMs > 0;
      if (needSetTimeout) {
        SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
        if (connectTimeoutMs > 0) {
          requestFactory.setConnectTimeout((int) connectTimeoutMs);
        }
        if (readTimeoutMs > 0) {
          requestFactory.setReadTimeout((int) readTimeoutMs);
        }
        builder.requestFactory(requestFactory);
      }
    });
  }

  private static Method getBeanFactoryMethod(BeanFactory factory, String factoryBeanName, String factoryMethodName) {
    Object factoryBean;
    try {
      factoryBean = factory.getBean(factoryBeanName);
    } catch (Exception e) {
      return null;
    }
    Class<?> beanClass = factoryBean.getClass();
    for (Method method : beanClass.getDeclaredMethods()) {
      if (factoryMethodName.equals(method.getName())) {
        method.setAccessible(true);
        return method;
      }
    }
    return null;
  }
}

The auto‑configuration collects all RestClient.Builder beans marked with @ClientEnhance, reads the corresponding @ClientConfig on their factory methods, and dynamically adds a ClientLogInterceptor and a custom SimpleClientHttpRequestFactory with the specified timeouts.

RestClient bean definitions

@Configuration
public class RestClientConfig {
  @Bean
  @ClientEnhance
  @ClientConfig(logEnable = true, connectTimeout = 3000, readTimeout = 5000)
  RestClient.Builder restClientBuilderEnhance() {
    return RestClient.builder();
  }

  @Bean
  RestClient.Builder restClientBuilder() {
    return RestClient.builder();
  }
}

Two beans are declared: one enhanced client with logging and timeout settings, and one plain client, demonstrating that enhanced and ordinary clients can coexist.

Test scenario

@RestController
@RequestMapping("/users")
public class UserController {
  @GetMapping("/query")
  public String query() throws Exception {
    TimeUnit.SECONDS.sleep(3);
    return "users query...";
  }
}

private final RestClient.Builder restClientBuilder;
public ClientController(@ClientEnhance RestClient.Builder restClientBuilder) {
  this.restClientBuilder = restClientBuilder;
}

@GetMapping("/query")
public Object query() {
  return restClientBuilder.build()
      .get()
      .uri("http://localhost:8080/users/query")
      .retrieve()
      .body(String.class);
}

The article includes screenshots (kept below) showing a normal request response and a timeout case, confirming that the interceptor logs the request/response and the configured timeouts take effect.

By separating the marker and configuration concerns into @ClientEnhance and @ClientConfig and leveraging Spring Boot’s auto‑configuration mechanism, developers obtain a zero‑intrusion, configurable way to add common cross‑cutting concerns such as logging and timeout handling to RestClient without altering existing client code.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaSpring Bootcustom-annotationRestClientAutoConfiguration
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.