Compare commits

..
Author SHA1 Message Date
buildmaster bc7d39fa04 Update SNAPSHOT to 3.1.3 2022-05-26 20:27:01 +00:00
Spencer Gibb 4cba1de153 don't upload docs to maven central 2022-05-26 12:08:43 -04:00
buildmaster 1bed516e03 Bumping versions 2022-04-28 10:56:37 +00:00
buildmaster a142f6f2c0 Bumping versions to 3.1.3-SNAPSHOT after release 2022-04-27 10:53:18 +00:00
buildmaster 434ce22328 Going back to snapshots 2022-04-27 10:53:18 +00:00
buildmaster 9f160e7688 Update SNAPSHOT to 3.1.2 2022-04-27 10:51:06 +00:00
Olga Maciaszek-Sharma a3f270d3b4 Revert "Update SNAPSHOT to 3.1.2"
This reverts commit 86a2f28a9e.
2022-04-27 11:09:37 +02:00
Olga Maciaszek-Sharma 1ff643f4c1 Revert "Going back to snapshots"
This reverts commit 4cd0c363be.
2022-04-27 11:09:33 +02:00
Olga Maciaszek-Sharma 6704982bcd Revert "Bumping versions to 3.1.3-SNAPSHOT after release"
This reverts commit 909da0afdf.
2022-04-27 11:09:30 +02:00
buildmaster 909da0afdf Bumping versions to 3.1.3-SNAPSHOT after release 2022-04-26 16:02:51 +00:00
buildmaster 4cd0c363be Going back to snapshots 2022-04-26 16:02:51 +00:00
buildmaster 86a2f28a9e Update SNAPSHOT to 3.1.2 2022-04-26 16:00:40 +00:00
Olga Maciaszek-Sharma 573d933b3d Adjust to changes in commons. 2022-03-29 17:04:32 +02:00
Olga Maciaszek-Sharma d76869f214 Reformat. 2022-03-16 13:56:57 +01:00
Bhavya Agrawal 12efc629a3 [CORRECTION] Add Test Support for @RequestMapping NoPath, OnlySlashPath, MissingSlashLeadingPath. (#692) 2022-03-16 13:53:35 +01:00
buildmaster 965c168c2c Bumping versions 2022-03-11 10:57:35 +00:00
Ryan Baxter f2386e365d Provide an alternate naming convention for CB ids to allow for configuration via configuration properties (#687)
Configuration properties cannot contain characters like hash, parens, or commas
2022-03-10 19:36:51 -05:00
Olga Maciaszek-Sharma 765431dce1 Fixes gh-680. 2022-02-25 13:14:16 +01:00
Olga Maciaszek-Sharma 75fc0f6b2e Unwrap cb exception (#683) 2022-02-24 12:35:02 +01:00
buildmaster f662c0547d Bumping versions 2022-02-24 10:53:13 +00:00
buildmaster aa4b8a280d Bumping versions to 3.1.2-SNAPSHOT after release 2022-02-17 16:55:45 +00:00
buildmaster 973c048be5 Going back to snapshots 2022-02-17 16:55:45 +00:00
buildmaster 5a19a0a128 Update SNAPSHOT to 3.1.1 2022-02-17 16:53:23 +00:00
Olga Maciaszek-Sharma e99dac2af1 Fix builder customisation for load-balanced scenarios. 2022-02-14 13:13:17 +01:00
Felix Dittrich 83b6f043dd Move Call of applyBuildCustomizers at end of getTarget Method (#674) 2022-01-28 10:42:00 +01:00
Olga MaciaszekSharma 813ff7c8b0 Resolve SpEL null in @FeignClient fields. Fixes gh-656. 2022-01-17 12:41:20 +01:00
Olga MaciaszekSharma b8519c01a0 Refactor property naming. 2022-01-17 10:11:21 +01:00
Olga Maciaszek-Sharma d626518dc5 Configure readtimeout for okhttp (#669) 2022-01-14 20:02:06 +01:00
26 changed files with 474 additions and 50 deletions
+2 -1
View File
@@ -40,7 +40,8 @@ and binding to the Spring Environment and other Spring programming model idioms.
== Building
:jdkversion: 1.8
:jdkversion: 17
=== Basic Compile and Test
+3 -1
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-openfeign</artifactId>
<version>3.1.1-SNAPSHOT</version>
<version>3.1.3</version>
</parent>
<artifactId>spring-cloud-openfeign-docs</artifactId>
<packaging>jar</packaging>
@@ -17,6 +17,8 @@
<main.basedir>${basedir}/..</main.basedir>
<configprops.inclusionPattern>feign.*</configprops.inclusionPattern>
<upload-docs-zip.phase>deploy</upload-docs-zip.phase>
<!-- Don't upload docs jar to central / repo.spring.io -->
<maven-deploy-plugin-default.phase>none</maven-deploy-plugin-default.phase>
</properties>
<dependencies>
<dependency>
+2 -1
View File
@@ -2,6 +2,7 @@
|Name | Default | Description
|feign.autoconfiguration.jackson.enabled | `false` | If true, PageJacksonModule and SortJacksonModule bean will be provided for Jackson page decoding.
|feign.circuitbreaker.alphanumeric-ids.enabled | `false` | If true, Circuit Breaker ids will only contain alphanumeric characters to allow for configuration via configuration properties.
|feign.circuitbreaker.enabled | `false` | If true, an OpenFeign client will be wrapped with a Spring Cloud CircuitBreaker circuit breaker.
|feign.circuitbreaker.group.enabled | `false` | If true, an OpenFeign client will be wrapped with a Spring Cloud CircuitBreaker circuit breaker with with group.
|feign.client.config | |
@@ -26,7 +27,7 @@
|feign.httpclient.hc5.socket-timeout-unit | | Default value for socket timeout unit.
|feign.httpclient.max-connections | `200` |
|feign.httpclient.max-connections-per-route | `50` |
|feign.httpclient.ok-http-client-properties.read-timeout | `60s` | {@link OkHttpClient} read timeout; defaults to 60 seconds.
|feign.httpclient.ok-http.read-timeout | `60s` | {@link OkHttpClient} read timeout; defaults to 60 seconds.
|feign.httpclient.time-to-live | `900` |
|feign.httpclient.time-to-live-unit | |
|feign.metrics.enabled | `true` | Enables metrics capability for Feign.
@@ -372,6 +372,47 @@ public class FooConfiguration {
To enable Spring Cloud CircuitBreaker group set the `feign.circuitbreaker.group.enabled` property to `true` (by default `false`).
[[spring-clou-feign-circuitbreaker-configurationproperties]]
=== Configuring CircuitBreakers With Configuration Properties
You can configure CircuitBreakers via configuration properties. To do set
`feign.circuitbreaker.alphanumeric-ids.enabled` to `true`. Since
you cannot use characters like `#`, `(`, `)` `,` in configuration property names we need to
change the naming convention for the ids of the circuit breakers generated by OpenFeign. The above
property will do this for you.
For example, if you had this Feign client
[source,java,indent=0]
----
@FeignClienturl = "http://localhost:8080")
public interface DemoClient {
@GetMapping("demo")
String getDemo();
}
----
You could configure it using configuration properties by doing the following
[source,yaml,indent=0]
----
feign:
circuitbreaker:
enabled: true
alphanumeric-ids:
enabled: true
resilience4j:
circuitbreaker:
instances:
DemoClientgetDemo:
minimumNumberOfCalls: 69
timelimiter:
instances:
DemoClientgetDemo:
timeoutDuration: 10s
----
[[spring-cloud-feign-circuitbreaker-fallback]]
=== Feign Spring Cloud CircuitBreaker Fallbacks
+3 -3
View File
@@ -4,14 +4,14 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-openfeign</artifactId>
<version>3.1.1-SNAPSHOT</version>
<version>3.1.3</version>
<packaging>pom</packaging>
<name>Spring Cloud OpenFeign</name>
<description>Spring Cloud OpenFeign</description>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-build</artifactId>
<version>3.1.1-SNAPSHOT</version>
<version>3.1.3</version>
<relativePath/>
</parent>
<scm>
@@ -26,7 +26,7 @@
<properties>
<main.basedir>${basedir}</main.basedir>
<jackson.version>2.11.3</jackson.version>
<spring-cloud-commons.version>3.1.1-SNAPSHOT</spring-cloud-commons.version>
<spring-cloud-commons.version>3.1.3</spring-cloud-commons.version>
<!-- Plugin versions -->
<maven-eclipse-plugin.version>2.10</maven-eclipse-plugin.version>
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-openfeign</artifactId>
<version>3.1.1-SNAPSHOT</version>
<version>3.1.3</version>
<relativePath>..</relativePath> <!-- lookup parent from repository -->
</parent>
<artifactId>spring-cloud-openfeign-core</artifactId>
@@ -166,10 +166,19 @@ public class FeignAutoConfiguration {
@Bean
@ConditionalOnMissingBean(CircuitBreakerNameResolver.class)
@ConditionalOnProperty(value = "feign.circuitbreaker.alphanumeric-ids.enabled", havingValue = "false",
matchIfMissing = true)
public CircuitBreakerNameResolver circuitBreakerNameResolver() {
return new DefaultCircuitBreakerNameResolver();
}
@Bean
@ConditionalOnMissingBean(CircuitBreakerNameResolver.class)
@ConditionalOnProperty(value = "feign.circuitbreaker.alphanumeric-ids.enabled", havingValue = "true")
public CircuitBreakerNameResolver alphanumericCircuitBreakerNameResolver() {
return new AlphanumericCircuitBreakerNameResolver();
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean(CircuitBreakerFactory.class)
@@ -189,6 +198,15 @@ public class FeignAutoConfiguration {
}
static class AlphanumericCircuitBreakerNameResolver extends DefaultCircuitBreakerNameResolver {
@Override
public String resolveCircuitBreakerName(String feignClientName, Target<?> target, Method method) {
return super.resolveCircuitBreakerName(feignClientName, target, method).replaceAll("[^a-zA-Z0-9]", "");
}
}
}
// the following configuration is for alternate feign clients if
@@ -287,7 +305,7 @@ public class FeignAutoConfiguration {
boolean followRedirects = httpClientProperties.isFollowRedirects();
int connectTimeout = httpClientProperties.getConnectionTimeout();
boolean disableSslValidation = httpClientProperties.isDisableSslValidation();
Duration readTimeout = httpClientProperties.getOkHttpClientProperties().getReadTimeout();
Duration readTimeout = httpClientProperties.getOkHttp().getReadTimeout();
this.okHttpClient = httpClientFactory.createBuilder(disableSslValidation)
.connectTimeout(connectTimeout, TimeUnit.MILLISECONDS).followRedirects(followRedirects)
.readTimeout(readTimeout).connectionPool(connectionPool).build();
@@ -17,6 +17,7 @@
package org.springframework.cloud.openfeign;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.LinkedHashMap;
@@ -29,6 +30,7 @@ import feign.Target;
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.cloud.client.circuitbreaker.NoFallbackAvailableException;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
@@ -95,15 +97,29 @@ class FeignCircuitBreakerInvocationHandler implements InvocationHandler {
try {
return this.fallbackMethodMap.get(method).invoke(fallback, args);
}
catch (Exception e) {
throw new IllegalStateException(e);
catch (Exception exception) {
unwrapAndRethrow(exception);
}
return null;
};
return circuitBreaker.run(supplier, fallbackFunction);
}
return circuitBreaker.run(supplier);
}
private void unwrapAndRethrow(Exception exception) {
if (exception instanceof InvocationTargetException || exception instanceof NoFallbackAvailableException) {
Throwable underlyingException = exception.getCause();
if (underlyingException instanceof RuntimeException) {
throw (RuntimeException) underlyingException;
}
if (underlyingException != null) {
throw new IllegalStateException(underlyingException);
}
throw new IllegalStateException(exception);
}
}
private Supplier<Object> asSupplier(final Method method, final Object[] args) {
final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
return () -> {
@@ -69,6 +69,7 @@ import org.springframework.util.StringUtils;
* @author Sam Kruglov
* @author Jasbir Singh
* @author Hyeonmin Park
* @author Felix Dittrich
*/
public class FeignClientFactoryBean
implements FactoryBean<Object>, InitializingBean, ApplicationContextAware, BeanFactoryAware {
@@ -132,7 +133,6 @@ public class FeignClientFactoryBean
// @formatter:on
configureFeign(context, builder);
applyBuildCustomizers(context, builder);
return builder;
}
@@ -374,6 +374,7 @@ public class FeignClientFactoryBean
Client client = getOptional(context, Client.class);
if (client != null) {
builder.client(client);
applyBuildCustomizers(context, builder);
Targeter targeter = get(context, Targeter.class);
return targeter.target(this, builder, context, target);
}
@@ -443,11 +444,17 @@ public class FeignClientFactoryBean
}
builder.client(client);
}
applyBuildCustomizers(context, builder);
Targeter targeter = get(context, Targeter.class);
return (T) targeter.target(this, builder, context, new HardCodedTarget<>(type, name, url));
}
private String cleanPath() {
if (path == null) {
return "";
}
String path = this.path.trim();
if (StringUtils.hasLength(path)) {
if (!path.startsWith("/")) {
@@ -302,7 +302,11 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo
if (resolver == null) {
return resolved;
}
return String.valueOf(resolver.evaluate(resolved, new BeanExpressionContext(beanFactory, null)));
Object evaluateValue = resolver.evaluate(resolved, new BeanExpressionContext(beanFactory, null));
if (evaluateValue != null) {
return String.valueOf(evaluateValue);
}
return null;
}
return value;
}
@@ -60,7 +60,7 @@ public class OkHttpFeignConfiguration {
FeignHttpClientProperties httpClientProperties) {
boolean followRedirects = httpClientProperties.isFollowRedirects();
int connectTimeout = httpClientProperties.getConnectionTimeout();
Duration reaTimeout = httpClientProperties.getOkHttpClientProperties().getReadTimeout();
Duration reaTimeout = httpClientProperties.getOkHttp().getReadTimeout();
this.okHttpClient = httpClientFactory.createBuilder(httpClientProperties.isDisableSslValidation())
.connectTimeout(connectTimeout, TimeUnit.MILLISECONDS).followRedirects(followRedirects)
.readTimeout(reaTimeout).connectionPool(connectionPool).build();
@@ -110,8 +110,9 @@ public class FeignBlockingLoadBalancerClient implements Client {
}
String reconstructedUrl = loadBalancerClient.reconstructURI(instance, originalUri).toString();
Request newRequest = buildRequest(request, reconstructedUrl);
LoadBalancerProperties loadBalancerProperties = loadBalancerClientFactory.getProperties(serviceId);
return executeWithLoadBalancerLifecycleProcessing(delegate, options, newRequest, lbRequest, lbResponse,
supportedLifecycleProcessors);
supportedLifecycleProcessors, loadBalancerProperties.isUseRawStatusCodeInResponseData());
}
protected Request buildRequest(Request request, String reconstructedUrl) {
@@ -50,14 +50,15 @@ final class LoadBalancerUtils {
static Response executeWithLoadBalancerLifecycleProcessing(Client feignClient, Request.Options options,
Request feignRequest, org.springframework.cloud.client.loadbalancer.Request lbRequest,
org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse,
Set<LoadBalancerLifecycle> supportedLifecycleProcessors, boolean loadBalanced) throws IOException {
Set<LoadBalancerLifecycle> supportedLifecycleProcessors, boolean loadBalanced, boolean useRawStatusCodes)
throws IOException {
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStartRequest(lbRequest, lbResponse));
try {
Response response = feignClient.execute(feignRequest, options);
if (loadBalanced) {
supportedLifecycleProcessors.forEach(
lifecycle -> lifecycle.onComplete(new CompletionContext<>(CompletionContext.Status.SUCCESS,
lbRequest, lbResponse, buildResponseData(response))));
lbRequest, lbResponse, buildResponseData(response, useRawStatusCodes))));
}
return response;
}
@@ -70,9 +71,12 @@ final class LoadBalancerUtils {
}
}
static ResponseData buildResponseData(Response response) {
static ResponseData buildResponseData(Response response, boolean useRawStatusCodes) {
HttpHeaders responseHeaders = new HttpHeaders();
response.headers().forEach((key, value) -> responseHeaders.put(key, new ArrayList<>(value)));
if (useRawStatusCodes) {
return new ResponseData(responseHeaders, null, buildRequestData(response.request()), response.status());
}
return new ResponseData(HttpStatus.resolve(response.status()), responseHeaders, null,
buildRequestData(response.request()));
}
@@ -87,9 +91,9 @@ final class LoadBalancerUtils {
static Response executeWithLoadBalancerLifecycleProcessing(Client feignClient, Request.Options options,
Request feignRequest, org.springframework.cloud.client.loadbalancer.Request lbRequest,
org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse,
Set<LoadBalancerLifecycle> supportedLifecycleProcessors) throws IOException {
Set<LoadBalancerLifecycle> supportedLifecycleProcessors, boolean useRawStatusCodes) throws IOException {
return executeWithLoadBalancerLifecycleProcessing(feignClient, options, feignRequest, lbRequest, lbResponse,
supportedLifecycleProcessors, true);
supportedLifecycleProcessors, true, useRawStatusCodes);
}
}
@@ -163,9 +163,10 @@ public class RetryableFeignBlockingLoadBalancerClient implements Client {
}
org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse = new DefaultResponse(
retrievedServiceInstance);
LoadBalancerProperties loadBalancerProperties = loadBalancerClientFactory.getProperties(serviceId);
Response response = LoadBalancerUtils.executeWithLoadBalancerLifecycleProcessing(delegate, options,
feignRequest, lbRequest, lbResponse, supportedLifecycleProcessors,
retrievedServiceInstance != null);
feignRequest, lbRequest, lbResponse, supportedLifecycleProcessors, retrievedServiceInstance != null,
loadBalancerProperties.isUseRawStatusCodeInResponseData());
int responseStatus = response.status();
if (retryPolicy != null && retryPolicy.retryableStatusCode(responseStatus)) {
if (LOG.isDebugEnabled()) {
@@ -95,7 +95,7 @@ public class FeignHttpClientProperties {
/**
* Additional {@link OkHttpClient}-specific properties.
*/
private OkHttpClientProperties okHttpClientProperties = new OkHttpClientProperties();
private OkHttp okHttp = new OkHttp();
public int getConnectionTimerRepeat() {
return connectionTimerRepeat;
@@ -169,12 +169,12 @@ public class FeignHttpClientProperties {
this.hc5 = hc5;
}
public OkHttpClientProperties getOkHttpClientProperties() {
return okHttpClientProperties;
public OkHttp getOkHttp() {
return okHttp;
}
public void setOkHttpClientProperties(OkHttpClientProperties okHttpClientProperties) {
this.okHttpClientProperties = okHttpClientProperties;
public void setOkHttp(OkHttp okHttp) {
this.okHttp = okHttp;
}
public static class Hc5Properties {
@@ -292,7 +292,7 @@ public class FeignHttpClientProperties {
/**
* {@link OkHttpClient}-specific properties.
*/
public static class OkHttpClientProperties {
public static class OkHttp {
/**
* {@link OkHttpClient} read timeout; defaults to 60 seconds.
@@ -196,6 +196,21 @@ public class PageJacksonModule extends Module {
return delegate.isEmpty();
}
@Override
public int hashCode() {
return delegate.hashCode();
}
@Override
public boolean equals(Object obj) {
return delegate.equals(obj);
}
@Override
public String toString() {
return delegate.toString();
}
}
}
@@ -14,6 +14,12 @@
"description": "If true, an OpenFeign client will be wrapped with a Spring Cloud CircuitBreaker circuit breaker.",
"defaultValue": "false"
},
{
"name": "feign.circuitbreaker.alphanumeric-ids.enabled",
"type": "java.lang.Boolean",
"description": "If true, Circuit Breaker ids will only contain alphanumeric characters to allow for configuration via configuration properties.",
"defaultValue": "false"
},
{
"name": "feign.circuitbreaker.group.enabled",
"type": "java.lang.Boolean",
@@ -17,35 +17,56 @@
package org.springframework.cloud.openfeign;
import java.lang.reflect.Field;
import java.util.stream.Stream;
import feign.Client;
import feign.Feign;
import feign.Logger;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Primary;
import org.springframework.core.annotation.Order;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
/**
* @author Matt King
* @author Sam Kruglov
* @author Felix Dittrich
* @author Olga Maciaszek-Sharma
*/
class FeignBuilderCustomizerTests {
private static final Targeter targeterSpy = spy(DefaultTargeter.class);
private static final Client defaultClient = mock(Client.class);
@Test
void testBuilderCustomizer() {
ArgumentCaptor<Feign.Builder> feignBuilderCaptor = ArgumentCaptor.forClass(Feign.Builder.class);
doCallRealMethod().when(targeterSpy).target(any(), feignBuilderCaptor.capture(), any(), any());
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
FeignBuilderCustomizerTests.SampleConfiguration2.class);
FeignClientFactoryBean clientFactoryBean = context.getBean(FeignClientFactoryBean.class);
FeignContext feignContext = context.getBean(FeignContext.class);
clientFactoryBean.getTarget();
Feign.Builder builder = clientFactoryBean.feign(feignContext);
Assertions.assertNotNull(feignBuilderCaptor.getValue());
Feign.Builder builder = feignBuilderCaptor.getValue();
assertFeignBuilderField(builder, "logLevel", Logger.Level.HEADERS);
assertFeignBuilderField(builder, "decode404", true);
@@ -62,13 +83,17 @@ class FeignBuilderCustomizerTests {
@Test
void testBuildCustomizerOrdered() {
ArgumentCaptor<Feign.Builder> feignBuilderCaptor = ArgumentCaptor.forClass(Feign.Builder.class);
doCallRealMethod().when(targeterSpy).target(any(), feignBuilderCaptor.capture(), any(), any());
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
FeignBuilderCustomizerTests.SampleConfiguration3.class);
FeignClientFactoryBean clientFactoryBean = context.getBean(FeignClientFactoryBean.class);
FeignContext feignContext = context.getBean(FeignContext.class);
clientFactoryBean.getTarget();
Feign.Builder builder = clientFactoryBean.feign(feignContext);
Assertions.assertNotNull(feignBuilderCaptor.getValue());
Feign.Builder builder = feignBuilderCaptor.getValue();
assertFeignBuilderField(builder, "logLevel", Logger.Level.FULL);
assertFeignBuilderField(builder, "decode404", true);
@@ -77,15 +102,19 @@ class FeignBuilderCustomizerTests {
@Test
void testBuildCustomizerOrderedWithAdditional() {
ArgumentCaptor<Feign.Builder> feignBuilderCaptor = ArgumentCaptor.forClass(Feign.Builder.class);
doCallRealMethod().when(targeterSpy).target(any(), feignBuilderCaptor.capture(), any(), any());
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
FeignBuilderCustomizerTests.SampleConfiguration3.class);
FeignClientFactoryBean clientFactoryBean = context.getBean(FeignClientFactoryBean.class);
clientFactoryBean.addCustomizer(builder -> builder.logLevel(Logger.Level.BASIC));
clientFactoryBean.addCustomizer(Feign.Builder::doNotCloseAfterDecode);
FeignContext feignContext = context.getBean(FeignContext.class);
clientFactoryBean.getTarget();
Feign.Builder builder = clientFactoryBean.feign(feignContext);
Assertions.assertNotNull(feignBuilderCaptor.getValue());
Feign.Builder builder = feignBuilderCaptor.getValue();
assertFeignBuilderField(builder, "logLevel", Logger.Level.BASIC);
assertFeignBuilderField(builder, "decode404", true);
assertFeignBuilderField(builder, "closeAfterDecode", false);
@@ -93,16 +122,41 @@ class FeignBuilderCustomizerTests {
context.close();
}
private static FeignClientFactoryBean defaultFeignClientFactoryBean() {
@ParameterizedTest(name = "should use custom HttpClient with config: {0}")
@MethodSource("testConfiguration")
void testBuildCustomizerWithCustomHttpClient(Class configClass) {
ArgumentCaptor<Feign.Builder> feignBuilderCaptor = ArgumentCaptor.forClass(Feign.Builder.class);
doCallRealMethod().when(targeterSpy).target(any(), feignBuilderCaptor.capture(), any(), any());
Client customClientMock = mock(Client.class);
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(configClass);
FeignClientFactoryBean clientFactoryBean = context.getBean(FeignClientFactoryBean.class);
clientFactoryBean.addCustomizer(builder -> builder.client(customClientMock));
clientFactoryBean.getTarget();
Assertions.assertNotNull(feignBuilderCaptor.getValue());
Feign.Builder builder = feignBuilderCaptor.getValue();
assertFeignBuilderField(builder, "client", customClientMock);
context.close();
}
private static FeignClientFactoryBean defaultFeignClientFactoryBean(String url) {
FeignClientFactoryBean feignClientFactoryBean = new FeignClientFactoryBean();
feignClientFactoryBean.setContextId("test");
feignClientFactoryBean.setName("test");
feignClientFactoryBean.setType(FeignClientFactoryTests.TestType.class);
feignClientFactoryBean.setPath("");
feignClientFactoryBean.setUrl("http://some.absolute.url");
if (url != null) {
feignClientFactoryBean.setUrl(url);
}
return feignClientFactoryBean;
}
private static Stream<Class> testConfiguration() {
return Stream.of(SampleConfiguration3.class, LoadBalancedSampleConfiguration.class);
}
@Configuration(proxyBeanMethods = false)
@Import(FeignClientsConfiguration.class)
protected static class SampleConfiguration2 {
@@ -129,7 +183,12 @@ class FeignBuilderCustomizerTests {
@Bean
FeignClientFactoryBean feignClientFactoryBean() {
return defaultFeignClientFactoryBean();
return defaultFeignClientFactoryBean("http://some.absolute.url");
}
@Bean
Targeter targeter() {
return targeterSpy;
}
}
@@ -167,7 +226,29 @@ class FeignBuilderCustomizerTests {
@Bean
FeignClientFactoryBean feignClientFactoryBean() {
return defaultFeignClientFactoryBean();
return defaultFeignClientFactoryBean("http://some.absolute.url");
}
@Bean
Targeter targeter() {
return targeterSpy;
}
@Bean
Client client() {
return defaultClient;
}
}
@Configuration(proxyBeanMethods = false)
@Import(SampleConfiguration3.class)
protected static class LoadBalancedSampleConfiguration {
@Primary
@Bean
FeignClientFactoryBean feignClientFactoryBean() {
return defaultFeignClientFactoryBean(null);
}
}
@@ -18,24 +18,30 @@ package org.springframework.cloud.openfeign;
import java.util.Collections;
import feign.Target;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.bind.annotation.GetMapping;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* @author Spencer Gibb
* @author Gang Li
* @author Michal Domagala
* @author Szymon Linowski
* @author Olga Maciaszek-Sharma
*/
class FeignClientsRegistrarTests {
@@ -97,14 +103,39 @@ class FeignClientsRegistrarTests {
@Test
void shouldPassSubLevelFeignClient() {
AnnotationConfigApplicationContext config = new AnnotationConfigApplicationContext();
((DefaultListableBeanFactory) config.getBeanFactory()).setAllowBeanDefinitionOverriding(false);
config.register(TopLevelSubLevelTestConfig.class);
assertThatCode(() -> config.refresh())
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
((DefaultListableBeanFactory) context.getBeanFactory()).setAllowBeanDefinitionOverriding(false);
context.register(TopLevelSubLevelTestConfig.class);
assertThatCode(context::refresh)
.as("Case https://github.com/spring-cloud/spring-cloud-openfeign/issues/331 should be solved")
.doesNotThrowAnyException();
}
@Test
@DisabledForJreRange(min = JRE.JAVA_16)
void shouldResolveNullUrl() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(NullUrlFeignClientTestConfig.class);
context.refresh();
Object feignClientBean = context.getBean(NullUrlFeignClient.class);
Object invocationHandlerLambda = ReflectionTestUtils.getField(feignClientBean, "h");
Target.HardCodedTarget<NullUrlFeignClient> target = (Target.HardCodedTarget<NullUrlFeignClient>) ReflectionTestUtils
.getField(invocationHandlerLambda, "arg$3");
assertThat(target.name()).isEqualTo("nullUrlFeignClient");
assertThat(target.url()).isEqualTo("http://nullUrlFeignClient");
}
@Test
void shouldResolveAndValidateNullName() {
assertThatIllegalStateException().isThrownBy(() -> {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(NullExpressionNameFeignClientTestConfig.class);
context.refresh();
});
}
@FeignClient(name = "fallbackTestClient", url = "http://localhost:8080/", fallback = FallbackClient.class)
protected interface FallbackClient {
@@ -122,6 +153,16 @@ class FeignClientsRegistrarTests {
}
@FeignClient(name = "nullUrlFeignClient", url = "${test.url:#{null}}", path = "${test.path:#{null}}")
protected interface NullUrlFeignClient {
}
@FeignClient(name = "${test.name:#{null}}")
protected interface NullExpressionNameFeignClient {
}
@Configuration(proxyBeanMethods = false)
@EnableAutoConfiguration
@EnableFeignClients(clients = { FeignClientsRegistrarTests.FallbackClient.class })
@@ -142,4 +183,18 @@ class FeignClientsRegistrarTests {
}
@Configuration(proxyBeanMethods = false)
@EnableAutoConfiguration
@EnableFeignClients(clients = NullUrlFeignClient.class)
protected static class NullUrlFeignClientTestConfig {
}
@Configuration(proxyBeanMethods = false)
@EnableAutoConfiguration
@EnableFeignClients(clients = NullExpressionNameFeignClient.class)
protected static class NullExpressionNameFeignClientTestConfig {
}
}
@@ -45,7 +45,7 @@ class FeignOkHttpConfigurationTests {
void setUp() {
this.context = new SpringApplicationBuilder()
.properties("debug=true", "feign.httpclient.disableSslValidation=true", "feign.okhttp.enabled=true",
"feign.httpclient.enabled=false", "feign.httpclient.okhttp-client-properties.read-timeout=9s")
"feign.httpclient.enabled=false", "feign.httpclient.okhttp.read-timeout=9s")
.web(WebApplicationType.NONE).sources(HttpClientConfiguration.class, FeignAutoConfiguration.class)
.run();
}
@@ -0,0 +1,78 @@
/*
* Copyright 2013-2022 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.circuitbreaker;
import feign.Target;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.openfeign.CircuitBreakerNameResolver;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* @author Ryan Baxter
*/
public class CircuitBreakerAutoConfigurationTests {
@SpringBootTest(classes = CircuitBreakerTests.Application.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
value = { "spring.application.name=springcircuittest", "spring.jmx.enabled=false",
"feign.circuitbreaker.enabled=true" })
@Nested
class DefaultNamingStrategy {
@Autowired
CircuitBreakerNameResolver nameResolver;
@Test
public void assertDefaultNamingStrategy() throws Exception {
Target target = mock(Target.class);
when(target.type()).thenReturn(CircuitBreakerTests.TestClientWithFactory.class);
assertThat(nameResolver.resolveCircuitBreakerName("foo", target,
CircuitBreakerTests.TestClientWithFactory.class.getMethod("getHello")))
.isEqualTo("TestClientWithFactory#getHello()");
}
}
@SpringBootTest(classes = CircuitBreakerTests.Application.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
value = { "spring.application.name=springcircuittest", "spring.jmx.enabled=false",
"feign.circuitbreaker.enabled=true", "feign.circuitbreaker.alphanumeric-ids.enabled=true" })
@Nested
class AlphanumericNamingStrategy {
@Autowired
CircuitBreakerNameResolver nameResolver;
@Test
public void assertAlphanumericNamingStrategy() throws Exception {
Target target = mock(Target.class);
when(target.type()).thenReturn(CircuitBreakerTests.TestClientWithFactory.class);
assertThat(nameResolver.resolveCircuitBreakerName("foo", target,
CircuitBreakerTests.TestClientWithFactory.class.getMethod("getHello")))
.isEqualTo("TestClientWithFactorygetHello");
}
}
}
@@ -16,6 +16,7 @@
package org.springframework.cloud.openfeign.circuitbreaker;
import java.io.IOException;
import java.util.function.Function;
import org.apache.commons.logging.Log;
@@ -47,6 +48,7 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* @author Spencer Gibb
@@ -62,6 +64,9 @@ class CircuitBreakerTests {
@Autowired
TestClient testClient;
@Autowired
ExceptionClient exceptionClient;
@Autowired
TestClientWithFactory testClientWithFactory;
@@ -108,6 +113,17 @@ class CircuitBreakerTests {
assertThat(testClientWithFactory.getException()).isEqualTo("Fixed response");
}
@Test
void testRuntimeExceptionUnwrapped() {
assertThatExceptionOfType(UnsupportedOperationException.class)
.isThrownBy(() -> exceptionClient.getRuntimeException());
}
@Test
void testCheckedExceptionWrapped() {
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> exceptionClient.getCheckedException());
}
@FeignClient(name = "test", url = "http://localhost:${server.port}/", fallback = Fallback.class)
protected interface TestClient {
@@ -119,6 +135,18 @@ class CircuitBreakerTests {
}
@FeignClient(name = "exceptionClient", url = "http://localhost:${server.port}/",
fallbackFactory = ExceptionThrowingFallbackFactory.class)
protected interface ExceptionClient {
@GetMapping("/runtimeException")
Hello getRuntimeException();
@GetMapping("/runtimeException")
Hello getCheckedException() throws IOException;
}
@Component
static class Fallback implements TestClient {
@@ -156,6 +184,25 @@ class CircuitBreakerTests {
}
static class ExceptionThrowingFallbackFactory implements FallbackFactory<ExceptionClient> {
@Override
public ExceptionClient create(Throwable cause) {
return new ExceptionClient() {
@Override
public Hello getRuntimeException() {
throw new UnsupportedOperationException("Not implemented!");
}
@Override
public Hello getCheckedException() throws IOException {
throw new IOException();
}
};
}
}
static class FallbackWithFactory implements TestClientWithFactory {
@Override
@@ -173,7 +220,7 @@ class CircuitBreakerTests {
@Configuration(proxyBeanMethods = false)
@EnableAutoConfiguration
@RestController
@EnableFeignClients(clients = { TestClient.class, TestClientWithFactory.class })
@EnableFeignClients(clients = { TestClient.class, TestClientWithFactory.class, ExceptionClient.class })
@Import(NoSecurityConfiguration.class)
protected static class Application implements TestClient {
@@ -225,6 +272,11 @@ class CircuitBreakerTests {
return new TestFallbackFactory();
}
@Bean
ExceptionThrowingFallbackFactory exceptionThrowingFallbackFactory() {
return new ExceptionThrowingFallbackFactory();
}
}
}
@@ -59,11 +59,11 @@ class FeignLoadBalancerAutoConfigurationTests {
@Test
void shouldInstantiateOkHttpFeignClientWhenEnabled() {
ConfigurableApplicationContext context = initContext("feign.httpclient.enabled=false",
"feign.okhttp.enabled=true", "spring.cloud.loadbalancer.retry.enabled=false",
"feign.httpclient.okhttp-client-properties.read-timeout=9s");
"feign.okhttp.enabled=true", "spring.cloud.loadbalancer.retry.enabled=false",
"feign.httpclient.okhttp.read-timeout=9s");
assertThatOneBeanPresent(context, BlockingLoadBalancerClient.class);
Map<String, FeignBlockingLoadBalancerClient> beans = context
.getBeansOfType(FeignBlockingLoadBalancerClient.class);
.getBeansOfType(FeignBlockingLoadBalancerClient.class);
assertThat(beans).as("Missing bean of type %s", OkHttpClient.class).hasSize(1);
Client client = beans.get("feignClient").getDelegate();
assertThat(client).isInstanceOf(OkHttpClient.class);
@@ -78,7 +78,9 @@ import static org.junit.jupiter.api.Assumptions.assumeTrue;
* @author Olga Maciaszek-Sharma
* @author Szymon Linowski
* @author Sam Kruglov
*/
* @author Bhavya Agrawal
**/
class SpringMvcContractTests {
private static final Class<?> EXECUTABLE_TYPE;
@@ -171,6 +173,39 @@ class SpringMvcContractTests {
assertThat(data.indexToName().get(0).iterator().next()).isEqualTo("id");
}
@Test
void testProcessAnnotations_SimpleNoPath() throws Exception {
Method method = TestTemplate_Simple.class.getDeclaredMethod("getTest");
MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
assertThat(data.template().url()).isEqualTo("/");
assertThat(data.template().method()).isEqualTo("GET");
assertThat(data.template().headers().get("Accept").iterator().next())
.isEqualTo(MediaType.APPLICATION_JSON_VALUE);
}
@Test
void testProcessAnnotations_SimplePathIsOnlyASlash() throws Exception {
Method method = TestTemplate_Simple.class.getDeclaredMethod("getSlashPath", String.class);
MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
assertThat(data.template().url()).isEqualTo("/?id=" + "{id}");
assertThat(data.template().method()).isEqualTo("GET");
assertThat(data.template().headers().get("Accept").iterator().next())
.isEqualTo(MediaType.APPLICATION_JSON_VALUE);
}
@Test
void testProcessAnnotations_MissingLeadingSlashInPath() throws Exception {
Method method = TestTemplate_Simple.class.getDeclaredMethod("getTestNoLeadingSlash", String.class);
MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
assertThat(data.template().url()).isEqualTo("/test?name=" + "{name}");
assertThat(data.template().method()).isEqualTo("GET");
assertThat(data.template().headers().get("Accept").iterator().next())
.isEqualTo(MediaType.APPLICATION_JSON_VALUE);
}
@Test
void testProcessAnnotations_SimpleGetMapping() throws Exception {
Method method = TestTemplate_Simple.class.getDeclaredMethod("getMappingTest", String.class);
@@ -616,6 +651,12 @@ class SpringMvcContractTests {
@PostMapping(produces = MediaType.APPLICATION_JSON_VALUE)
TestObject postMappingTest(@RequestBody TestObject object);
@GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<TestObject> getSlashPath(@RequestParam("id") String id);
@GetMapping(path = "test", produces = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<TestObject> getTestNoLeadingSlash(@RequestParam("name") String name);
}
@RequestMapping("/prepend/{classId}")
+2 -2
View File
@@ -6,11 +6,11 @@
<parent>
<artifactId>spring-cloud-dependencies-parent</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>3.1.0</version>
<version>3.1.3</version>
<relativePath/>
</parent>
<artifactId>spring-cloud-openfeign-dependencies</artifactId>
<version>3.1.1-SNAPSHOT</version>
<version>3.1.3</version>
<packaging>pom</packaging>
<name>spring-cloud-openfeign-dependencies</name>
<description>Spring Cloud OpenFeign Dependencies</description>
+1 -1
View File
@@ -5,7 +5,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-openfeign</artifactId>
<version>3.1.1-SNAPSHOT</version>
<version>3.1.3</version>
<relativePath>..</relativePath>
</parent>
<artifactId>spring-cloud-starter-openfeign</artifactId>