From 5e2bcc54b1298a0d694d89ec73ba644603be7db7 Mon Sep 17 00:00:00 2001 From: 98001yash Date: Thu, 2 Apr 2026 07:57:57 +0530 Subject: [PATCH 1/2] Add documentation for FeignClientBuilder Signed-off-by: 98001yash --- .../ROOT/pages/spring-cloud-openfeign.adoc | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/docs/modules/ROOT/pages/spring-cloud-openfeign.adoc b/docs/modules/ROOT/pages/spring-cloud-openfeign.adoc index bbaadc7d5..8c428953c 100644 --- a/docs/modules/ROOT/pages/spring-cloud-openfeign.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-openfeign.adoc @@ -879,6 +879,46 @@ protected interface DemoFeignClient { } ---- + +=== FeignClientBuilder + +`FeignClientBuilder` allows programmatic creation of Feign clients without using the `@FeignClient` annotation. + +It builds clients in the same way as `@FeignClient`, but provides flexibility for dynamic use cases. + +Unlike `@FeignClient`, which defines clients statically, `FeignClientBuilder` allows creating clients dynamically at runtime. + +==== Basic Usage + +[source,java,indent=0] +---- +@Autowired +private ApplicationContext applicationContext; + +FeignClientBuilder builder = new FeignClientBuilder(applicationContext); + +MyClient client = builder + .forType(MyClient.class, "myClient") + .url("http://localhost:8080") + .build(); +---- + +==== Configuration Options + +* `url(String url)` - Sets the target URL +* `path(String path)` - Adds a base path +* `contextId(String contextId)` - Unique identifier +* `dismiss404(boolean)` - Ignore 404 errors +* `inheritParentContext(boolean)` - Inherit parent config +* `fallback(Class)` - Fallback class +* `customize(FeignBuilderCustomizer)` - Customize Feign builder + +==== When to Use + +* Dynamic client creation +* Runtime configuration +* When `@FeignClient` is not sufficient + [[reactive-support]] === Reactive Support As at the time of active development of Spring Cloud OpenFeign, the https://github.com/OpenFeign/feign[OpenFeign project] did not support reactive clients, such as https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/reactive/function/client/WebClient.html[Spring WebClient], such support could not be added to Spring Cloud OpenFeign either. @@ -893,6 +933,7 @@ We discourage using Feign clients in the early stages of application lifecycle, Similarly, depending on how you are using your Feign clients, you may see initialization errors when starting your application. To work around this problem you can use an `ObjectProvider` when autowiring your client. [source,java,indent=0] + ---- @Autowired ObjectProvider testFeignClient; From beb033d20e8282879126d899ff826a028499a58d Mon Sep 17 00:00:00 2001 From: 98001yash Date: Thu, 2 Apr 2026 12:55:20 +0530 Subject: [PATCH 2/2] fix: prevent duplicate cache error handler execution in FeignCachingInvocationHandlerFactory Closes #681 Signed-off-by: 98001yash --- .../ROOT/pages/spring-cloud-openfeign.adoc | 8 +++ .../FeignCachingInvocationHandlerFactory.java | 68 ++++++++++++------- 2 files changed, 50 insertions(+), 26 deletions(-) diff --git a/docs/modules/ROOT/pages/spring-cloud-openfeign.adoc b/docs/modules/ROOT/pages/spring-cloud-openfeign.adoc index 8c428953c..d84724939 100644 --- a/docs/modules/ROOT/pages/spring-cloud-openfeign.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-openfeign.adoc @@ -745,6 +745,14 @@ public interface DemoClient { You can also disable the feature via property `spring.cloud.openfeign.cache.enabled=false`. +==== Limitations + +Using `@Cacheable(sync = true)` with Feign clients may cause recursive cache invocation and result in an `IllegalStateException`. + +This happens due to the interaction between Feign proxies and Spring Cache synchronization. + +As a workaround, avoid using `sync = true` or disable Feign caching (`spring.cloud.openfeign.cache.enabled=false`). + [[spring-requestmapping-support]] === Spring @RequestMapping Support diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignCachingInvocationHandlerFactory.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignCachingInvocationHandlerFactory.java index 4d94d8bba..27716eb2c 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignCachingInvocationHandlerFactory.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignCachingInvocationHandlerFactory.java @@ -39,6 +39,9 @@ public class FeignCachingInvocationHandlerFactory implements InvocationHandlerFa private final CacheInterceptor cacheInterceptor; + // ADDED: ThreadLocal guard + private static final ThreadLocal CACHE_IN_PROGRESS = ThreadLocal.withInitial(() -> false); + public FeignCachingInvocationHandlerFactory(InvocationHandlerFactory delegateFactory, CacheInterceptor cacheInterceptor) { this.delegateFactory = delegateFactory; @@ -50,32 +53,45 @@ public InvocationHandler create(Target target, Map dispat final InvocationHandler delegateHandler = delegateFactory.create(target, dispatch); return (proxy, method, argsNullable) -> { Object[] args = Optional.ofNullable(argsNullable).orElseGet(() -> new Object[0]); - return cacheInterceptor.invoke(new MethodInvocation() { - @Override - public Method getMethod() { - return method; - } - - @Override - public Object[] getArguments() { - return args; - } - - @Override - public Object proceed() throws Throwable { - return delegateHandler.invoke(proxy, method, args); - } - - @Override - public Object getThis() { - return target; - } - - @Override - public AccessibleObject getStaticPart() { - return method; - } - }); + + // ✅ ADDED: Prevent nested cache invocation + if (CACHE_IN_PROGRESS.get()) { + return delegateHandler.invoke(proxy, method, args); + } + + try { + CACHE_IN_PROGRESS.set(true); + + return cacheInterceptor.invoke(new MethodInvocation() { + @Override + public Method getMethod() { + return method; + } + + @Override + public Object[] getArguments() { + return args; + } + + @Override + public Object proceed() throws Throwable { + return delegateHandler.invoke(proxy, method, args); + } + + @Override + public Object getThis() { + return target; + } + + @Override + public AccessibleObject getStaticPart() { + return method; + } + }); + } + finally { + CACHE_IN_PROGRESS.remove(); + } }; }