From 39d5fa2e4264fcf81c3cdef011e7ae3de8277728 Mon Sep 17 00:00:00 2001 From: hutiefang Date: Wed, 1 Jul 2026 23:35:08 +0800 Subject: [PATCH 1/6] Fix circuit breaker Feign builder direct usage Signed-off-by: hutiefang --- .../FeignCircuitBreakerInvocationHandler.java | 8 ++- .../openfeign/FeignClientsConfiguration.java | 11 ++- .../FeignAutoConfigurationTests.java | 71 +++++++++++++++++++ 3 files changed, 85 insertions(+), 5 deletions(-) diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignCircuitBreakerInvocationHandler.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignCircuitBreakerInvocationHandler.java index 6acac2c0d..b58304efa 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignCircuitBreakerInvocationHandler.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignCircuitBreakerInvocationHandler.java @@ -95,9 +95,11 @@ else if ("toString".equals(method.getName())) { return toString(); } - String circuitName = circuitBreakerNameResolver.resolveCircuitBreakerName(feignClientName, target, method); - CircuitBreaker circuitBreaker = circuitBreakerGroupEnabled ? factory.create(circuitName, feignClientName) - : factory.create(circuitName); + String resolvedFeignClientName = feignClientName != null ? feignClientName : target.name(); + String circuitName = circuitBreakerNameResolver.resolveCircuitBreakerName(resolvedFeignClientName, target, + method); + CircuitBreaker circuitBreaker = circuitBreakerGroupEnabled + ? factory.create(circuitName, resolvedFeignClientName) : factory.create(circuitName); Supplier supplier = asSupplier(method, args); if (this.nullableFallbackFactory != null) { Function fallbackFunction = throwable -> { diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java index f85e59b30..10734ed26 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java @@ -36,6 +36,7 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -248,8 +249,14 @@ public Feign.Builder defaultFeignBuilder(Retryer retryer) { @Scope("prototype") @ConditionalOnMissingBean @ConditionalOnBean(CircuitBreakerFactory.class) - public Feign.Builder circuitBreakerFeignBuilder() { - return FeignCircuitBreaker.builder(); + public Feign.Builder circuitBreakerFeignBuilder(CircuitBreakerFactory circuitBreakerFactory, + @Value("${spring.cloud.openfeign.circuitbreaker.group.enabled:false}") boolean circuitBreakerGroupEnabled, + ObjectProvider circuitBreakerNameResolver) { + return FeignCircuitBreaker.builder() + .circuitBreakerFactory(circuitBreakerFactory) + .circuitBreakerGroupEnabled(circuitBreakerGroupEnabled) + .circuitBreakerNameResolver(circuitBreakerNameResolver + .getIfAvailable(() -> (feignClientName, target, method) -> Feign.configKey(target.type(), method))); } } diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java index 705a4153b..0aae57b8c 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java @@ -17,7 +17,11 @@ package org.springframework.cloud.openfeign; import java.lang.reflect.Method; +import java.util.Map; +import java.util.function.Supplier; +import feign.Feign; +import feign.InvocationHandlerFactory; import feign.Target; import org.assertj.core.api.Condition; import org.junit.jupiter.api.Test; @@ -25,6 +29,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.client.circuitbreaker.CircuitBreaker; import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; import org.springframework.cloud.openfeign.FeignAutoConfiguration.CircuitBreakerPresentFeignTargeterConfiguration.AlphanumericCircuitBreakerNameResolver; import org.springframework.cloud.openfeign.security.OAuth2AccessTokenInterceptor; @@ -33,7 +38,10 @@ import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; /** * @author Tim Peeters @@ -91,6 +99,63 @@ void shouldInstantiateFeignCircuitBreakerTargeterWhenEnabledWithCustomCircuitBre }); } + @Test + void shouldConfigureCircuitBreakerFeignBuilderWhenUsedDirectly() { + CircuitBreakerFactory circuitBreakerFactory = mock(CircuitBreakerFactory.class); + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(FeignAutoConfiguration.class, FeignClientsConfiguration.class)) + .withPropertyValues("spring.cloud.openfeign.circuitbreaker.enabled=true", + "spring.cloud.openfeign.circuitbreaker.group.enabled=true", + "spring.cloud.openfeign.httpclient.hc5.enabled=false") + .withBean(CircuitBreakerFactory.class, () -> circuitBreakerFactory) + .run(ctx -> { + CircuitBreakerNameResolver circuitBreakerNameResolver = ctx.getBean(CircuitBreakerNameResolver.class); + Feign.Builder builder = ctx.getBean(Feign.Builder.class); + + assertThat(builder).isInstanceOf(FeignCircuitBreaker.Builder.class) + .hasFieldOrPropertyWithValue("circuitBreakerFactory", circuitBreakerFactory) + .hasFieldOrPropertyWithValue("circuitBreakerGroupEnabled", true) + .hasFieldOrPropertyWithValue("circuitBreakerNameResolver", circuitBreakerNameResolver); + }); + } + + @Test + void shouldConfigureCircuitBreakerFeignBuilderWithoutNameResolverBean() { + CircuitBreakerFactory circuitBreakerFactory = mock(CircuitBreakerFactory.class); + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(FeignClientsConfiguration.class)) + .withPropertyValues("spring.cloud.openfeign.circuitbreaker.enabled=true", + "spring.cloud.openfeign.httpclient.hc5.enabled=false") + .withBean(CircuitBreakerFactory.class, () -> circuitBreakerFactory) + .run(ctx -> { + Feign.Builder builder = ctx.getBean(Feign.Builder.class); + + assertThat(builder).isInstanceOf(FeignCircuitBreaker.Builder.class) + .hasFieldOrPropertyWithValue("circuitBreakerFactory", circuitBreakerFactory) + .hasFieldOrProperty("circuitBreakerNameResolver"); + }); + } + + @Test + void shouldUseTargetNameWhenFeignClientNameIsNotSet() throws Throwable { + CircuitBreakerFactory circuitBreakerFactory = mock(CircuitBreakerFactory.class); + CircuitBreaker circuitBreaker = mock(CircuitBreaker.class); + Method method = DirectCircuitBreakerClient.class.getMethod("get"); + InvocationHandlerFactory.MethodHandler methodHandler = mock(InvocationHandlerFactory.MethodHandler.class); + Target target = new Target.HardCodedTarget<>(DirectCircuitBreakerClient.class, + "directClient", "http://localhost"); + FeignCircuitBreakerInvocationHandler handler = new FeignCircuitBreakerInvocationHandler(circuitBreakerFactory, + null, target, Map.of(method, methodHandler), null, true, + (feignClientName, targetType, targetMethod) -> feignClientName + "#" + targetMethod.getName()); + + when(circuitBreakerFactory.create("directClient#get", "directClient")).thenReturn(circuitBreaker); + when(circuitBreaker.run(any(Supplier.class))) + .thenAnswer(invocation -> invocation.>getArgument(0).get()); + when(methodHandler.invoke(null)).thenReturn("ok"); + + assertThat(handler.invoke(null, method, null)).isEqualTo("ok"); + verify(circuitBreakerFactory).create("directClient#get", "directClient"); + } + @Test void shouldInstantiateFeignOAuth2FeignRequestInterceptorWithoutInterceptors() { runner @@ -147,6 +212,12 @@ private void assertThatFeignCircuitBreakerTargeterHasSameCircuitBreakerNameResol assertThat(bean).isExactlyInstanceOf(beanClass); } + interface DirectCircuitBreakerClient { + + String get(); + + } + static class CustomCircuitBreakerNameResolver implements CircuitBreakerNameResolver { @Override From 193e977e055919256fe16980e6020edc570e7b41 Mon Sep 17 00:00:00 2001 From: hutiefang Date: Sat, 4 Jul 2026 10:37:59 +0800 Subject: [PATCH 2/6] Align direct Feign builder circuit breaker resolver Signed-off-by: hutiefang --- .../openfeign/FeignClientsConfiguration.java | 12 +++++- .../FeignAutoConfigurationTests.java | 41 ++++++++++++++++++- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java index 10734ed26..dc54d4b5c 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java @@ -46,6 +46,8 @@ import org.springframework.boot.http.converter.autoconfigure.ClientHttpMessageConvertersCustomizer; import org.springframework.cloud.client.circuitbreaker.CircuitBreaker; import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; +import org.springframework.cloud.openfeign.FeignAutoConfiguration.CircuitBreakerPresentFeignTargeterConfiguration.AlphanumericCircuitBreakerNameResolver; +import org.springframework.cloud.openfeign.FeignAutoConfiguration.CircuitBreakerPresentFeignTargeterConfiguration.DefaultCircuitBreakerNameResolver; import org.springframework.cloud.openfeign.clientconfig.FeignClientConfigurer; import org.springframework.cloud.openfeign.support.AbstractFormWriter; import org.springframework.cloud.openfeign.support.FeignEncoderProperties; @@ -251,12 +253,20 @@ public Feign.Builder defaultFeignBuilder(Retryer retryer) { @ConditionalOnBean(CircuitBreakerFactory.class) public Feign.Builder circuitBreakerFeignBuilder(CircuitBreakerFactory circuitBreakerFactory, @Value("${spring.cloud.openfeign.circuitbreaker.group.enabled:false}") boolean circuitBreakerGroupEnabled, + @Value("${spring.cloud.openfeign.circuitbreaker.alphanumeric-ids.enabled:true}") boolean alphanumericIdsEnabled, ObjectProvider circuitBreakerNameResolver) { return FeignCircuitBreaker.builder() .circuitBreakerFactory(circuitBreakerFactory) .circuitBreakerGroupEnabled(circuitBreakerGroupEnabled) .circuitBreakerNameResolver(circuitBreakerNameResolver - .getIfAvailable(() -> (feignClientName, target, method) -> Feign.configKey(target.type(), method))); + .getIfAvailable(() -> defaultCircuitBreakerNameResolver(alphanumericIdsEnabled))); + } + + private CircuitBreakerNameResolver defaultCircuitBreakerNameResolver(boolean alphanumericIdsEnabled) { + if (alphanumericIdsEnabled) { + return new AlphanumericCircuitBreakerNameResolver(); + } + return new DefaultCircuitBreakerNameResolver(); } } diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java index 0aae57b8c..9d5d607cb 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java @@ -32,10 +32,12 @@ import org.springframework.cloud.client.circuitbreaker.CircuitBreaker; import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; import org.springframework.cloud.openfeign.FeignAutoConfiguration.CircuitBreakerPresentFeignTargeterConfiguration.AlphanumericCircuitBreakerNameResolver; +import org.springframework.cloud.openfeign.FeignAutoConfiguration.CircuitBreakerPresentFeignTargeterConfiguration.DefaultCircuitBreakerNameResolver; import org.springframework.cloud.openfeign.security.OAuth2AccessTokenInterceptor; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -120,7 +122,7 @@ void shouldConfigureCircuitBreakerFeignBuilderWhenUsedDirectly() { } @Test - void shouldConfigureCircuitBreakerFeignBuilderWithoutNameResolverBean() { + void shouldConfigureCircuitBreakerFeignBuilderWithoutNameResolverBean() throws NoSuchMethodException { CircuitBreakerFactory circuitBreakerFactory = mock(CircuitBreakerFactory.class); new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(FeignClientsConfiguration.class)) .withPropertyValues("spring.cloud.openfeign.circuitbreaker.enabled=true", @@ -128,10 +130,45 @@ void shouldConfigureCircuitBreakerFeignBuilderWithoutNameResolverBean() { .withBean(CircuitBreakerFactory.class, () -> circuitBreakerFactory) .run(ctx -> { Feign.Builder builder = ctx.getBean(Feign.Builder.class); + CircuitBreakerNameResolver circuitBreakerNameResolver = (CircuitBreakerNameResolver) ReflectionTestUtils + .getField(builder, "circuitBreakerNameResolver"); + Method method = DirectCircuitBreakerClient.class.getMethod("get"); + Target target = new Target.HardCodedTarget<>( + DirectCircuitBreakerClient.class, "directClient", "http://localhost"); assertThat(builder).isInstanceOf(FeignCircuitBreaker.Builder.class) .hasFieldOrPropertyWithValue("circuitBreakerFactory", circuitBreakerFactory) - .hasFieldOrProperty("circuitBreakerNameResolver"); + .hasFieldOrPropertyWithValue("circuitBreakerNameResolver", circuitBreakerNameResolver); + assertThat(circuitBreakerNameResolver) + .isExactlyInstanceOf(AlphanumericCircuitBreakerNameResolver.class); + assertThat(circuitBreakerNameResolver.resolveCircuitBreakerName("directClient", target, method)) + .isEqualTo(Feign.configKey(target.type(), method).replaceAll("[^a-zA-Z0-9]", "")); + }); + } + + @Test + void shouldConfigureDefaultCircuitBreakerFeignBuilderNameResolverWhenAlphanumericIdsDisabled() + throws NoSuchMethodException { + CircuitBreakerFactory circuitBreakerFactory = mock(CircuitBreakerFactory.class); + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(FeignClientsConfiguration.class)) + .withPropertyValues("spring.cloud.openfeign.circuitbreaker.enabled=true", + "spring.cloud.openfeign.circuitbreaker.alphanumeric-ids.enabled=false", + "spring.cloud.openfeign.httpclient.hc5.enabled=false") + .withBean(CircuitBreakerFactory.class, () -> circuitBreakerFactory) + .run(ctx -> { + Feign.Builder builder = ctx.getBean(Feign.Builder.class); + CircuitBreakerNameResolver circuitBreakerNameResolver = (CircuitBreakerNameResolver) ReflectionTestUtils + .getField(builder, "circuitBreakerNameResolver"); + Method method = DirectCircuitBreakerClient.class.getMethod("get"); + Target target = new Target.HardCodedTarget<>( + DirectCircuitBreakerClient.class, "directClient", "http://localhost"); + + assertThat(builder).isInstanceOf(FeignCircuitBreaker.Builder.class) + .hasFieldOrPropertyWithValue("circuitBreakerFactory", circuitBreakerFactory) + .hasFieldOrPropertyWithValue("circuitBreakerNameResolver", circuitBreakerNameResolver); + assertThat(circuitBreakerNameResolver).isExactlyInstanceOf(DefaultCircuitBreakerNameResolver.class); + assertThat(circuitBreakerNameResolver.resolveCircuitBreakerName("directClient", target, method)) + .isEqualTo(Feign.configKey(target.type(), method)); }); } From e83057918ff496fef03c0c4d228c316179dfb281 Mon Sep 17 00:00:00 2001 From: hutiefang Date: Tue, 7 Jul 2026 00:02:48 +0800 Subject: [PATCH 3/6] Preserve circuit breaker builder method signature Signed-off-by: hutiefang --- .../openfeign/FeignClientsConfiguration.java | 20 +++++++++++++++++-- .../FeignAutoConfigurationTests.java | 10 ++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java index dc54d4b5c..6f8e2557c 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java @@ -240,6 +240,18 @@ public Feign.Builder feignBuilder(Retryer retryer) { @ConditionalOnProperty("spring.cloud.openfeign.circuitbreaker.enabled") protected static class CircuitBreakerPresentFeignBuilderConfiguration { + @Autowired + private CircuitBreakerFactory circuitBreakerFactory; + + @Value("${spring.cloud.openfeign.circuitbreaker.group.enabled:false}") + private boolean circuitBreakerGroupEnabled; + + @Value("${spring.cloud.openfeign.circuitbreaker.alphanumeric-ids.enabled:true}") + private boolean alphanumericIdsEnabled; + + @Autowired + private ObjectProvider circuitBreakerNameResolver; + @Bean @Scope("prototype") @ConditionalOnMissingBean({ Feign.Builder.class, CircuitBreakerFactory.class }) @@ -251,9 +263,13 @@ public Feign.Builder defaultFeignBuilder(Retryer retryer) { @Scope("prototype") @ConditionalOnMissingBean @ConditionalOnBean(CircuitBreakerFactory.class) + public Feign.Builder circuitBreakerFeignBuilder() { + return circuitBreakerFeignBuilder(circuitBreakerFactory, circuitBreakerGroupEnabled, alphanumericIdsEnabled, + circuitBreakerNameResolver); + } + public Feign.Builder circuitBreakerFeignBuilder(CircuitBreakerFactory circuitBreakerFactory, - @Value("${spring.cloud.openfeign.circuitbreaker.group.enabled:false}") boolean circuitBreakerGroupEnabled, - @Value("${spring.cloud.openfeign.circuitbreaker.alphanumeric-ids.enabled:true}") boolean alphanumericIdsEnabled, + boolean circuitBreakerGroupEnabled, boolean alphanumericIdsEnabled, ObjectProvider circuitBreakerNameResolver) { return FeignCircuitBreaker.builder() .circuitBreakerFactory(circuitBreakerFactory) diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java index 9d5d607cb..63158692a 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java @@ -17,6 +17,7 @@ package org.springframework.cloud.openfeign; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.Map; import java.util.function.Supplier; @@ -101,6 +102,15 @@ void shouldInstantiateFeignCircuitBreakerTargeterWhenEnabledWithCustomCircuitBre }); } + @Test + void shouldKeepNoArgCircuitBreakerFeignBuilderSignature() throws NoSuchMethodException { + Method method = FeignClientsConfiguration.CircuitBreakerPresentFeignBuilderConfiguration.class + .getDeclaredMethod("circuitBreakerFeignBuilder"); + + assertThat(Modifier.isPublic(method.getModifiers())).isTrue(); + assertThat(method.getReturnType()).isEqualTo(Feign.Builder.class); + } + @Test void shouldConfigureCircuitBreakerFeignBuilderWhenUsedDirectly() { CircuitBreakerFactory circuitBreakerFactory = mock(CircuitBreakerFactory.class); From 1129178a29ad51802bca0e8793f1ff2f40a766dc Mon Sep 17 00:00:00 2001 From: hutiefang76 <137664623+hutiefang76@users.noreply.github.com> Date: Thu, 27 Aug 2026 02:10:56 +0800 Subject: [PATCH 4/6] Refine circuit breaker builder configuration Signed-off-by: hutiefang76 <137664623+hutiefang76@users.noreply.github.com> --- .../openfeign/FeignAutoConfiguration.java | 9 +-- .../FeignCircuitBreakerProperties.java | 75 +++++++++++++++++++ .../openfeign/FeignClientsConfiguration.java | 33 +++----- .../FeignAutoConfigurationTests.java | 18 +++-- 4 files changed, 102 insertions(+), 33 deletions(-) create mode 100644 spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignCircuitBreakerProperties.java diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java index cb6b1f413..7e875b643 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java @@ -37,7 +37,6 @@ import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.hint.TypeReference; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -86,7 +85,7 @@ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(Feign.class) @EnableConfigurationProperties({ FeignClientProperties.class, FeignHttpClientProperties.class, - FeignEncoderProperties.class, FeignOAuth2Properties.class }) + FeignEncoderProperties.class, FeignOAuth2Properties.class, FeignCircuitBreakerProperties.class }) public class FeignAutoConfiguration { private static final Log LOG = LogFactory.getLog(FeignAutoConfiguration.class); @@ -189,10 +188,10 @@ public CircuitBreakerNameResolver alphanumericCircuitBreakerNameResolver() { @ConditionalOnMissingBean @ConditionalOnBean(CircuitBreakerFactory.class) public Targeter circuitBreakerFeignTargeter(CircuitBreakerFactory circuitBreakerFactory, - @Value("${spring.cloud.openfeign.circuitbreaker.group.enabled:false}") boolean circuitBreakerGroupEnabled, + FeignCircuitBreakerProperties circuitBreakerProperties, CircuitBreakerNameResolver circuitBreakerNameResolver) { - return new FeignCircuitBreakerTargeter(circuitBreakerFactory, circuitBreakerGroupEnabled, - circuitBreakerNameResolver); + return new FeignCircuitBreakerTargeter(circuitBreakerFactory, + circuitBreakerProperties.getGroup().isEnabled(), circuitBreakerNameResolver); } static class DefaultCircuitBreakerNameResolver implements CircuitBreakerNameResolver { diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignCircuitBreakerProperties.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignCircuitBreakerProperties.java new file mode 100644 index 000000000..7003a6a2c --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignCircuitBreakerProperties.java @@ -0,0 +1,75 @@ +/* + * Copyright 2013-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.openfeign; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties for Feign circuit breaker support. + */ +@ConfigurationProperties("spring.cloud.openfeign.circuitbreaker") +public class FeignCircuitBreakerProperties { + + private Group group = new Group(); + + private AlphanumericIds alphanumericIds = new AlphanumericIds(); + + public Group getGroup() { + return group; + } + + public void setGroup(Group group) { + this.group = group; + } + + public AlphanumericIds getAlphanumericIds() { + return alphanumericIds; + } + + public void setAlphanumericIds(AlphanumericIds alphanumericIds) { + this.alphanumericIds = alphanumericIds; + } + + public static class Group { + + private boolean enabled; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + } + + public static class AlphanumericIds { + + private boolean enabled = true; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + } + +} diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java index 6f8e2557c..3b63d22cf 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java @@ -36,7 +36,6 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -240,18 +239,6 @@ public Feign.Builder feignBuilder(Retryer retryer) { @ConditionalOnProperty("spring.cloud.openfeign.circuitbreaker.enabled") protected static class CircuitBreakerPresentFeignBuilderConfiguration { - @Autowired - private CircuitBreakerFactory circuitBreakerFactory; - - @Value("${spring.cloud.openfeign.circuitbreaker.group.enabled:false}") - private boolean circuitBreakerGroupEnabled; - - @Value("${spring.cloud.openfeign.circuitbreaker.alphanumeric-ids.enabled:true}") - private boolean alphanumericIdsEnabled; - - @Autowired - private ObjectProvider circuitBreakerNameResolver; - @Bean @Scope("prototype") @ConditionalOnMissingBean({ Feign.Builder.class, CircuitBreakerFactory.class }) @@ -259,27 +246,27 @@ public Feign.Builder defaultFeignBuilder(Retryer retryer) { return Feign.builder().retryer(retryer); } + public Feign.Builder circuitBreakerFeignBuilder() { + return FeignCircuitBreaker.builder(); + } + @Bean @Scope("prototype") @ConditionalOnMissingBean @ConditionalOnBean(CircuitBreakerFactory.class) - public Feign.Builder circuitBreakerFeignBuilder() { - return circuitBreakerFeignBuilder(circuitBreakerFactory, circuitBreakerGroupEnabled, alphanumericIdsEnabled, - circuitBreakerNameResolver); - } - public Feign.Builder circuitBreakerFeignBuilder(CircuitBreakerFactory circuitBreakerFactory, - boolean circuitBreakerGroupEnabled, boolean alphanumericIdsEnabled, + FeignCircuitBreakerProperties circuitBreakerProperties, ObjectProvider circuitBreakerNameResolver) { return FeignCircuitBreaker.builder() .circuitBreakerFactory(circuitBreakerFactory) - .circuitBreakerGroupEnabled(circuitBreakerGroupEnabled) + .circuitBreakerGroupEnabled(circuitBreakerProperties.getGroup().isEnabled()) .circuitBreakerNameResolver(circuitBreakerNameResolver - .getIfAvailable(() -> defaultCircuitBreakerNameResolver(alphanumericIdsEnabled))); + .getIfAvailable(() -> defaultCircuitBreakerNameResolver(circuitBreakerProperties))); } - private CircuitBreakerNameResolver defaultCircuitBreakerNameResolver(boolean alphanumericIdsEnabled) { - if (alphanumericIdsEnabled) { + private CircuitBreakerNameResolver defaultCircuitBreakerNameResolver( + FeignCircuitBreakerProperties circuitBreakerProperties) { + if (circuitBreakerProperties.getAlphanumericIds().isEnabled()) { return new AlphanumericCircuitBreakerNameResolver(); } return new DefaultCircuitBreakerNameResolver(); diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java index 63158692a..7328ec5a8 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java @@ -27,6 +27,7 @@ import org.assertj.core.api.Condition; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -36,6 +37,7 @@ import org.springframework.cloud.openfeign.FeignAutoConfiguration.CircuitBreakerPresentFeignTargeterConfiguration.DefaultCircuitBreakerNameResolver; import org.springframework.cloud.openfeign.security.OAuth2AccessTokenInterceptor; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.test.util.ReflectionTestUtils; @@ -104,11 +106,15 @@ void shouldInstantiateFeignCircuitBreakerTargeterWhenEnabledWithCustomCircuitBre @Test void shouldKeepNoArgCircuitBreakerFeignBuilderSignature() throws NoSuchMethodException { - Method method = FeignClientsConfiguration.CircuitBreakerPresentFeignBuilderConfiguration.class + Method noArgMethod = FeignClientsConfiguration.CircuitBreakerPresentFeignBuilderConfiguration.class .getDeclaredMethod("circuitBreakerFeignBuilder"); + Method beanMethod = FeignClientsConfiguration.CircuitBreakerPresentFeignBuilderConfiguration.class + .getDeclaredMethod("circuitBreakerFeignBuilder", CircuitBreakerFactory.class, + FeignCircuitBreakerProperties.class, ObjectProvider.class); - assertThat(Modifier.isPublic(method.getModifiers())).isTrue(); - assertThat(method.getReturnType()).isEqualTo(Feign.Builder.class); + assertThat(Modifier.isPublic(noArgMethod.getModifiers())).isTrue(); + assertThat(noArgMethod.getReturnType()).isEqualTo(Feign.Builder.class); + assertThat(beanMethod.isAnnotationPresent(Bean.class)).isTrue(); } @Test @@ -134,7 +140,8 @@ void shouldConfigureCircuitBreakerFeignBuilderWhenUsedDirectly() { @Test void shouldConfigureCircuitBreakerFeignBuilderWithoutNameResolverBean() throws NoSuchMethodException { CircuitBreakerFactory circuitBreakerFactory = mock(CircuitBreakerFactory.class); - new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(FeignClientsConfiguration.class)) + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(FeignAutoConfiguration.class, FeignClientsConfiguration.class)) .withPropertyValues("spring.cloud.openfeign.circuitbreaker.enabled=true", "spring.cloud.openfeign.httpclient.hc5.enabled=false") .withBean(CircuitBreakerFactory.class, () -> circuitBreakerFactory) @@ -160,7 +167,8 @@ void shouldConfigureCircuitBreakerFeignBuilderWithoutNameResolverBean() throws N void shouldConfigureDefaultCircuitBreakerFeignBuilderNameResolverWhenAlphanumericIdsDisabled() throws NoSuchMethodException { CircuitBreakerFactory circuitBreakerFactory = mock(CircuitBreakerFactory.class); - new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(FeignClientsConfiguration.class)) + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(FeignAutoConfiguration.class, FeignClientsConfiguration.class)) .withPropertyValues("spring.cloud.openfeign.circuitbreaker.enabled=true", "spring.cloud.openfeign.circuitbreaker.alphanumeric-ids.enabled=false", "spring.cloud.openfeign.httpclient.hc5.enabled=false") From ee9bbf895b00e23006390d835418e19dc351ea43 Mon Sep 17 00:00:00 2001 From: hutiefang76 <137664623+hutiefang76@users.noreply.github.com> Date: Thu, 27 Aug 2026 23:55:00 +0800 Subject: [PATCH 5/6] Preserve circuit breaker targeter API Signed-off-by: hutiefang76 <137664623+hutiefang76@users.noreply.github.com> --- .../cloud/openfeign/FeignAutoConfiguration.java | 13 +++++++++++-- .../openfeign/FeignAutoConfigurationTests.java | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java index 7e875b643..498a3eb06 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java @@ -37,6 +37,7 @@ import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.hint.TypeReference; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -183,6 +184,14 @@ public CircuitBreakerNameResolver alphanumericCircuitBreakerNameResolver() { return new AlphanumericCircuitBreakerNameResolver(); } + @SuppressWarnings("rawtypes") + public Targeter circuitBreakerFeignTargeter(CircuitBreakerFactory circuitBreakerFactory, + @Value("${spring.cloud.openfeign.circuitbreaker.group.enabled:false}") boolean circuitBreakerGroupEnabled, + CircuitBreakerNameResolver circuitBreakerNameResolver) { + return new FeignCircuitBreakerTargeter(circuitBreakerFactory, circuitBreakerGroupEnabled, + circuitBreakerNameResolver); + } + @SuppressWarnings("rawtypes") @Bean @ConditionalOnMissingBean @@ -190,8 +199,8 @@ public CircuitBreakerNameResolver alphanumericCircuitBreakerNameResolver() { public Targeter circuitBreakerFeignTargeter(CircuitBreakerFactory circuitBreakerFactory, FeignCircuitBreakerProperties circuitBreakerProperties, CircuitBreakerNameResolver circuitBreakerNameResolver) { - return new FeignCircuitBreakerTargeter(circuitBreakerFactory, - circuitBreakerProperties.getGroup().isEnabled(), circuitBreakerNameResolver); + return circuitBreakerFeignTargeter(circuitBreakerFactory, circuitBreakerProperties.getGroup().isEnabled(), + circuitBreakerNameResolver); } static class DefaultCircuitBreakerNameResolver implements CircuitBreakerNameResolver { diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java index 7328ec5a8..efcdf8b4a 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java @@ -117,6 +117,21 @@ void shouldKeepNoArgCircuitBreakerFeignBuilderSignature() throws NoSuchMethodExc assertThat(beanMethod.isAnnotationPresent(Bean.class)).isTrue(); } + @Test + void shouldKeepCircuitBreakerFeignTargeterSignature() throws NoSuchMethodException { + Method existingMethod = FeignAutoConfiguration.CircuitBreakerPresentFeignTargeterConfiguration.class + .getDeclaredMethod("circuitBreakerFeignTargeter", CircuitBreakerFactory.class, boolean.class, + CircuitBreakerNameResolver.class); + Method beanMethod = FeignAutoConfiguration.CircuitBreakerPresentFeignTargeterConfiguration.class + .getDeclaredMethod("circuitBreakerFeignTargeter", CircuitBreakerFactory.class, + FeignCircuitBreakerProperties.class, CircuitBreakerNameResolver.class); + + assertThat(Modifier.isPublic(existingMethod.getModifiers())).isTrue(); + assertThat(existingMethod.getReturnType()).isEqualTo(Targeter.class); + assertThat(existingMethod.isAnnotationPresent(Bean.class)).isFalse(); + assertThat(beanMethod.isAnnotationPresent(Bean.class)).isTrue(); + } + @Test void shouldConfigureCircuitBreakerFeignBuilderWhenUsedDirectly() { CircuitBreakerFactory circuitBreakerFactory = mock(CircuitBreakerFactory.class); From 8876ec32ffb9a8075cf7b61a8f1510ad28e96d87 Mon Sep 17 00:00:00 2001 From: hutiefang76 <137664623+hutiefang76@users.noreply.github.com> Date: Sat, 29 Aug 2026 08:48:55 +0800 Subject: [PATCH 6/6] Deprecate legacy circuit breaker auto-configuration methods Signed-off-by: hutiefang76 <137664623+hutiefang76@users.noreply.github.com> --- .../cloud/openfeign/FeignAutoConfiguration.java | 5 +++++ .../cloud/openfeign/FeignClientsConfiguration.java | 5 +++++ .../cloud/openfeign/FeignAutoConfigurationTests.java | 2 ++ 3 files changed, 12 insertions(+) diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java index 498a3eb06..56f28cde9 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java @@ -184,6 +184,11 @@ public CircuitBreakerNameResolver alphanumericCircuitBreakerNameResolver() { return new AlphanumericCircuitBreakerNameResolver(); } + /** + * @deprecated in favor of + * {@link #circuitBreakerFeignTargeter(CircuitBreakerFactory, FeignCircuitBreakerProperties, CircuitBreakerNameResolver)}. + */ + @Deprecated @SuppressWarnings("rawtypes") public Targeter circuitBreakerFeignTargeter(CircuitBreakerFactory circuitBreakerFactory, @Value("${spring.cloud.openfeign.circuitbreaker.group.enabled:false}") boolean circuitBreakerGroupEnabled, diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java index 3b63d22cf..ae7792bd0 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java @@ -246,6 +246,11 @@ public Feign.Builder defaultFeignBuilder(Retryer retryer) { return Feign.builder().retryer(retryer); } + /** + * @deprecated in favor of + * {@link #circuitBreakerFeignBuilder(CircuitBreakerFactory, FeignCircuitBreakerProperties, ObjectProvider)}. + */ + @Deprecated public Feign.Builder circuitBreakerFeignBuilder() { return FeignCircuitBreaker.builder(); } diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java index efcdf8b4a..6ae23257d 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java @@ -114,6 +114,7 @@ void shouldKeepNoArgCircuitBreakerFeignBuilderSignature() throws NoSuchMethodExc assertThat(Modifier.isPublic(noArgMethod.getModifiers())).isTrue(); assertThat(noArgMethod.getReturnType()).isEqualTo(Feign.Builder.class); + assertThat(noArgMethod.isAnnotationPresent(Deprecated.class)).isTrue(); assertThat(beanMethod.isAnnotationPresent(Bean.class)).isTrue(); } @@ -128,6 +129,7 @@ void shouldKeepCircuitBreakerFeignTargeterSignature() throws NoSuchMethodExcepti assertThat(Modifier.isPublic(existingMethod.getModifiers())).isTrue(); assertThat(existingMethod.getReturnType()).isEqualTo(Targeter.class); + assertThat(existingMethod.isAnnotationPresent(Deprecated.class)).isTrue(); assertThat(existingMethod.isAnnotationPresent(Bean.class)).isFalse(); assertThat(beanMethod.isAnnotationPresent(Bean.class)).isTrue(); }