Skip to content

Add interface clients support for circuit breaker #1532

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
053deb5
Init draft.
OlgaMaciaszek Jun 16, 2025
8606c70
Merge remote-tracking branch 'origin/main' into add-interface-clients…
OlgaMaciaszek Jun 18, 2025
6dfd121
Handle missing fallbacks. Fix configuration.
OlgaMaciaszek Jun 18, 2025
5c1e91f
Create proxy to invoke fallback method.
OlgaMaciaszek Jun 23, 2025
12352c3
Handle proxy target instantiation exceptions.
OlgaMaciaszek Jun 23, 2025
1d9b8a6
Add decorator.
OlgaMaciaszek Jul 1, 2025
034aae7
Switch to using decorator.
OlgaMaciaszek Jul 1, 2025
397ff05
Refactor CircuitBreakerRestClientAdapterDecorator.
OlgaMaciaszek Jul 1, 2025
de9b5ce
Refactor CircuitBreakerRestClientAdapterDecorator and CircuitBreakerR…
OlgaMaciaszek Jul 2, 2025
4c3dac6
Handle empty or null fallbackClassName. Add tests.
OlgaMaciaszek Jul 3, 2025
a77fa3e
Repackage. Add decorator tests.
OlgaMaciaszek Jul 3, 2025
19c065b
Merge remote-tracking branch 'origin/main' into add-interface-clients…
OlgaMaciaszek Jul 3, 2025
d449a70
Add more tests. Refactor.
OlgaMaciaszek Jul 7, 2025
fc81ce7
Draft reactive implementation.
OlgaMaciaszek Jul 8, 2025
34d34fb
Add tests for reactive implementation.
OlgaMaciaszek Jul 9, 2025
200dc59
Fix checkstyle. Adjust to changes in FW. Match fallback method by ret…
OlgaMaciaszek Jul 9, 2025
bd3dbaa
Fix configuration.
OlgaMaciaszek Jul 15, 2025
6dc3bb2
Fix reactive implementation and add more tests.
OlgaMaciaszek Jul 15, 2025
3223d8c
Refactor.
OlgaMaciaszek Jul 16, 2025
f7d0645
Add javadocs.
OlgaMaciaszek Jul 16, 2025
6a290b6
Add property flag to autoconfiguration.
OlgaMaciaszek Jul 17, 2025
d8d6636
Reword javadoc.
OlgaMaciaszek Jul 17, 2025
15dfe57
Add docs.
OlgaMaciaszek Jul 17, 2025
ac02fef
Allow using multiple fallback classes.
OlgaMaciaszek Jul 17, 2025
1fe805b
Fix the logic for resolving fallback from multiple fallback classes.
OlgaMaciaszek Jul 18, 2025
9dd9b1b
Add javadoc and regenerate configprops.
OlgaMaciaszek Jul 18, 2025
dcc8d8a
Allow defining default fallback for all groups. Add more tests.
OlgaMaciaszek Jul 18, 2025
a736cdd
Use String map keys. Handle default settings for all groups. Refactor…
OlgaMaciaszek Jul 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 107 additions & 5 deletions docs/modules/ROOT/pages/spring-cloud-circuitbreaker.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ Spring Cloud supports the following circuit-breaker implementations:
[[core-concepts]]
== Core Concepts

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.
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.
The following example shows a simple example of how to use this API:

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

For detailed information on how to customize a given implementation see
the following documentation:
For detailed information on how to customize a given implementation see the following documentation:

* link:../../../../spring-cloud-circuitbreaker/reference/spring-cloud-circuitbreaker-resilience4j.html[Resilience4J]
* 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]
* link:../../../../../spring-cloud-circuitbreaker/reference/spring-cloud-circuitbreaker-spring-retry.html[Spring Retry]

Some `CircuitBreaker` implementations such as `Resilience4JCircuitBreaker` call `customize` method every time `CircuitBreaker#run` is called.
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,
for example, in case of https://resilience4j.readme.io/docs/circuitbreaker#section-consume-emitted-circuitbreakerevents[consuming Resilience4j's events].
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, for example, in case of https://resilience4j.readme.io/docs/circuitbreaker#section-consume-emitted-circuitbreakerevents[consuming Resilience4j's events].

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

Expand All @@ -102,3 +103,104 @@ Customizer.once(circuitBreaker -> {
.onStateTransition(event -> log.info("{}: {}", event.getCircuitBreakerName(), event.getStateTransition()));
}, CircuitBreaker::getName)
----

