Skip to content

Commit 470a4b3

Browse files
Add interface clients support for circuit breaker (#1532)
--------- Signed-off-by: Olga Maciaszek-Sharma <[email protected]>
1 parent 5c655b3 commit 470a4b3

21 files changed

+2204
-14
lines changed

docs/modules/ROOT/pages/spring-cloud-circuitbreaker.adoc

Lines changed: 107 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ Spring Cloud supports the following circuit-breaker implementations:
1616
[[core-concepts]]
1717
== Core Concepts
1818

19-
To create a circuit breaker in your code, you can use the `CircuitBreakerFactory` API. When you include a Spring Cloud Circuit Breaker starter on your classpath, a bean that implements this API is automatically created for you.
19+
To create a circuit breaker in your code, you can use the `CircuitBreakerFactory` API.
20+
When you include a Spring Cloud Circuit Breaker starter on your classpath, a bean that implements this API is automatically created for you.
2021
The following example shows a simple example of how to use this API:
2122

2223
[source,java]
@@ -82,16 +83,16 @@ that caused the failure.
8283
You can configure your circuit breakers by creating beans of type `Customizer`.
8384
The `Customizer` interface has a single method (called `customize`) that takes the `Object` to customize.
8485

85-
For detailed information on how to customize a given implementation see
86-
the following documentation:
86+
For detailed information on how to customize a given implementation see the following documentation:
8787

8888
* link:../../../../spring-cloud-circuitbreaker/reference/spring-cloud-circuitbreaker-resilience4j.html[Resilience4J]
8989
* link:https://github.com/alibaba/spring-cloud-alibaba/blob/master/spring-cloud-alibaba-docs/src/main/asciidoc/circuitbreaker-sentinel.adoc#circuit-breaker-spring-cloud-circuit-breaker-with-sentinel--configuring-sentinel-circuit-breakers[Sentinel]
9090
* link:../../../../../spring-cloud-circuitbreaker/reference/spring-cloud-circuitbreaker-spring-retry.html[Spring Retry]
9191

9292
Some `CircuitBreaker` implementations such as `Resilience4JCircuitBreaker` call `customize` method every time `CircuitBreaker#run` is called.
93-
It can be inefficient. In that case, you can use `CircuitBreaker#once` method. It is useful where calling `customize` many times doesn't make sense,
94-
for example, in case of https://resilience4j.readme.io/docs/circuitbreaker#section-consume-emitted-circuitbreakerevents[consuming Resilience4j's events].
93+
It can be inefficient.
94+
In that case, you can use `CircuitBreaker#once` method.
95+
It is useful where calling `customize` many times doesn't make sense, for example, in case of https://resilience4j.readme.io/docs/circuitbreaker#section-consume-emitted-circuitbreakerevents[consuming Resilience4j's events].
9596

9697
The following example shows the way for each `io.github.resilience4j.circuitbreaker.CircuitBreaker` to consume events.
9798

