Init draft.

Signed-off-by: Olga Maciaszek-Sharma <olga.maciaszek-sharma@broadcom.com>
This commit is contained in:
Olga Maciaszek-Sharma
2025-06-16 13:55:44 +02:00
parent 0df8c22502
commit 053deb5210
5 changed files with 310 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
package org.springframework.cloud.client;
/**
* @author Olga Maciaszek-Sharma
*/
public abstract class AbstractCloudHttpClientServiceProperties {
private String fallbackClass;
public String getFallbackClass() {
return fallbackClass;
}
public void setFallbackClass(String fallbackClass) {
this.fallbackClass = fallbackClass;
}
}

View File

@@ -0,0 +1,30 @@
package org.springframework.cloud.client;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author Olga Maciaszek-Sharma
*/
@ConfigurationProperties("spring.cloud.http.client.service")
public class CloudHttpClientServiceProperties extends AbstractCloudHttpClientServiceProperties {
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 {
}
}

View File

@@ -0,0 +1,24 @@
package org.springframework.cloud.client.circuitbreaker;
import java.lang.reflect.Method;
import org.jspecify.annotations.Nullable;
import org.springframework.web.service.invoker.HttpRequestValues;
/**
* @author Olga Maciaszek-Sharma
*/
public class CircuitBreakerRequestValueProcessor implements HttpRequestValues.Processor {
public static final String METHOD_ATTRIBUTE_NAME = "spring.cloud.method.name";
public static final String PARAMETER_TYPES_ATTRIBUTE_NAME = "spring.cloud.method.parameter-types";
public static final String ARGUMENTS_ATTRIBUTE_NAME = "spring.cloud.method.arguments";
@Override
public void process(Method method, @Nullable Object[] arguments, HttpRequestValues.Builder builder) {
builder.addAttribute(METHOD_ATTRIBUTE_NAME, method.getName());
builder.addAttribute(PARAMETER_TYPES_ATTRIBUTE_NAME, method.getParameterTypes());
builder.addAttribute(ARGUMENTS_ATTRIBUTE_NAME, arguments);
}
}

View File