[[interface-clients]]
== Spring Interface Clients Support

We provide support for Spring Interface Clients integration through the following configurers:

- `CircuitBreakerRestClientHttpServiceGroupConfigurer`
- `CircuitBreakerWebClientHttpServiceGroupConfigurer`

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].

When fallback class names are configured using:

- `spring.cloud.http.client.service.group.[groupName].fallback-class-names` (for a specific group), or
- `spring.cloud.http.client.service.fallback-class-names` (as a default for all groups),

a CircuitBreaker adapter is added to the respective group:
- `CircuitBreakerAdapterDecorator` is used with `RestClient`
- `ReactiveCircuitBreakerAdapterDecorator` is used with `WebClient`

=== How CircuitBreaker Adapters Work

The adapters wrap `@HttpExchange` method calls with CircuitBreaker logic. When a fallback is triggered, a proxy is created using the user-defined fallback class. The appropriate fallback method is selected by matching:

- A method with the same name and parameter types, or
- A method with the same name and parameter types preceded by a `Throwable` argument (to access the cause of failure)

Given the following interface:

[source,java]
----
@HttpExchange("/test")
public interface TestService {

@GetExchange("/{id}")
Person test(@PathVariable UUID id);

@GetExchange
String test();
}
----

A matching fallback class could be:

[source,java]
----
public class TestServiceFallback {

public Person test(UUID id);

public String test(Throwable cause);
}
----

[NOTE]
====
Both the fallback class and its methods must be `public`.
====

[TIP]
====
Fallback methods must *not* include `@HttpExchange` or any related annotations.
====

=== Configuring Fallbacks

Fallback class names are configured via properties as a map:

- *Key:* Fully qualified interface class name (or use `default` to specify a fallback class for all interfaces)
- *Value:* Fully qualified fallback class name

The following example applies to all client groups and the setup will result in using `com.example.http.verification.client.fallback.PersonServiceFallbacks` as fallback class for `PersonService` and `com.example.http.verification.client.fallback.DefaultFallbacks` for all other services for all the groups.

[source,yml]
----
spring:
cloud:
http:
client:
service:
fallback-class-names:
-default: com.example.http.verification.client.fallback.DefaultFallbacks
-com.example.http.verification.client.clients.PersonService: com.example.http.verification.client.fallback.PersonServiceFallbacks
----


The example below applies only to the `verification` group:

[source,yml]
----
spring:
cloud:
http:
client:
service:
group:
-verification:
fallback-class-names:
-default: com.example.http.verification.client.fallback.DefaultFallbacks
-com.example.http.verification.client.clients.PersonService: com.example.http.verification.client.fallback.PersonServiceFallbacks
----
2 changes: 2 additions & 0 deletions docs/modules/ROOT/partials/_configprops.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
|spring.cloud.discovery.client.simple.order | |
|spring.cloud.discovery.enabled | `+++true+++` | Enables discovery client health indicators.
|spring.cloud.features.enabled | `+++true+++` | Enables the features endpoint.
|spring.cloud.http.client.service.fallback-class-names | | Name of the class that contains fallback methods to be called by {@link CircuitBreakerAdapterDecorator} or {@link ReactiveCircuitBreakerAdapterDecorator} in case a fallback is triggered. <p> Both the fallback class and the fallback methods must be public. </p>
|spring.cloud.http.client.service.group | | Maps properties to groups by group name.
|spring.cloud.httpclientfactories.apache.enabled | `+++true+++` | Enables creation of Apache Http Client factory beans.
|spring.cloud.httpclientfactories.ok.enabled | `+++true+++` | Enables creation of OK Http Client factory beans.
|spring.cloud.hypermedia.refresh.fixed-delay | `+++5000+++` |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright 2013-2025 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.client;

import java.util.HashMap;
import java.util.Map;

import org.springframework.cloud.client.circuitbreaker.httpservice.CircuitBreakerAdapterDecorator;
import org.springframework.cloud.client.circuitbreaker.httpservice.ReactiveCircuitBreakerAdapterDecorator;