@@ -102,3 +103,104 @@ Customizer.once(circuitBreaker -> {
102103
.onStateTransition(event -> log.info("{}: {}", event.getCircuitBreakerName(), event.getStateTransition()));
103104
}, CircuitBreaker::getName)
104105
----
106+
107+
[[interface-clients]]
108+
== Spring Interface Clients Support
109+
110+
Spring Cloud provides support for Spring Interface Clients integration through the following configurers:
111+
112+
- `CircuitBreakerRestClientHttpServiceGroupConfigurer`
113+
- `CircuitBreakerWebClientHttpServiceGroupConfigurer`
114+
115+
These configurers enable CircuitBreaker support for https://docs.spring.io/spring-framework/reference/7.0-SNAPSHOT/integration/rest-clients.html#rest-http-interface-group-config[Spring Interface Client Groups].
116+
117+
When fallback classes are configured using the `@HttpServiceFallbackAnnotation`,
118+
CircuitBreaker adapter decorators are added:
119+
- `CircuitBreakerAdapterDecorator` is used with `RestClient`
120+
- `ReactiveCircuitBreakerAdapterDecorator` is used with `WebClient`
121+
122+
[NOTE]
123+
====
124+
You can disable CircuitBreaker integration for HTTP service clients by setting the appropriate property:
125+
126+
- For blocking (`RestClient`) clients: `spring.cloud.circuitbreaker.http-services.enabled=false`
127+
- For reactive (`WebClient`) clients: `spring.cloud.circuitbreaker.reactive-http-services.enabled=false`
128+
129+
This prevents CircuitBreaker decorators from being applied to interface-based HTTP client groups.
130+
====
131+
132+
=== Declaring Fallbacks with Annotations
133+
134+
Fallbacks are configured using the `@HttpServiceFallback` annotation on configuration classes.
135+
This annotation allows you to declare:
136+
137+
- The fallback implementation class (via `value`)
138+
- The service interfaces the fallback supports (via `forService`, optional)
139+
- The group the fallback applies to (via `forGroup`, optional)
140+
141+
Multiple `@HttpServiceFallback` annotations can be declared on the same class using Java’s `@Repeatable` annotation mechanism.
142+
If no group is specified, the fallback applies to all groups that do not have an explicit per-group fallback for the given service interfaces.
143+
144+
Fallback classes are resolved using the following precedence:
145+
146+
. A fallback class with both matching `forService` and `forGroup`
147+
. A fallback class with matching `forService` and no `forGroup` (global fallback for service)
148+
. A fallback class with no `forService` or `forGroup` (default for all services in group or globally)
149+
150+
==== Example
151+
152+
[source,java]
153+
----
154+
@HttpServiceFallback(value = DefaultFallbacks.class)
155+
@HttpServiceFallback(value = GroupAndServiceSpecificFallbacks.class, service = {BillingService.class, ShippingService.class}, group = "billing")
156+
public class MyFallbackConfig {
157+
...
158+
}
159+
----
160+
161+
This configuration results in:
162+
163+
- `DefaultFallbacks` used as a global fallback for all services not explicitly handled
164+
- `GroupAndServiceSpecificFallbacks` used only for `BillingService` and `ShippingService` within the `"billing"` group
165+
166+
[NOTE]
167+
====
168+
- The fallback class and its methods must be `public`
169+
- Fallback methods must not be annotated with `@HttpExchange` annotations
170+
====
171+
172+
=== How CircuitBreaker Adapters Work
173+
174+
The adapters wrap `@HttpExchange` method calls with CircuitBreaker logic.
175+
When a fallback is triggered, a proxy is created using the user-defined fallback class.
176+
The appropriate fallback method is selected by matching:
177+
178+
- A method with the same name and parameter types, or
179+
- A method with the same name and parameter types preceded by a `Throwable` argument (to access the cause of failure)
180+
181+
Given the following interface:
182+
183+
[source,java]
184+
----
185+
@HttpExchange("/test")
186+
public interface TestService {
187+
188+
@GetExchange("/{id}")
189+
Person test(@PathVariable UUID id);
190+
191+
@GetExchange
192+
String test();
193+
}
194+
----
195+
196+
A matching fallback class could be:
197+
198+
[source,java]
199+
----
200+
public class TestServiceFallback {
201+
202+
public Person test(UUID id);
203+
204+
public String test(Throwable cause);
205+
}
206+
----

docs/modules/ROOT/partials/_configprops.adoc

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,6 @@
33