@@ -0,0 +1,186 @@
package org.springframework.cloud.client.circuitbreaker;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.StreamingHttpOutputMessage;
import org.springframework.util.Assert;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.RestClientAdapter;
import org.springframework.web.service.invoker.HttpExchangeAdapter;
import org.springframework.web.service.invoker.HttpRequestValues;
import org.springframework.web.util.UriBuilderFactory;
/**
* @author Olga Maciaszek-Sharma
* @author Rossen Stoyanchev
*/
public class CircuitBreakerRestClientAdapter implements HttpExchangeAdapter {
// FIXME: get fallbacks
private final RestClient restClient;
private final CircuitBreaker circuitBreaker;
private final Class<?> fallbacks;
private CircuitBreakerRestClientAdapter(RestClient restClient, CircuitBreaker circuitBreaker,
// TODO: generics
Class<?> fallbacks) {
this.restClient = restClient;
this.circuitBreaker = circuitBreaker;
this.fallbacks = fallbacks;
}
@Override
public boolean supportsRequestAttributes() {
return true;
}
@Override
public void exchange(HttpRequestValues requestValues) {
Map<String, Object> attributes = requestValues.getAttributes();
String methodName = String.valueOf(attributes
.get(CircuitBreakerRequestValueProcessor.METHOD_ATTRIBUTE_NAME));
Class<?>[] parameterTypes = (Class<?>[]) attributes
.get(CircuitBreakerRequestValueProcessor.PARAMETER_TYPES_ATTRIBUTE_NAME);
Method method;
try {
method = fallbacks.getMethod(methodName, parameterTypes);
method.setAccessible(true);
}
catch (NoSuchMethodException e) {
// TODO
throw new RuntimeException(e);
}
circuitBreaker.run(() -> newRequest(requestValues).retrieve().toBodilessEntity(),
throwable -> {
try {
return method.invoke(this,
attributes.get(CircuitBreakerRequestValueProcessor.ARGUMENTS_ATTRIBUTE_NAME));
}
catch (IllegalAccessException | InvocationTargetException e) {
// TODO
throw new RuntimeException(e);
}
});
}
@Override
public HttpHeaders exchangeForHeaders(HttpRequestValues values) {
return circuitBreaker.run(() -> newRequest(values).retrieve().toBodilessEntity()
.getHeaders());
}
@SuppressWarnings("unchecked")
@Override
public <T> @Nullable T exchangeForBody(HttpRequestValues values, ParameterizedTypeReference<T> bodyType) {
return circuitBreaker.run(() -> {
if (bodyType.getType().equals(InputStream.class)) {
return (T) newRequest(values).exchange((request, response) -> response.getBody(), false);
}
return newRequest(values).retrieve().body(bodyType);
});
}
@Override
public ResponseEntity<Void> exchangeForBodilessEntity(HttpRequestValues values) {
return circuitBreaker.run(() -> newRequest(values).retrieve().toBodilessEntity());
}
@SuppressWarnings("unchecked")
@Override
public <T> ResponseEntity<T> exchangeForEntity(HttpRequestValues values, ParameterizedTypeReference<T> bodyType) {
return circuitBreaker.run(() -> {
if (bodyType.getType().equals(InputStream.class)) {
return (ResponseEntity<T>) newRequest(values).exchangeForRequiredValue((request, response) ->
ResponseEntity.status(response.getStatusCode())
.headers(response.getHeaders())
.body(response.getBody()), false);
}
return newRequest(values).retrieve().toEntity(bodyType);
});
}
@SuppressWarnings("unchecked")
private <B> RestClient.RequestBodySpec newRequest(HttpRequestValues values) {
HttpMethod httpMethod = values.getHttpMethod();
Assert.notNull(httpMethod, "HttpMethod is required");
RestClient.RequestBodyUriSpec uriSpec = this.restClient.method(httpMethod);
RestClient.RequestBodySpec bodySpec;
if (values.getUri() != null) {
bodySpec = uriSpec.uri(values.getUri());
}
else if (values.getUriTemplate() != null) {
UriBuilderFactory uriBuilderFactory = values.getUriBuilderFactory();
if (uriBuilderFactory != null) {
URI uri = uriBuilderFactory.expand(values.getUriTemplate(), values.getUriVariables());
bodySpec = uriSpec.uri(uri);
}
else {
bodySpec = uriSpec.uri(values.getUriTemplate(), values.getUriVariables());
}
}
else {
throw new IllegalStateException("Neither full URL nor URI template");
}
bodySpec.headers(headers -> headers.putAll(values.getHeaders()));
if (!values.getCookies().isEmpty()) {
List<String> cookies = new ArrayList<>();
values.getCookies()
.forEach((name, cookieValues) -> cookieValues.forEach(value -> {
HttpCookie cookie = new HttpCookie(name, value);
cookies.add(cookie.toString());
}));
bodySpec.header(HttpHeaders.COOKIE, String.join("; ", cookies));
}
if (values.getApiVersion() != null) {
bodySpec.apiVersion(values.getApiVersion());
}
bodySpec.attributes(attributes -> attributes.putAll(values.getAttributes()));
B body = (B) values.getBodyValue();
if (body != null) {
if (body instanceof StreamingHttpOutputMessage.Body streamingBody) {
bodySpec.body(streamingBody);
}
else if (values.getBodyValueType() != null) {
bodySpec.body(body, (ParameterizedTypeReference<? super B>) values.getBodyValueType());
}
else {
bodySpec.body(body);
}
}
return bodySpec;
}
/**
* Create a {@link RestClientAdapter} for the given {@link RestClient}.
*/
public static CircuitBreakerRestClientAdapter create(RestClient restClient, CircuitBreaker circuitBreaker,
Class<?> fallbacks) {
return new CircuitBreakerRestClientAdapter(restClient, circuitBreaker, fallbacks);
}
}

View File

@@ -0,0 +1,53 @@
package org.springframework.cloud.client.circuitbreaker;
import org.springframework.cloud.client.CloudHttpClientServiceProperties;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.RestClientHttpServiceGroupConfigurer;
/**
* @author Olga Maciaszek-Sharma
*/
public class CircuitBreakerRestClientHttpServiceGroupConfigurer implements RestClientHttpServiceGroupConfigurer {
// Make sure Boot's configurers run before
private static final int ORDER = 11;
private final CloudHttpClientServiceProperties clientServiceProperties;
public CircuitBreakerRestClientHttpServiceGroupConfigurer(CloudHttpClientServiceProperties clientServiceProperties) {
this.clientServiceProperties = clientServiceProperties;
}
@Override
public void configureGroups(Groups<RestClient.Builder> groups) {
groups.forEachGroup((group, clientBuilder, factoryBuilder) -> {
String groupName = group.name();
CloudHttpClientServiceProperties.Group groupProperties = clientServiceProperties.getGroup()
.get(groupName);
String fallbackClass = groupProperties == null ? null : groupProperties.getFallbackClass();
factoryBuilder.httpRequestValuesProcessor(new CircuitBreakerRequestValueProcessor());
Class<?> fallbacks = null;
try {
fallbacks = Class.forName(fallbackClass);
}
catch (ClassNotFoundException e) {
// TODO
throw new RuntimeException(e);
}
// TODO: change to decorator
factoryBuilder.exchangeAdapter(CircuitBreakerRestClientAdapter.create(RestClient.builder()
.build(), buildCircuitBreaker(), fallbacks));
});
}
private CircuitBreaker buildCircuitBreaker() {
return null;
}
@Override
public int getOrder() {
return ORDER;
}
}