/**
* Spring Cloud-specific {@code HttpClientServiceProperties}.
*
* @author Olga Maciaszek-Sharma
* @since 5.0.0
*/
public abstract class AbstractCloudHttpClientServiceProperties {

/**
* Name of the class that contains fallback methods to be called by
* {@link CircuitBreakerAdapterDecorator} or
* {@link ReactiveCircuitBreakerAdapterDecorator} in case a fallback is triggered.
* <p>
* Both the fallback class and the fallback methods must be public.
* </p>
*/
private Map<String, String> fallbackClassNames = new HashMap<>();

public Map<String, String> getFallbackClassNames() {
return fallbackClassNames;
}

public void setFallbackClassNames(Map<String, String> fallbackClassNames) {
this.fallbackClassNames = fallbackClassNames;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2013-2025 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.client;

import java.util.LinkedHashMap;
import java.util.Map;

import org.springframework.boot.context.properties.ConfigurationProperties;

/**
* Group specific implementation of {@link AbstractCloudHttpClientServiceProperties}.
*
* @author Olga Maciaszek-Sharma
* @since 5.0.0
*/
@ConfigurationProperties("spring.cloud.http.client.service")
public class CloudHttpClientServiceProperties extends AbstractCloudHttpClientServiceProperties {

/**
* Maps properties to groups by group name.
*/
private Map<String, Group> group = new LinkedHashMap<>();

public Map<String, Group> getGroup() {
return this.group;
}

public void setGroup(Map<String, Group> group) {
this.group = group;
}

/**
* Properties for a single HTTP Service client group.
*/
public static class Group extends AbstractCloudHttpClientServiceProperties {

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@
import org.springframework.boot.health.contributor.HealthIndicator;
import org.springframework.cloud.client.actuator.FeaturesEndpoint;
import org.springframework.cloud.client.actuator.HasFeatures;
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker;
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory;
import org.springframework.cloud.client.circuitbreaker.httpservice.CircuitBreakerRestClientHttpServiceGroupConfigurer;
import org.springframework.cloud.client.circuitbreaker.httpservice.CircuitBreakerWebClientHttpServiceGroupConfigurer;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.discovery.health.DiscoveryClientHealthIndicator;
import org.springframework.cloud.client.discovery.health.DiscoveryClientHealthIndicatorProperties;
Expand All @@ -39,6 +45,8 @@
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.support.RestClientHttpServiceGroupConfigurer;
import org.springframework.web.reactive.function.client.support.WebClientHttpServiceGroupConfigurer;

/**
* {@link EnableAutoConfiguration Auto-configuration} for Spring Cloud Commons Client.
Expand All @@ -49,8 +57,43 @@
* @author Omer Naci Soydemir
*/
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(CloudHttpClientServiceProperties.class)
public class CommonsClientAutoConfiguration {

@ConditionalOnClass({ CircuitBreaker.class, RestClientHttpServiceGroupConfigurer.class })
@ConditionalOnBean(CircuitBreakerFactory.class)
@ConditionalOnProperty(value = "spring.cloud.circuitbreaker.interface-clients.enabled", havingValue = "true",
matchIfMissing = true)
@Configuration(proxyBeanMethods = false)
protected static class CircuitBreakerInterfaceClientsAutoConfiguration {

@Bean
public CircuitBreakerRestClientHttpServiceGroupConfigurer circuitBreakerRestClientConfigurer(
CloudHttpClientServiceProperties properties, CircuitBreakerFactory<?, ?> circuitBreakerFactory) {
return new CircuitBreakerRestClientHttpServiceGroupConfigurer(properties, circuitBreakerFactory);
}

}

@ConditionalOnClass({ CircuitBreaker.class, ReactiveCircuitBreaker.class,
WebClientHttpServiceGroupConfigurer.class })
@ConditionalOnBean({ CircuitBreakerFactory.class, ReactiveCircuitBreakerFactory.class })
@ConditionalOnProperty(value = "spring.cloud.circuitbreaker.reactive-interface-clients.enabled",
havingValue = "true", matchIfMissing = true)
@Configuration(proxyBeanMethods = false)
protected static class ReactiveCircuitBreakerInterfaceClientsAutoConfiguration {

@Bean
public CircuitBreakerWebClientHttpServiceGroupConfigurer circuitBreakerWebClientConfigurer(
CloudHttpClientServiceProperties properties,
ReactiveCircuitBreakerFactory<?, ?> reactiveCircuitBreakerFactory,
CircuitBreakerFactory<?, ?> circuitBreakerFactory) {
return new CircuitBreakerWebClientHttpServiceGroupConfigurer(properties, reactiveCircuitBreakerFactory,
circuitBreakerFactory);
}

}

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(HealthIndicator.class)
@EnableConfigurationProperties(DiscoveryClientHealthIndicatorProperties.class)
Expand Down
Loading
Loading