diff --git a/docs/modules/ROOT/pages/spring-cloud-openfeign.adoc b/docs/modules/ROOT/pages/spring-cloud-openfeign.adoc index bbaadc7d5..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 @@ -879,6 +887,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 +941,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; 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(); + } }; }