44
|spring.cloud.compatibility-verifier.compatible-boot-versions | `+++4.0.x+++` | Default accepted versions for the Spring Boot dependency. You can set {@code x} for the patch version if you don't want to specify a concrete value. Example: {@code 3.5.x}
55
|spring.cloud.compatibility-verifier.enabled | `+++false+++` | Enables creation of Spring Cloud compatibility verification.
6-
|spring.cloud.config.allow-override | `+++true+++` | Flag to indicate that {@link #isOverrideSystemProperties() systemPropertiesOverride} can be used. Set to false to prevent users from changing the default accidentally. Default true.
7-
|spring.cloud.config.initialize-on-context-refresh | `+++false+++` | Flag to initialize bootstrap configuration on context refresh event. Default false.
8-
|spring.cloud.config.override-none | `+++false+++` | Flag to indicate that when {@link #setAllowOverride(boolean) allowOverride} is true, external properties should take lowest priority and should not override any existing property sources (including local config files). Default false. This will only have an effect when using config first bootstrap.
9-
|spring.cloud.config.override-system-properties | `+++true+++` | Flag to indicate that the external properties should override system properties. Default true.
10-
|spring.cloud.decrypt-environment-post-processor.enabled | `+++true+++` | Enable the DecryptEnvironmentPostProcessor.
116
|spring.cloud.discovery.client.composite-indicator.enabled | `+++true+++` | Enables discovery client composite health indicator.
127
|spring.cloud.discovery.client.health-indicator.enabled | `+++true+++` |
138
|spring.cloud.discovery.client.health-indicator.include-description | `+++false+++` |
@@ -73,11 +68,7 @@
7368
|spring.cloud.loadbalancer.subset.size | `+++100+++` | Max subset size of deterministic subsetting.
7469
|spring.cloud.loadbalancer.x-forwarded.enabled | `+++false+++` | To Enable X-Forwarded Headers.
7570
|spring.cloud.loadbalancer.zone | | Spring Cloud LoadBalancer zone.
76-
|spring.cloud.refresh.additional-property-sources-to-retain | | Additional property sources to retain during a refresh. Typically only system property sources are retained. This property allows property sources, such as property sources created by EnvironmentPostProcessors to be retained as well.
7771
|spring.cloud.refresh.enabled | `+++true+++` | Enables autoconfiguration for the refresh scope and associated features.
78-
|spring.cloud.refresh.extra-refreshable | `+++true+++` | Additional bean names or class names for beans to post process into refresh scope.
79-
|spring.cloud.refresh.never-refreshable | `+++true+++` | Comma separated list of bean names or class names for beans to never be refreshed or rebound.
80-
|spring.cloud.refresh.on-restart.enabled | `+++true+++` | Enable refreshing context on start.
8172
|spring.cloud.service-registry.auto-registration.enabled | `+++true+++` | Whether service auto-registration is enabled. Defaults to true.
8273
|spring.cloud.service-registry.auto-registration.fail-fast | `+++false+++` | Whether startup fails if there is no AutoServiceRegistration. Defaults to false.
8374
|spring.cloud.service-registry.auto-registration.register-management | `+++true+++` | Whether to register the management as a service. Defaults to true.

spring-cloud-commons/src/main/java/org/springframework/cloud/client/CommonsClientAutoConfiguration.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@
3131
import org.springframework.boot.health.contributor.HealthIndicator;
3232
import org.springframework.cloud.client.actuator.FeaturesEndpoint;
3333
import org.springframework.cloud.client.actuator.HasFeatures;
34+
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
35+
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
36+
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker;
37+
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory;
38+
import org.springframework.cloud.client.circuitbreaker.httpservice.CircuitBreakerRestClientHttpServiceGroupConfigurer;
39+
import org.springframework.cloud.client.circuitbreaker.httpservice.CircuitBreakerWebClientHttpServiceGroupConfigurer;
3440
import org.springframework.cloud.client.discovery.DiscoveryClient;
3541
import org.springframework.cloud.client.discovery.health.DiscoveryClientHealthIndicator;
3642
import org.springframework.cloud.client.discovery.health.DiscoveryClientHealthIndicatorProperties;
@@ -39,6 +45,8 @@
3945
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
4046
import org.springframework.context.annotation.Bean;
4147
import org.springframework.context.annotation.Configuration;
48+
import org.springframework.web.client.support.RestClientHttpServiceGroupConfigurer;
49+
import org.springframework.web.reactive.function.client.support.WebClientHttpServiceGroupConfigurer;
4250

4351
/**
4452
* {@link EnableAutoConfiguration Auto-configuration} for Spring Cloud Commons Client.
@@ -51,6 +59,39 @@
5159
@Configuration(proxyBeanMethods = false)
5260
public class CommonsClientAutoConfiguration {
5361

62+
@ConditionalOnClass({ CircuitBreaker.class, RestClientHttpServiceGroupConfigurer.class })
63+
@ConditionalOnBean(CircuitBreakerFactory.class)
64+
@ConditionalOnProperty(value = "spring.cloud.circuitbreaker.http-services.enabled", havingValue = "true",
65+
matchIfMissing = true)
66+
@Configuration(proxyBeanMethods = false)
67+
protected static class CircuitBreakerInterfaceClientsAutoConfiguration {
68+
69+
@Bean
70+
public CircuitBreakerRestClientHttpServiceGroupConfigurer circuitBreakerRestClientConfigurer(
71+
CircuitBreakerFactory<?, ?> circuitBreakerFactory) {
72+
return new CircuitBreakerRestClientHttpServiceGroupConfigurer(circuitBreakerFactory);
73+
}
74+
75+
}
76+
77+
@ConditionalOnClass({ CircuitBreaker.class, ReactiveCircuitBreaker.class,
78+
WebClientHttpServiceGroupConfigurer.class })
79+
@ConditionalOnBean({ CircuitBreakerFactory.class, ReactiveCircuitBreakerFactory.class })
80+
@ConditionalOnProperty(value = "spring.cloud.circuitbreaker.reactive-http-services.enabled", havingValue = "true",
81+
matchIfMissing = true)
82+
@Configuration(proxyBeanMethods = false)
83+
protected static class ReactiveCircuitBreakerInterfaceClientsAutoConfiguration {
84+
85+
@Bean
86+
public CircuitBreakerWebClientHttpServiceGroupConfigurer circuitBreakerWebClientConfigurer(
87+
ReactiveCircuitBreakerFactory<?, ?> reactiveCircuitBreakerFactory,
88+
CircuitBreakerFactory<?, ?> circuitBreakerFactory) {
89+
return new CircuitBreakerWebClientHttpServiceGroupConfigurer(reactiveCircuitBreakerFactory,
90+
circuitBreakerFactory);
91+
}
92+
93+
}
94+
5495
@Configuration(proxyBeanMethods = false)
5596
@ConditionalOnClass(HealthIndicator.class)
5697
@EnableConfigurationProperties(DiscoveryClientHealthIndicatorProperties.class)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*
2+
* Copyright 2013-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.client.circuitbreaker.httpservice;
18+
19+
import java.util.Map;
20+
import java.util.function.Function;
21+
22+
import org.apache.commons.logging.Log;
23+
import org.apache.commons.logging.LogFactory;
24+
import org.jspecify.annotations.Nullable;
25+
26+
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
27+
import org.springframework.core.ParameterizedTypeReference;
28+
import org.springframework.http.HttpHeaders;
29+
import org.springframework.http.ResponseEntity;
30+
import org.springframework.web.service.invoker.HttpExchangeAdapter;
31+
import org.springframework.web.service.invoker.HttpExchangeAdapterDecorator;
32+
import org.springframework.web.service.invoker.HttpRequestValues;
33+
34+
import static org.springframework.cloud.client.circuitbreaker.httpservice.CircuitBreakerConfigurerUtils.createProxies;
35+
import static org.springframework.cloud.client.circuitbreaker.httpservice.CircuitBreakerConfigurerUtils.getFallback;
36+
37+
/**
38+
* Blocking implementation of {@link HttpExchangeAdapterDecorator} that wraps
39+
* {@code @HttpExchange}
40+
* <p>
41+
* In the event of a CircuitBreaker fallback, this class uses the user-provided fallback
42+
* class to create a proxy. The fallback method is selected by matching either:
43+
* <ul>
44+
* <li>A method with the same name and argument types as the original method, or</li>
45+
* <li>A method with the same name and the original arguments preceded by a
46+
* {@link Throwable}, allowing the user to access the cause of failure within the
47+
* fallback.</li>
48+
* </ul>
49+
* Once a matching method is found, it is invoked to provide the fallback behavior. Both
50+
* the fallback class and the fallback methods must be public.
51+
* </p>
52+
*
53+
* @author Olga Maciaszek-Sharma
54+
* @since 5.0.0
55+
* @see HttpServiceFallback
56+
*/
57+
public class CircuitBreakerAdapterDecorator extends HttpExchangeAdapterDecorator {
58+
59+
private static final Log LOG = LogFactory.getLog(CircuitBreakerAdapterDecorator.class);
60+
61+
private final CircuitBreaker circuitBreaker;
62+
63+
private final Map<String, Class<?>> fallbackClasses;
64+
65+
private volatile Map<String, Object> fallbackProxies;
66+
67+
public CircuitBreakerAdapterDecorator(HttpExchangeAdapter delegate, CircuitBreaker circuitBreaker,
68+
Map<String, Class<?>> fallbackClasses) {
69+
super(delegate);
70+
this.circuitBreaker = circuitBreaker;
71+
this.fallbackClasses = fallbackClasses;
72+
}
73+
74+
@Override
75+
public void exchange(HttpRequestValues requestValues) {
76+
circuitBreaker.run(() -> {
77+
super.exchange(requestValues);
78+
return null;
79+
}, createFallbackHandler(requestValues));
80+
}
81+
82+
@Override
83+
public HttpHeaders exchangeForHeaders(HttpRequestValues values) {
84+
Object result = circuitBreaker.run(() -> super.exchangeForHeaders(values), createFallbackHandler(values));
85+
return castIfPossible(result);
86+
}
87+
88+
@Override
89+
public <T> @Nullable T exchangeForBody(HttpRequestValues values, ParameterizedTypeReference<T> bodyType) {
90+
Object result = circuitBreaker.run(() -> super.exchangeForBody(values, bodyType),
91+
createFallbackHandler(values));
92+
return castIfPossible(result);
93+
}
94+
95+
@Override
96+
public ResponseEntity<Void> exchangeForBodilessEntity(HttpRequestValues values) {
97+
Object result = circuitBreaker.run(() -> super.exchangeForBodilessEntity(values),
98+
createFallbackHandler(values));
99+
return castIfPossible(result);
100+
}
101+
102+
@Override
103+
public <T> ResponseEntity<T> exchangeForEntity(HttpRequestValues values, ParameterizedTypeReference<T> bodyType) {
104+
Object result = circuitBreaker.run(() -> super.exchangeForEntity(values, bodyType),
105+
createFallbackHandler(values));
106+
return castIfPossible(result);
107+
}
108+
109+
// Visible for tests
110+
CircuitBreaker getCircuitBreaker() {
111+
return circuitBreaker;
112+
}
113+
114+
// Visible for tests
115+
Map<String, Class<?>> getFallbackClasses() {
116+
return fallbackClasses;
117+
}
118+
119+
@SuppressWarnings("unchecked")
120+
private <T> T castIfPossible(Object result) {
121+
try {
122+
return (T) result;
123+
}
124+
catch (ClassCastException exception) {
125+
if (LOG.isErrorEnabled()) {
126+
LOG.error("Failed to cast object of type " + result.getClass() + " to expected type.");
127+
}
128+
throw exception;
129+
}
130+
}
131+
132+
// Visible for tests
133+
Function<Throwable, Object> createFallbackHandler(HttpRequestValues requestValues) {
134+
return throwable -> getFallback(requestValues, throwable, getFallbackProxies(), fallbackClasses);
135+
}
136+
137+
private Map<String, Object> getFallbackProxies() {
138+
if (fallbackProxies == null) {
139+
synchronized (this) {
140+
if (fallbackProxies == null) {
141+
fallbackProxies = createProxies(fallbackClasses);
142+
}
143+
}
144+
}
145+
return fallbackProxies;
146+
}
147+
148+
}

0 commit comments

Comments
 (0)