Init draft.
Signed-off-by: Olga Maciaszek-Sharma <olga.maciaszek-sharma@broadcom.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user