Compare commits

..
20 Commits
Author SHA1 Message Date
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
36 changed files with 428 additions and 55 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
strategy:
matrix:
java: ["17"]
java: ["8", "11", "16"]
steps:
- uses: actions/checkout@v2
+1 -1
View File
@@ -1,3 +1,3 @@
# Enable auto-env through the sdkman_auto_env config
# Add key=value pairs of SDKs to use below
java=17.0.1-tem
java=8.0.292.hs-adpt
+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
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-openfeign</artifactId>
<version>4.0.0-M1</version>
<version>3.1.2</version>
</parent>
<artifactId>spring-cloud-openfeign-docs</artifactId>
<packaging>jar</packaging>
+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 | |
@@ -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
+10 -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>4.0.0-M1</version>
<version>3.1.2</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>4.0.0-M1</version>
<version>3.1.2</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>4.0.0-M1</spring-cloud-commons.version>
<spring-cloud-commons.version>3.1.2</spring-cloud-commons.version>
<!-- Plugin versions -->
<maven-eclipse-plugin.version>2.10</maven-eclipse-plugin.version>
@@ -64,6 +64,13 @@
</additionalConfig>
</configuration>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-openfeign</artifactId>
<version>4.0.0-M1</version>
<version>3.1.2</version>
<relativePath>..</relativePath> <!-- lookup parent from repository -->
</parent>
<artifactId>spring-cloud-openfeign-core</artifactId>
@@ -25,6 +25,8 @@ import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import javax.annotation.PreDestroy;
import com.fasterxml.jackson.databind.Module;
import feign.Capability;
import feign.Client;
@@ -34,7 +36,6 @@ import feign.Target;
import feign.hc5.ApacheHttp5Client;
import feign.httpclient.ApacheHttpClient;
import feign.okhttp.OkHttpClient;
import jakarta.annotation.PreDestroy;
import okhttp3.ConnectionPool;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -165,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)
@@ -188,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
@@ -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,6 +444,9 @@ 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));
}
@@ -23,11 +23,11 @@ import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.concurrent.TimeUnit;
import javax.annotation.PreDestroy;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import jakarta.annotation.PreDestroy;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hc.client5.http.config.RequestConfig;
@@ -20,7 +20,8 @@ import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;
import jakarta.annotation.PreDestroy;
import javax.annotation.PreDestroy;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.client.config.RequestConfig;
@@ -19,7 +19,8 @@ package org.springframework.cloud.openfeign.clientconfig;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import jakarta.annotation.PreDestroy;
import javax.annotation.PreDestroy;
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
@@ -1,5 +1,5 @@
/*
* Copyright 2016-2022 the original author or authors.
* Copyright 2016-2021 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.
@@ -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()) {
@@ -1,5 +1,5 @@
/*
* Copyright 2015-2022 the original author or authors.
* Copyright 2015-2021 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2016-2022 the original author or authors.
* Copyright 2016-2021 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.
@@ -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);
}
}
@@ -32,6 +32,8 @@ import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.servlet.http.HttpServletRequest;
import feign.Capability;
import feign.Feign;
import feign.InvocationHandlerFactory;
@@ -45,7 +47,6 @@ import feign.codec.EncodeException;
import feign.codec.Encoder;
import feign.codec.ErrorDecoder;
import feign.micrometer.MicrometerCapability;
import jakarta.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
@@ -48,9 +48,8 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
* @author Olga Maciaszek-Sharma
*/
@SpringBootTest(classes = FeignHttpClientUrlTestsWithRetryableLoadBalancer.TestConfig.class,
webEnvironment = DEFINED_PORT,
value = { "spring.application.name=feignclienturlwithretryableloadbalancertest", "feign.hystrix.enabled=false",
"feign.okhttp.enabled=false" })
webEnvironment = DEFINED_PORT, value = { "spring.application.name=feignclienturlwithretryableloadbalancertest",
"feign.hystrix.enabled=false", "feign.okhttp.enabled=false" })
@DirtiesContext
class FeignHttpClientUrlTestsWithRetryableLoadBalancer {
@@ -21,8 +21,9 @@ import java.util.Objects;
import java.util.UUID;
import java.util.function.Function;
import javax.servlet.http.HttpServletRequest;
import feign.RequestInterceptor;
import jakarta.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.ObjectProvider;
@@ -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,13 +48,13 @@ 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
*/
@SpringBootTest(classes = CircuitBreakerTests.Application.class, webEnvironment = WebEnvironment.DEFINED_PORT,
value = { "spring.application.name=springcircuittest", "spring.jmx.enabled=false",
"feign.circuitbreaker.enabled=true" })
@SpringBootTest(classes = CircuitBreakerTests.Application.class, webEnvironment = WebEnvironment.DEFINED_PORT, value = {
"spring.application.name=springcircuittest", "spring.jmx.enabled=false", "feign.circuitbreaker.enabled=true" })
@DirtiesContext
class CircuitBreakerTests {
@@ -63,6 +64,9 @@ class CircuitBreakerTests {
@Autowired
TestClient testClient;
@Autowired
ExceptionClient exceptionClient;
@Autowired
TestClientWithFactory testClientWithFactory;
@@ -109,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 {
@@ -120,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 {
@@ -157,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
@@ -174,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 {
@@ -226,6 +272,11 @@ class CircuitBreakerTests {
return new TestFallbackFactory();
}
@Bean
ExceptionThrowingFallbackFactory exceptionThrowingFallbackFactory() {
return new ExceptionThrowingFallbackFactory();
}
}
}
@@ -65,9 +65,9 @@ import static org.mockito.Mockito.when;
* Commons project, so here we are only testing the interactions between
* {@link FeignBlockingLoadBalancerClient} and its delegates.
*
* @author Olga Maciaszek-Sharma
* @see <a href=
* "https://github.com/spring-cloud/spring-cloud-commons/blob/main/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/blocking/client/BlockingLoadBalancerClientTests.java">BlockingLoadBalancerClientTests</a>
* @author Olga Maciaszek-Sharma
*/
@ExtendWith(MockitoExtension.class)
class FeignBlockingLoadBalancerClientTests {
@@ -69,9 +69,9 @@ import static org.mockito.Mockito.when;
* Commons project, so here we are only testing the interactions between
* {@link RetryableFeignBlockingLoadBalancerClient} and its delegates.
*
* @author Olga Maciaszek-Sharma
* @see <a href=
* "https://github.com/spring-cloud/spring-cloud-commons/blob/main/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/blocking/client/BlockingLoadBalancerClientTests.java">BlockingLoadBalancerClientTests</a>
* @author Olga Maciaszek-Sharma
*/
@ExtendWith(MockitoExtension.class)
class RetryableFeignBlockingLoadBalancerClientTests {
@@ -16,7 +16,8 @@
package org.springframework.cloud.openfeign.security;
import jakarta.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
@@ -16,7 +16,8 @@
package org.springframework.cloud.openfeign.security;
import jakarta.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
@@ -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}")
@@ -32,12 +32,13 @@ import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.Part;
import feign.Client;
import feign.Logger;
import feign.RequestInterceptor;
import feign.codec.EncodeException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.Part;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
+2 -2
View File
@@ -6,11 +6,11 @@
<parent>
<artifactId>spring-cloud-dependencies-parent</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>4.0.0-M1</version>
<version>3.1.2</version>
<relativePath/>
</parent>
<artifactId>spring-cloud-openfeign-dependencies</artifactId>
<version>4.0.0-M1</version>
<version>3.1.2</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>4.0.0-M1</version>
<version>3.1.2</version>
<relativePath>..</relativePath>
</parent>
<artifactId>spring-cloud-starter-openfeign</artifactId>