Micrometer Support (#462)

Setting up micrometer for Feign, fixes #457
This makes spring-cloud-openfeign capable of configuring any Capability (e.g.: Metrics5Capability for Dropwizard Metrics or MicrometerCapability for micrometer). This also auto-configures MicrometerCapability if a MeterRegistry is available and feign-micrometer is on the classpath.
This commit is contained in:
Jonatan Ivanov
2021-02-09 11:58:10 -08:00
committed by GitHub
parent 9ac5972600
commit e745dd0c7c
21 changed files with 1263 additions and 73 deletions

View File

@@ -21,6 +21,7 @@
|feign.httpclient.max-connections-per-route | `50` |
|feign.httpclient.time-to-live | `900` |
|feign.httpclient.time-to-live-unit | |
|feign.metrics.enabled | `true` | Enables metrics capability for Feign.
|feign.okhttp.enabled | `false` | Enables the use of the OK HTTP Client by Feign.
|===

View File

@@ -32,9 +32,9 @@ Example spring boot app
@EnableFeignClients
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
----
@@ -44,14 +44,14 @@ public class Application {
----
@FeignClient("stores")
public interface StoreClient {
@RequestMapping(method = RequestMethod.GET, value = "/stores")
List<Store> getStores();
@RequestMapping(method = RequestMethod.GET, value = "/stores")
List<Store> getStores();
@RequestMapping(method = RequestMethod.GET, value = "/stores")
Page<Store> getStores(Pageable pageable);
@RequestMapping(method = RequestMethod.GET, value = "/stores")
Page<Store> getStores(Pageable pageable);
@RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json")
Store update(@PathVariable("storeId") Long storeId, Store store);
@RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json")
Store update(@PathVariable("storeId") Long storeId, Store store);
}
----
@@ -65,7 +65,7 @@ of the `@FeignClient` annotation.
The load-balancer client above will want to discover the physical addresses
for the "stores" service. If your application is a Eureka client then
it will resolve the service in the Eureka service registry. If you
don't want to use Eureka, you can simply configure a list of servers
don't want to use Eureka, you can configure a list of servers
in your external configuration using https://cloud.spring.io/spring-cloud-static/spring-cloud-commons/current/reference/html/#simplediscoveryclient[`SimpleDiscoveryClient`].
Spring Cloud OpenFeign supports all the features available for the blocking mode of Spring Cloud LoadBalancer. You can read more about them in the https://docs.spring.io/spring-cloud-commons/docs/current/reference/html/#spring-cloud-loadbalancer[project documentation].
@@ -84,7 +84,7 @@ Spring Cloud lets you take full control of the feign client by declaring additio
----
@FeignClient(name = "stores", configuration = FooConfiguration.class)
public interface StoreClient {
//..
//..
}
----
@@ -104,7 +104,7 @@ Placeholders are supported in the `name` and `url` attributes.
----
@FeignClient(name = "${feign.name}", url = "${feign.url}")
public interface StoreClient {
//..
//..
}
----
@@ -113,10 +113,11 @@ Spring Cloud OpenFeign provides the following beans by default for feign (`BeanT
* `Decoder` feignDecoder: `ResponseEntityDecoder` (which wraps a `SpringDecoder`)
* `Encoder` feignEncoder: `SpringEncoder`
* `Logger` feignLogger: `Slf4jLogger`
* `MicrometerCapability` micrometerCapability: If `feign-micrometer` is on the classpath and `MeterRegistry` is available
* `Contract` feignContract: `SpringMvcContract`
* `Feign.Builder` feignBuilder: `FeignCircuitBreaker.Builder`
* `Client` feignClient: if Spring Cloud LoadBalancer is in the classpath, `FeignBlockingLoadBalancerClient` is used.
If none of them is in the classpath, the default feign client is used.
* `Client` feignClient: If Spring Cloud LoadBalancer is on the classpath, `FeignBlockingLoadBalancerClient` is used.
If none of them is on the classpath, the default feign client is used.
NOTE: `spring-cloud-starter-openfeign` supports `spring-cloud-starter-loadbalancer`. However, as is an optional dependency, you need to make sure it been added to your project if you want to use it.
@@ -132,6 +133,7 @@ Spring Cloud OpenFeign _does not_ provide the following beans by default for fei
* `Collection<RequestInterceptor>`
* `SetterFactory`
* `QueryMapEncoder`
* `Capability` (`MicrometerCapability` is provided by default)
A bean of `Retryer.NEVER_RETRY` with the type `Retryer` is created by default, which will disable retrying.
Notice this retrying behavior is different from the Feign default one, where it will automatically retry IOExceptions,
@@ -143,15 +145,15 @@ Creating a bean of one of those type and placing it in a `@FeignClient` configur
----
@Configuration
public class FooConfiguration {
@Bean
public Contract feignContract() {
return new feign.Contract.Default();
}
@Bean
public Contract feignContract() {
return new feign.Contract.Default();
}
@Bean
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
return new BasicAuthRequestInterceptor("user", "password");
}
@Bean
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
return new BasicAuthRequestInterceptor("user", "password");
}
}
----
@@ -163,25 +165,29 @@ application.yml
[source,yaml]
----
feign:
client:
config:
feignName:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: full
errorDecoder: com.example.SimpleErrorDecoder
retryer: com.example.SimpleRetryer
defaultQueryParameters:
query: queryValue
defaultRequestHeaders:
header: headerValue
requestInterceptors:
- com.example.FooRequestInterceptor
- com.example.BarRequestInterceptor
decode404: false
encoder: com.example.SimpleEncoder
decoder: com.example.SimpleDecoder
contract: com.example.SimpleContract
client:
config:
feignName:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: full
errorDecoder: com.example.SimpleErrorDecoder
retryer: com.example.SimpleRetryer
defaultQueryParameters:
query: queryValue
defaultRequestHeaders:
header: headerValue
requestInterceptors:
- com.example.FooRequestInterceptor
- com.example.BarRequestInterceptor
decode404: false
encoder: com.example.SimpleEncoder
decoder: com.example.SimpleDecoder
contract: com.example.SimpleContract
capabilities:
- com.example.FooCapability
- com.example.BarCapability
metrics.enabled: false
----
Default configurations can be specified in the `@EnableFeignClients` attribute `defaultConfiguration` in a similar manner as described above. The difference is that this configuration will apply to _all_ feign clients.
@@ -194,12 +200,12 @@ application.yml
[source,yaml]
----
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: basic
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: basic
----
If we create both `@Configuration` bean and configuration properties, configuration properties will win.
@@ -215,7 +221,7 @@ collision of these configuration beans.
----
@FeignClient(contextId = "fooClient", name = "stores", configuration = FooConfiguration.class)
public interface FooClient {
//..
//..
}
----
@@ -223,7 +229,7 @@ public interface FooClient {
----
@FeignClient(contextId = "barClient", name = "stores", configuration = BarConfiguration.class)
public interface BarClient {
//..
//..
}
----
@@ -279,12 +285,13 @@ class FooController {
private FooClient adminClient;
@Autowired
public FooController(Decoder decoder, Encoder encoder, Client client, Contract contract) {
@Autowired
public FooController(Client client, Encoder encoder, Decoder decoder, Contract contract, MicrometerCapability micrometerCapability) {
this.fooClient = Feign.builder().client(client)
.encoder(encoder)
.decoder(decoder)
.contract(contract)
.addCapability(micrometerCapability)
.requestInterceptor(new BasicAuthRequestInterceptor("user", "user"))
.target(FooClient.class, "https://PROD-SVC");
@@ -292,9 +299,10 @@ class FooController {
.encoder(encoder)
.decoder(decoder)
.contract(contract)
.addCapability(micrometerCapability)
.requestInterceptor(new BasicAuthRequestInterceptor("admin", "admin"))
.target(FooClient.class, "https://PROD-SVC");
}
}
}
----
@@ -321,7 +329,7 @@ To disable Spring Cloud CircuitBreaker support on a per-client basis create a va
----
@Configuration
public class FooConfiguration {
@Bean
@Bean
@Scope("prototype")
public Feign.Builder feignBuilder() {
return Feign.builder();
@@ -371,8 +379,8 @@ This allows grouping common operations into convenient base interfaces.
----
public interface UserService {
@RequestMapping(method = RequestMethod.GET, value ="/users/{id}")
User getUser(@PathVariable("id") long id);
@RequestMapping(method = RequestMethod.GET, value ="/users/{id}")
User getUser(@PathVariable("id") long id);
}
----
@@ -455,10 +463,63 @@ For example, the following would set the `Logger.Level` to `FULL`:
----
@Configuration
public class FooConfiguration {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
----
=== Feign Capability support
The Feign capabilities expose core Feign components so that these components can be modified. For example, the capabilities can take the `Client`, _decorate_ it, and give the decorated instance back to Feign.
The support for metrics libraries is a good real-life example for this. See <<feign-metrics>>.
Creating one or more `Capability` beans and placing them in a `@FeignClient` configuration lets you register them and modify the behavior of the involved client.
[source,java,indent=0]
----
@Configuration
public class FooConfiguration {
@Bean
Capability customCapability() {
return new CustomCapability();
}
}
----
=== Feign metrics
If all of the following conditions are true, a `MicrometerCapability` bean is created and registered so that your Feign client publishes metrics to Micrometer:
* `feign-micrometer` is on the classpath
* A `MeterRegistry` bean is available
* feign metrics properties are set to `true` (by default)
- `feign.metrics.enabled=true` (for all clients)
- `feign.client.config.feignName.metrics.enabled=true` (for a single client)
NOTE: If your application already uses Micrometer, enabling metrics is as simple as putting `feign-micrometer` onto your classpath.
You can also disable the feature by either:
* excluding `feign-micrometer` from your classpath
* setting one of the feign metrics properties to `false`
- `feign.metrics.enabled=false`
- `feign.client.config.feignName.metrics.enabled=false`
NOTE: `feign.metrics.enabled=false` disables metrics support for *all* Feign clients regardless of the value of the client-level flags: `feign.client.config.feignName.metrics.enabled`.
If you want to enable or disable merics per client, don't set `feign.metrics.enabled` and use `feign.client.config.feignName.metrics.enabled`.
You can also customize the `MicrometerCapability` by registering your own bean:
[source,java,indent=0]
----
@Configuration
public class FooConfiguration {
@Bean
public MicrometerCapability micrometerCapability(MeterRegistry meterRegistry) {
return new MicrometerCapability(meterRegistry);
}
}
----
@@ -477,10 +538,10 @@ For example, the `Params` class defines parameters `param1` and `param2`:
----
// Params.java
public class Params {
private String param1;
private String param2;
private String param1;
private String param2;
// [Getters and setters omitted for brevity]
// [Getters and setters omitted for brevity]
}
----
@@ -491,8 +552,8 @@ The following feign client uses the `Params` class by using the `@SpringQueryMap
@FeignClient("demo")
public interface DemoTemplate {
@GetMapping(path = "/demo")
String demoEndpoint(@SpringQueryMap Params params);
@GetMapping(path = "/demo")
String demoEndpoint(@SpringQueryMap Params params);
}
----
@@ -513,8 +574,8 @@ and deserialize HATEOAS representation models: https://docs.spring.io/spring-hat
@FeignClient("demo")
public interface DemoTemplate {
@GetMapping(path = "/stores")
CollectionModel<Store> getStores();
@GetMapping(path = "/stores")
CollectionModel<Store> getStores();
}
----
@@ -543,8 +604,8 @@ Note that both variable name and the path segment placeholder are called `matrix
@FeignClient("demo")
public interface DemoTemplate {
@GetMapping(path = "/stores")
CollectionModel<Store> getStores();
@GetMapping(path = "/stores")
CollectionModel<Store> getStores();
}
----

View File

@@ -99,6 +99,11 @@
<artifactId>feign-slf4j</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-micrometer</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>

View File

@@ -22,6 +22,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import feign.Capability;
import feign.Client;
import feign.Contract;
import feign.ExceptionPropagationPolicy;
@@ -60,6 +61,7 @@ import org.springframework.util.StringUtils;
* @author Olga Maciaszek-Sharma
* @author Ilia Ilinykh
* @author Marcin Grzejszczak
* @author Jonatan Ivanov
*/
public class FeignClientFactoryBean
implements FactoryBean<Object>, InitializingBean, ApplicationContextAware, BeanFactoryAware {
@@ -199,6 +201,12 @@ public class FeignClientFactoryBean
if (exceptionPropagationPolicy != null) {
builder.exceptionPropagationPolicy(exceptionPropagationPolicy);
}
Map<String, Capability> capabilities = getInheritedAwareInstances(context, Capability.class);
if (capabilities != null) {
capabilities.values().stream().sorted(AnnotationAwareOrderComparator.INSTANCE)
.forEach(builder::addCapability);
}
}
protected void configureUsingProperties(FeignClientProperties.FeignClientConfiguration config,
@@ -264,6 +272,10 @@ public class FeignClientFactoryBean
if (Objects.nonNull(config.getExceptionPropagationPolicy())) {
builder.exceptionPropagationPolicy(config.getExceptionPropagationPolicy());
}
if (config.getCapabilities() != null) {
config.getCapabilities().stream().map(this::getOrInstantiate).forEach(builder::addCapability);
}
}
private <T> T getOrInstantiate(Class<T> tClass) {

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2021-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.
* 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;
import java.util.Map;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
/**
* @author Jonatan Ivanov
*/
class FeignClientMetricsEnabledCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
FeignClientProperties feignClientProperties = context.getBeanFactory()
.getBeanProvider(FeignClientProperties.class).getIfAvailable();
if (feignClientProperties != null) {
Map<String, FeignClientProperties.FeignClientConfiguration> feignClientConfigMap = feignClientProperties
.getConfig();
if (feignClientConfigMap != null) {
FeignClientProperties.FeignClientConfiguration feignClientConfig = feignClientConfigMap
.get(context.getEnvironment().getProperty("feign.client.name"));
if (feignClientConfig != null) {
FeignClientProperties.MetricsProperties metrics = feignClientConfig.getMetrics();
if (metrics != null && metrics.getEnabled() != null) {
return metrics.getEnabled();
}
}
}
}
return true;
}
}

View File

@@ -22,6 +22,7 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import feign.Capability;
import feign.Contract;
import feign.ExceptionPropagationPolicy;
import feign.Logger;
@@ -37,6 +38,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
* @author Eko Kurniawan Khannedy
* @author Ilia Ilinykh
* @author Ram Anaswara
* @author Jonatan Ivanov
*/
@ConfigurationProperties("feign.client")
public class FeignClientProperties {
@@ -134,6 +136,10 @@ public class FeignClientProperties {
private ExceptionPropagationPolicy exceptionPropagationPolicy;
private List<Class<Capability>> capabilities;
private MetricsProperties metrics;
public Logger.Level getLoggerLevel() {
return loggerLevel;
}
@@ -238,6 +244,22 @@ public class FeignClientProperties {
this.exceptionPropagationPolicy = exceptionPropagationPolicy;
}
public List<Class<Capability>> getCapabilities() {
return capabilities;
}
public void setCapabilities(List<Class<Capability>> capabilities) {
this.capabilities = capabilities;
}
public MetricsProperties getMetrics() {
return metrics;
}
public void setMetrics(MetricsProperties metrics) {
this.metrics = metrics;
}
@Override
public boolean equals(Object o) {
if (this == o) {
@@ -255,14 +277,50 @@ public class FeignClientProperties {
&& Objects.equals(decoder, that.decoder) && Objects.equals(contract, that.contract)
&& Objects.equals(exceptionPropagationPolicy, that.exceptionPropagationPolicy)
&& Objects.equals(defaultRequestHeaders, that.defaultRequestHeaders)
&& Objects.equals(defaultQueryParameters, that.defaultQueryParameters);
&& Objects.equals(defaultQueryParameters, that.defaultQueryParameters)
&& Objects.equals(capabilities, that.capabilities) && Objects.equals(metrics, that.metrics);
}
@Override
public int hashCode() {
return Objects.hash(loggerLevel, connectTimeout, readTimeout, retryer, errorDecoder, requestInterceptors,
decode404, encoder, decoder, contract, exceptionPropagationPolicy, defaultQueryParameters,
defaultRequestHeaders);
defaultRequestHeaders, capabilities, metrics);
}
}
/**
* Metrics configuration for Feign Client.
*/
public static class MetricsProperties {
private Boolean enabled = true;
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
MetricsProperties that = (MetricsProperties) o;
return Objects.equals(enabled, that.enabled);
}
@Override
public int hashCode() {
return Objects.hash(enabled);
}
}

View File

@@ -27,7 +27,9 @@ import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.form.MultipartFormContentProcessor;
import feign.form.spring.SpringFormEncoder;
import feign.micrometer.MicrometerCapability;
import feign.optionals.OptionalDecoder;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.ObjectProvider;
@@ -62,6 +64,7 @@ import static feign.form.ContentType.MULTIPART;
* @author Dave Syer
* @author Venil Noronha
* @author Darren Foong
* @author Jonatan Ivanov
*/
@Configuration(proxyBeanMethods = false)
public class FeignClientsConfiguration {
@@ -203,4 +206,19 @@ public class FeignClientsConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(type = "io.micrometer.core.instrument.MeterRegistry")
@ConditionalOnClass(name = "feign.micrometer.MicrometerCapability")
@ConditionalOnProperty(name = "feign.metrics.enabled", matchIfMissing = true)
@Conditional(FeignClientMetricsEnabledCondition.class)
protected static class MetricsConfiguration {
@Bean
@ConditionalOnMissingBean
public MicrometerCapability micrometerCapability(MeterRegistry meterRegistry) {
return new MicrometerCapability(meterRegistry);
}
}
}

View File

@@ -43,6 +43,12 @@
"type": "java.lang.Boolean",
"description": "Enables the request sent by Feign to be compressed.",
"defaultValue": "false"
},
{
"name": "feign.metrics.enabled",
"type": "java.lang.Boolean",
"description": "Enables metrics capability for Feign.",
"defaultValue": "true"
}
]
}

View File

@@ -0,0 +1,140 @@
/*
* Copyright 2021-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.
* 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;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import feign.Capability;
import feign.Contract;
import feign.ExceptionPropagationPolicy;
import feign.Logger;
import feign.RequestInterceptor;
import feign.Retryer;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.codec.ErrorDecoder;
import org.assertj.core.util.Lists;
import org.assertj.core.util.Maps;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsAndHashCodeConsistency;
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsConsistency;
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsReflexivity;
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsSymmetricity;
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsTransitivity;
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertHashCodeConsistency;
/**
* @author Jonatan Ivanov
*/
class FeignClientConfigurationTests {
@Test
void shouldDefaultToValuesWhenFieldsNotSet() {
FeignClientProperties.FeignClientConfiguration config = new FeignClientProperties.FeignClientConfiguration();
assertThat(config.getLoggerLevel()).isNull();
assertThat(config.getConnectTimeout()).isNull();
assertThat(config.getReadTimeout()).isNull();
assertThat(config.getRetryer()).isNull();
assertThat(config.getErrorDecoder()).isNull();
assertThat(config.getRequestInterceptors()).isNull();
assertThat(config.getDefaultRequestHeaders()).isNull();
assertThat(config.getDefaultQueryParameters()).isNull();
assertThat(config.getDecode404()).isNull();
assertThat(config.getDecoder()).isNull();
assertThat(config.getEncoder()).isNull();
assertThat(config.getContract()).isNull();
assertThat(config.getExceptionPropagationPolicy()).isNull();
assertThat(config.getCapabilities()).isNull();
assertThat(config.getMetrics()).isNull();
}
@Test
void shouldReturnValuesWhenSet() {
FeignClientProperties.FeignClientConfiguration config = new FeignClientProperties.FeignClientConfiguration();
config.setLoggerLevel(Logger.Level.FULL);
config.setConnectTimeout(21);
config.setReadTimeout(42);
config.setRetryer(Retryer.class);
config.setErrorDecoder(ErrorDecoder.class);
List<Class<RequestInterceptor>> requestInterceptors = Lists.list(RequestInterceptor.class);
config.setRequestInterceptors(requestInterceptors);
Map<String, Collection<String>> defaultRequestHeaders = Maps.newHashMap("default", Lists.emptyList());
config.setDefaultRequestHeaders(defaultRequestHeaders);
Map<String, Collection<String>> defaultQueryParameters = Maps.newHashMap("default", Lists.emptyList());
config.setDefaultQueryParameters(defaultQueryParameters);
config.setDecode404(true);
config.setDecoder(Decoder.class);
config.setEncoder(Encoder.class);
config.setContract(Contract.class);
config.setExceptionPropagationPolicy(ExceptionPropagationPolicy.UNWRAP);
List<Class<Capability>> capabilities = Lists.list(Capability.class);
config.setCapabilities(capabilities);
FeignClientProperties.MetricsProperties metrics = new FeignClientProperties.MetricsProperties();
config.setMetrics(metrics);
assertThat(config.getLoggerLevel()).isSameAs(Logger.Level.FULL);
assertThat(config.getConnectTimeout()).isEqualTo(21);
assertThat(config.getReadTimeout()).isEqualTo(42);
assertThat(config.getRetryer()).isSameAs(Retryer.class);
assertThat(config.getErrorDecoder()).isSameAs(ErrorDecoder.class);
assertThat(config.getRequestInterceptors()).isSameAs(requestInterceptors);
assertThat(config.getDefaultRequestHeaders()).isSameAs(defaultRequestHeaders);
assertThat(config.getDefaultQueryParameters()).isSameAs(defaultQueryParameters);
assertThat(config.getDecode404()).isTrue();
assertThat(config.getDecoder()).isSameAs(Decoder.class);
assertThat(config.getEncoder()).isSameAs(Encoder.class);
assertThat(config.getContract()).isSameAs(Contract.class);
assertThat(config.getExceptionPropagationPolicy()).isSameAs(ExceptionPropagationPolicy.UNWRAP);
assertThat(config.getCapabilities()).isSameAs(capabilities);
assertThat(config.getMetrics()).isSameAs(metrics);
}
/**
* Sanity-checks equals and hashCode contracts but does not check every variation of
* the fields.
*/
@Test
void shouldHaveSomewhatValidEqualsAndHashCode() {
FeignClientProperties.FeignClientConfiguration configOne = new FeignClientProperties.FeignClientConfiguration();
FeignClientProperties.FeignClientConfiguration configTwo = new FeignClientProperties.FeignClientConfiguration();
FeignClientProperties.FeignClientConfiguration configThree = new FeignClientProperties.FeignClientConfiguration();
FeignClientProperties.FeignClientConfiguration differentConfig = new FeignClientProperties.FeignClientConfiguration();
differentConfig.setDecode404(true);
assertEqualsReflexivity(configOne);
assertEqualsSymmetricity(configOne, configTwo);
assertEqualsSymmetricity(configOne, differentConfig);
assertEqualsSymmetricity(configOne, 42);
assertEqualsTransitivity(configOne, configTwo, configThree);
assertEqualsConsistency(configOne, configTwo);
assertEqualsConsistency(configOne, differentConfig);
assertEqualsConsistency(configOne, 42);
assertEqualsConsistency(configOne, null);
assertHashCodeConsistency(configOne);
assertEqualsAndHashCodeConsistency(configOne, configTwo);
}
}

View File

@@ -0,0 +1,121 @@
/*
* Copyright 2021-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.
* 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;
import java.util.Map;
import feign.Capability;
import feign.Contract;
import feign.RequestLine;
import feign.micrometer.MicrometerCapability;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Jonatan Ivanov
*/
@DirtiesContext
@ActiveProfiles("no-foo-metrics")
@SpringBootTest(classes = FeignClientDisabledClientLevelFeaturesTests.TestConfiguration.class)
class FeignClientDisabledClientLevelFeaturesTests {
@Autowired
private FeignContext context;
@Autowired
private FooClient foo;
@Autowired
private BarClient bar;
@Test
void clientsAvailable() {
assertThat(foo).isNotNull();
assertThat(bar).isNotNull();
}
@Test
void capabilitiesShouldNotBeAvailableWhenDisabled() {
assertThat(context.getInstance("foo", MicrometerCapability.class)).isNull();
assertThat(context.getInstances("foo", Capability.class)).isEmpty();
assertThat(context.getInstance("bar", MicrometerCapability.class)).isNotNull();
Map<String, Capability> barCapabilities = context.getInstances("bar", Capability.class);
assertThat(barCapabilities).hasSize(2);
assertThat(barCapabilities.get("micrometerCapability")).isExactlyInstanceOf(MicrometerCapability.class);
assertThat(barCapabilities.get("noOpCapability")).isExactlyInstanceOf(NoOpCapability.class);
}
@FeignClient(name = "foo", url = "https://foo", configuration = FooConfiguration.class)
interface FooClient {
@RequestLine("GET /")
String get();
}
@FeignClient(name = "bar", url = "https://bar", configuration = BarConfiguration.class)
interface BarClient {
@RequestMapping(value = "/", method = RequestMethod.GET)
String get();
}
@Configuration(proxyBeanMethods = false)
@EnableAutoConfiguration
@EnableConfigurationProperties(FeignClientProperties.class)
@EnableFeignClients(clients = { FooClient.class, BarClient.class })
protected static class TestConfiguration {
}
public static class FooConfiguration {
@Bean // if the feign configuration empty, the context is not able to start
public Contract feignContract() {
return new Contract.Default();
}
}
public static class BarConfiguration {
@Bean
public Capability noOpCapability() {
return new NoOpCapability();
}
}
private static class NoOpCapability implements Capability {
}
}

View File

@@ -0,0 +1,120 @@
/*
* Copyright 2021-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.
* 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;
import java.util.Map;
import feign.Capability;
import feign.Contract;
import feign.RequestLine;
import feign.micrometer.MicrometerCapability;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Jonatan Ivanov
*/
@DirtiesContext
@ActiveProfiles("no-metrics")
@SpringBootTest(classes = FeignClientDisabledFeaturesTests.TestConfiguration.class)
class FeignClientDisabledFeaturesTests {
@Autowired
private FeignContext context;
@Autowired
private FooClient foo;
@Autowired
private BarClient bar;
@Test
void clientsAvailable() {
assertThat(foo).isNotNull();
assertThat(bar).isNotNull();
}
@Test
void capabilitiesShouldNotBeAvailable() {
assertThat(context.getInstance("foo", MicrometerCapability.class)).isNull();
assertThat(context.getInstances("foo", Capability.class)).isEmpty();
assertThat(context.getInstance("bar", MicrometerCapability.class)).isNull();
Map<String, Capability> barCapabilities = context.getInstances("bar", Capability.class);
assertThat(barCapabilities).hasSize(1);
assertThat(barCapabilities.get("noOpCapability")).isExactlyInstanceOf(NoOpCapability.class);
}
@FeignClient(name = "foo", url = "https://foo", configuration = FooConfiguration.class)
interface FooClient {
@RequestLine("GET /")
String get();
}
@FeignClient(name = "bar", url = "https://bar", configuration = BarConfiguration.class)
interface BarClient {
@RequestMapping(value = "/", method = RequestMethod.GET)
String get();
}
@Configuration(proxyBeanMethods = false)
@EnableAutoConfiguration
@EnableConfigurationProperties(FeignClientProperties.class)
@EnableFeignClients(clients = { FooClient.class, BarClient.class })
protected static class TestConfiguration {
}
public static class FooConfiguration {
@Bean // if the feign configuration empty, the context is not able to start
public Contract feignContract() {
return new Contract.Default();
}
}
public static class BarConfiguration {
@Bean
public Capability noOpCapability() {
return new NoOpCapability();
}
}
private static class NoOpCapability implements Capability {
}
}

View File

@@ -44,6 +44,7 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Michael Cramer
* @author Jonatan Ivanov
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = FeignClientErrorDecoderTests.TestConfiguration.class)
@@ -85,7 +86,8 @@ public class FeignClientErrorDecoderTests {
@SuppressWarnings({ "unchecked", "ConstantConditions" })
private Object getErrorDecoderFromClient(final Object client) {
Object invocationHandler = ReflectionTestUtils.getField(client, "h");
Object invocationHandlerLambda = ReflectionTestUtils.getField(client, "h");
Object invocationHandler = ReflectionTestUtils.getField(invocationHandlerLambda, "arg$2");
Map<Method, InvocationHandlerFactory.MethodHandler> dispatch = (Map<Method, InvocationHandlerFactory.MethodHandler>) ReflectionTestUtils
.getField(invocationHandler, "dispatch");
Method key = new ArrayList<>(dispatch.keySet()).get(0);

View File

@@ -0,0 +1,225 @@
/*
* Copyright 2021-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.
* 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;
import java.util.HashMap;
import org.assertj.core.util.Maps;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* @author Jonatan Ivanov
*/
@ExtendWith({ MockitoExtension.class })
class FeignClientMetricsEnabledConditionTests {
@Mock
private ConditionContext context;
@Mock
private AnnotatedTypeMetadata metadata;
@Mock
private ConfigurableListableBeanFactory beanFactory;
@Mock
private ObjectProvider<FeignClientProperties> beanProvider;
@Mock
private Environment environment;
private final FeignClientMetricsEnabledCondition condition = new FeignClientMetricsEnabledCondition();
@BeforeEach
void setUp() {
when(context.getBeanFactory()).thenReturn(beanFactory);
when(beanFactory.getBeanProvider(FeignClientProperties.class)).thenReturn(beanProvider);
}
@AfterEach
void tearDown() {
verify(context).getBeanFactory();
verify(beanFactory).getBeanProvider(FeignClientProperties.class);
verify(beanProvider).getIfAvailable();
}
@Test
void shouldMatchWhenFeignClientPropertiesBeanIsMissing() {
when(beanProvider.getIfAvailable()).thenReturn(null);
assertThat(condition.matches(context, metadata)).isTrue();
verify(environment, never()).getProperty("feign.client.name");
}
@Test
void shouldMatchWhenConfigMapIsMissing() {
FeignClientProperties feignClientProperties = mock(FeignClientProperties.class);
when(beanProvider.getIfAvailable()).thenReturn(feignClientProperties);
when(feignClientProperties.getConfig()).thenReturn(null);
assertThat(condition.matches(context, metadata)).isTrue();
verify(environment, never()).getProperty("feign.client.name");
}
@Test
void shouldMatchWhenConfigMapDoesNotContainTheConfig() {
FeignClientProperties feignClientProperties = mock(FeignClientProperties.class);
when(beanProvider.getIfAvailable()).thenReturn(feignClientProperties);
when(context.getEnvironment()).thenReturn(environment);
when(environment.getProperty("feign.client.name")).thenReturn("foo");
when(feignClientProperties.getConfig()).thenReturn(new HashMap<>());
assertThat(condition.matches(context, metadata)).isTrue();
verify(environment).getProperty("feign.client.name");
}
@Test
void shouldMatchWhenClientNameIsNull() {
FeignClientProperties feignClientProperties = mock(FeignClientProperties.class);
when(beanProvider.getIfAvailable()).thenReturn(feignClientProperties);
when(context.getEnvironment()).thenReturn(environment);
when(environment.getProperty("feign.client.name")).thenReturn(null);
when(feignClientProperties.getConfig()).thenReturn(new HashMap<>());
assertThat(condition.matches(context, metadata)).isTrue();
verify(environment).getProperty("feign.client.name");
}
@Test
void shouldMatchWhenClientNameIsEmpty() {
FeignClientProperties feignClientProperties = mock(FeignClientProperties.class);
when(beanProvider.getIfAvailable()).thenReturn(feignClientProperties);
when(context.getEnvironment()).thenReturn(environment);
when(environment.getProperty("feign.client.name")).thenReturn("");
when(feignClientProperties.getConfig()).thenReturn(new HashMap<>());
assertThat(condition.matches(context, metadata)).isTrue();
verify(environment).getProperty("feign.client.name");
}
@Test
void shouldMatchWhenConfigMapContainsNullConfig() {
FeignClientProperties feignClientProperties = mock(FeignClientProperties.class);
when(beanProvider.getIfAvailable()).thenReturn(feignClientProperties);
when(context.getEnvironment()).thenReturn(environment);
when(environment.getProperty("feign.client.name")).thenReturn("foo");
when(feignClientProperties.getConfig()).thenReturn(Maps.newHashMap("foo", null));
assertThat(condition.matches(context, metadata)).isTrue();
verify(environment).getProperty("feign.client.name");
}
@Test
void shouldMatchWhenMetricsConfigurationIsMissing() {
FeignClientProperties feignClientProperties = mock(FeignClientProperties.class);
FeignClientProperties.FeignClientConfiguration feignClientConfig = mock(
FeignClientProperties.FeignClientConfiguration.class);
when(beanProvider.getIfAvailable()).thenReturn(feignClientProperties);
when(context.getEnvironment()).thenReturn(environment);
when(environment.getProperty("feign.client.name")).thenReturn("foo");
when(feignClientProperties.getConfig()).thenReturn(Maps.newHashMap("foo", feignClientConfig));
when(feignClientConfig.getMetrics()).thenReturn(null);
assertThat(condition.matches(context, metadata)).isTrue();
verify(environment).getProperty("feign.client.name");
}
@Test
void shouldMatchWhenEnabledFlagIsNotSet() {
FeignClientProperties feignClientProperties = mock(FeignClientProperties.class);
FeignClientProperties.FeignClientConfiguration feignClientConfig = mock(
FeignClientProperties.FeignClientConfiguration.class);
when(beanProvider.getIfAvailable()).thenReturn(feignClientProperties);
when(context.getEnvironment()).thenReturn(environment);
when(environment.getProperty("feign.client.name")).thenReturn("foo");
when(feignClientProperties.getConfig()).thenReturn(Maps.newHashMap("foo", feignClientConfig));
when(feignClientConfig.getMetrics()).thenReturn(new FeignClientProperties.MetricsProperties());
assertThat(condition.matches(context, metadata)).isTrue();
verify(environment).getProperty("feign.client.name");
}
@Test
void shouldMatchWhenEnabledFlagIsNull() {
FeignClientProperties feignClientProperties = mock(FeignClientProperties.class);
FeignClientProperties.FeignClientConfiguration feignClientConfig = mock(
FeignClientProperties.FeignClientConfiguration.class);
when(beanProvider.getIfAvailable()).thenReturn(feignClientProperties);
when(context.getEnvironment()).thenReturn(environment);
when(environment.getProperty("feign.client.name")).thenReturn("foo");
when(feignClientProperties.getConfig()).thenReturn(Maps.newHashMap("foo", feignClientConfig));
FeignClientProperties.MetricsProperties metricsProperties = new FeignClientProperties.MetricsProperties();
metricsProperties.setEnabled(null);
when(feignClientConfig.getMetrics()).thenReturn(metricsProperties);
assertThat(condition.matches(context, metadata)).isTrue();
verify(environment).getProperty("feign.client.name");
}
@Test
void shouldMatchWhenMetricsConfigurationIsEnabled() {
FeignClientProperties feignClientProperties = mock(FeignClientProperties.class);
FeignClientProperties.FeignClientConfiguration feignClientConfig = mock(
FeignClientProperties.FeignClientConfiguration.class);
when(beanProvider.getIfAvailable()).thenReturn(feignClientProperties);
when(context.getEnvironment()).thenReturn(environment);
when(environment.getProperty("feign.client.name")).thenReturn("foo");
when(feignClientProperties.getConfig()).thenReturn(Maps.newHashMap("foo", feignClientConfig));
FeignClientProperties.MetricsProperties metricsProperties = new FeignClientProperties.MetricsProperties();
metricsProperties.setEnabled(true);
when(feignClientConfig.getMetrics()).thenReturn(metricsProperties);
assertThat(condition.matches(context, metadata)).isTrue();
verify(environment).getProperty("feign.client.name");
}
@Test
void shouldNotMatchWhenMetricsConfigurationIsEnabled() {
FeignClientProperties feignClientProperties = mock(FeignClientProperties.class);
FeignClientProperties.FeignClientConfiguration feignClientConfig = mock(
FeignClientProperties.FeignClientConfiguration.class);
when(beanProvider.getIfAvailable()).thenReturn(feignClientProperties);
when(context.getEnvironment()).thenReturn(environment);
when(environment.getProperty("feign.client.name")).thenReturn("foo");
when(feignClientProperties.getConfig()).thenReturn(Maps.newHashMap("foo", feignClientConfig));
FeignClientProperties.MetricsProperties metricsProperties = new FeignClientProperties.MetricsProperties();
metricsProperties.setEnabled(false);
when(feignClientConfig.getMetrics()).thenReturn(metricsProperties);
assertThat(condition.matches(context, metadata)).isFalse();
verify(environment).getProperty("feign.client.name");
}
}

View File

@@ -16,6 +16,9 @@
package org.springframework.cloud.openfeign;
import java.util.Map;
import feign.Capability;
import feign.Contract;
import feign.ExceptionPropagationPolicy;
import feign.Logger;
@@ -28,6 +31,7 @@ import feign.auth.BasicAuthRequestInterceptor;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.codec.ErrorDecoder;
import feign.micrometer.MicrometerCapability;
import feign.optionals.OptionalDecoder;
import feign.querymap.BeanQueryMapEncoder;
import feign.slf4j.Slf4jLogger;
@@ -48,6 +52,7 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Spencer Gibb
* @author Jonatan Ivanov
*/
@SpringBootTest(classes = FeignClientOverrideDefaultsTests.TestConfiguration.class)
@DirtiesContext
@@ -137,6 +142,22 @@ class FeignClientOverrideDefaultsTests {
.containsValues(ExceptionPropagationPolicy.UNWRAP);
}
@Test
void shouldOverrideMicrometerCapability() {
assertThat(context.getInstance("foo", MicrometerCapability.class))
.isExactlyInstanceOf(TestMicrometerCapability.class);
Map<String, Capability> fooCapabilities = context.getInstances("foo", Capability.class);
assertThat(fooCapabilities).hasSize(1);
assertThat(fooCapabilities.get("micrometerCapability")).isExactlyInstanceOf(TestMicrometerCapability.class);
assertThat(context.getInstance("bar", MicrometerCapability.class))
.isExactlyInstanceOf(TestMicrometerCapability.class);
Map<String, Capability> barCapabilities = context.getInstances("bar", Capability.class);
assertThat(barCapabilities).hasSize(2);
assertThat(barCapabilities.get("micrometerCapability")).isExactlyInstanceOf(TestMicrometerCapability.class);
assertThat(barCapabilities.get("noOpCapability")).isExactlyInstanceOf(NoOpCapability.class);
}
@FeignClient(name = "foo", url = "https://foo", configuration = FooConfiguration.class)
interface FooClient {
@@ -164,6 +185,11 @@ class FeignClientOverrideDefaultsTests {
};
}
@Bean
MicrometerCapability micrometerCapability() {
return new TestMicrometerCapability();
}
}
public static class FooConfiguration {
@@ -232,6 +258,19 @@ class FeignClientOverrideDefaultsTests {
return ExceptionPropagationPolicy.UNWRAP;
}
@Bean
public Capability noOpCapability() {
return new NoOpCapability();
}
}
private static class TestMicrometerCapability extends feign.micrometer.MicrometerCapability {
}
private static class NoOpCapability implements Capability {
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2021-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.
* 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;
import java.util.Map;
import org.assertj.core.util.Maps;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsAndHashCodeConsistency;
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsConsistency;
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsReflexivity;
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsSymmetricity;
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsTransitivity;
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertHashCodeConsistency;
/**
* @author Jonatan Ivanov
*/
class FeignClientPropertiesTests {
@Test
void shouldDefaultToValuesWhenFieldsNotSet() {
FeignClientProperties properties = new FeignClientProperties();
assertThat(properties.isDefaultToProperties()).isTrue();
assertThat(properties.getDefaultConfig()).isEqualTo("default");
assertThat(properties.getConfig()).isEmpty();
assertThat(properties.isDecodeSlash()).isTrue();
}
@Test
void shouldReturnValuesWhenSet() {
FeignClientProperties properties = new FeignClientProperties();
properties.setDefaultToProperties(false);
properties.setDefaultConfig("custom");
Map<String, FeignClientProperties.FeignClientConfiguration> configMap = Maps.newHashMap("foo", null);
properties.setConfig(configMap);
properties.setDecodeSlash(false);
assertThat(properties.isDefaultToProperties()).isFalse();
assertThat(properties.getDefaultConfig()).isEqualTo("custom");
assertThat(properties.getConfig()).isSameAs(configMap);
assertThat(properties.isDecodeSlash()).isFalse();
}
/**
* Sanity-checks equals and hashCode contracts but does not check every variation of
* the fields.
*/
@Test
void shouldHaveSomewhatValidEqualsAndHashCode() {
FeignClientProperties propsOne = new FeignClientProperties();
FeignClientProperties propsTwo = new FeignClientProperties();
FeignClientProperties propsThree = new FeignClientProperties();
FeignClientProperties differentProps = new FeignClientProperties();
differentProps.setDecodeSlash(false);
assertEqualsReflexivity(propsOne);
assertEqualsSymmetricity(propsOne, propsTwo);
assertEqualsSymmetricity(propsOne, differentProps);
assertEqualsSymmetricity(propsOne, 42);
assertEqualsTransitivity(propsOne, propsTwo, propsThree);
assertEqualsConsistency(propsOne, propsTwo);
assertEqualsConsistency(propsOne, differentProps);
assertEqualsConsistency(propsOne, 42);
assertEqualsConsistency(propsOne, null);
assertHashCodeConsistency(propsOne);
assertEqualsAndHashCodeConsistency(propsOne, propsTwo);
}
}

View File

@@ -19,9 +19,11 @@ package org.springframework.cloud.openfeign;
import java.lang.reflect.Field;
import java.util.List;
import feign.Capability;
import feign.Feign;
import feign.Logger;
import feign.RequestInterceptor;
import feign.micrometer.MicrometerCapability;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -40,6 +42,7 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* @author matt king
* @author Jonatan Ivanov
*/
@DirtiesContext
@RunWith(SpringJUnit4ClassRunner.class)
@@ -67,6 +70,10 @@ public class FeignClientUsingConfigurerTest {
List<RequestInterceptor> interceptors = (List) getBuilderValue(builder, "requestInterceptors");
assertThat(interceptors.size()).as("interceptors not set").isEqualTo(3);
assertThat(getBuilderValue(builder, "logLevel")).as("log level not set").isEqualTo(Logger.Level.FULL);
List<Capability> capabilities = (List) getBuilderValue(builder, "capabilities");
assertThat(capabilities).hasSize(2).hasAtLeastOneElementOfType(NoOpCapability.class)
.hasAtLeastOneElementOfType(MicrometerCapability.class);
}
private Object getBuilderValue(Feign.Builder builder, String member) {
@@ -84,9 +91,12 @@ public class FeignClientUsingConfigurerTest {
Feign.Builder builder = factoryBean.feign(context);
List<RequestInterceptor> interceptors = (List) getBuilderValue(builder, "requestInterceptors");
assertThat(interceptors).as("interceptors not set").isEmpty();
assertThat(factoryBean.isInheritParentContext()).as("is inheriting from parent configuration").isFalse();
List<Capability> capabilities = (List) getBuilderValue(builder, "capabilities");
assertThat(capabilities).hasSize(2).hasAtLeastOneElementOfType(NoOpCapability.class)
.hasAtLeastOneElementOfType(MicrometerCapability.class);
}
@Test
@@ -97,6 +107,10 @@ public class FeignClientUsingConfigurerTest {
Feign.Builder builder = factoryBean.feign(context);
assertThat(getBuilderValue(builder, "logLevel")).as("log level not set").isEqualTo(Logger.Level.HEADERS);
List<Capability> capabilities = (List) getBuilderValue(builder, "capabilities");
assertThat(capabilities).hasSize(2).hasAtLeastOneElementOfType(NoOpCapability.class)
.hasAtLeastOneElementOfType(MicrometerCapability.class);
}
@EnableAutoConfiguration
@@ -110,6 +124,11 @@ public class FeignClientUsingConfigurerTest {
};
}
@Bean
public NoOpCapability noOpCapability() {
return new NoOpCapability();
}
}
public static class NoInheritConfiguration {
@@ -119,6 +138,11 @@ public class FeignClientUsingConfigurerTest {
return Logger.Level.HEADERS;
}
@Bean
public NoOpCapability noOpCapability() {
return new NoOpCapability();
}
@Bean
public FeignClientConfigurer feignClientConfigurer() {
return new FeignClientConfigurer() {
@@ -143,4 +167,8 @@ public class FeignClientUsingConfigurerTest {
}
private static class NoOpCapability implements Capability {
}
}

View File

@@ -34,6 +34,8 @@ import java.util.stream.Stream;
import javax.servlet.http.HttpServletRequest;
import feign.Capability;
import feign.Feign;
import feign.InvocationHandlerFactory;
import feign.Request;
import feign.RequestInterceptor;
@@ -43,6 +45,7 @@ import feign.Retryer;
import feign.codec.EncodeException;
import feign.codec.Encoder;
import feign.codec.ErrorDecoder;
import feign.micrometer.MicrometerCapability;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -76,6 +79,7 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
* @author Eko Kurniawan Khannedy
* @author Olga Maciaszek-Sharma
* @author Ilia Ilinykh
* @author Jonatan Ivanov
*/
@SuppressWarnings("FieldMayBeFinal")
@RunWith(SpringJUnit4ClassRunner.class)
@@ -232,8 +236,22 @@ public class FeignClientUsingPropertiesTests {
assertThat(options.readTimeoutMillis()).isEqualTo(5000);
}
@Test
public void clientShouldContainCapabilities() {
fooFactoryBean.setApplicationContext(applicationContext);
Feign.Builder feignBuilder = fooFactoryBean.feign(context);
FooClient fooClient = feignBuilder.target(FooClient.class, "http://localhost:" + port);
String response = fooClient.foo();
assertThat(response).isEqualTo("OK");
List<Capability> capabilities = (List) ReflectionTestUtils.getField(feignBuilder, "capabilities");
assertThat(capabilities).hasSize(2).hasAtLeastOneElementOfType(NoOpCapability.class)
.hasAtLeastOneElementOfType(MicrometerCapability.class);
}
private Request.Options getRequestOptions(Proxy client) {
Object invocationHandler = ReflectionTestUtils.getField(client, "h");
Object invocationHandlerLambda = ReflectionTestUtils.getField(client, "h");
Object invocationHandler = ReflectionTestUtils.getField(invocationHandlerLambda, "arg$2");
Map<Method, InvocationHandlerFactory.MethodHandler> dispatch = (Map<Method, InvocationHandlerFactory.MethodHandler>) ReflectionTestUtils
.getField(Objects.requireNonNull(invocationHandler), "dispatch");
Method key = new ArrayList<>(dispatch.keySet()).get(0);
@@ -385,4 +403,8 @@ public class FeignClientUsingPropertiesTests {
}
public static class NoOpCapability implements Capability {
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2021-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.
* 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;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsAndHashCodeConsistency;
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsConsistency;
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsReflexivity;
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsSymmetricity;
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsTransitivity;
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertHashCodeConsistency;
/**
* @author Jonatan Ivanov
*/
class MetricsPropertiesTests {
@Test
void shouldBeEnabledByDefault() {
FeignClientProperties.MetricsProperties properties = new FeignClientProperties.MetricsProperties();
assertThat(properties.getEnabled()).isTrue();
}
@Test
void shouldBeDisabledWhenSet() {
FeignClientProperties.MetricsProperties properties = new FeignClientProperties.MetricsProperties();
properties.setEnabled(false);
assertThat(properties.getEnabled()).isFalse();
}
/**
* Sanity-checks equals and hashCode contracts but does not check every variation of
* the fields.
*/
@Test
void shouldHaveSomewhatValidEqualsAndHashCode() {
FeignClientProperties.MetricsProperties propertyOne = new FeignClientProperties.MetricsProperties();
FeignClientProperties.MetricsProperties propertyTwo = new FeignClientProperties.MetricsProperties();
FeignClientProperties.MetricsProperties propertyThree = new FeignClientProperties.MetricsProperties();
FeignClientProperties.MetricsProperties differentProperty = new FeignClientProperties.MetricsProperties();
differentProperty.setEnabled(false);
assertEqualsReflexivity(propertyOne);
assertEqualsSymmetricity(propertyOne, propertyTwo);
assertEqualsSymmetricity(propertyOne, differentProperty);
assertEqualsSymmetricity(propertyOne, 42);
assertEqualsTransitivity(propertyOne, propertyTwo, propertyThree);
assertEqualsConsistency(propertyOne, propertyTwo);
assertEqualsConsistency(propertyOne, differentProperty);
assertEqualsConsistency(propertyOne, 42);
assertEqualsConsistency(propertyOne, null);
assertHashCodeConsistency(propertyOne);
assertEqualsAndHashCodeConsistency(propertyOne, propertyTwo);
}
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2021-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.
* 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.test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* A few assertions to sanity-check equals and hashCode contracts:
* https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html See
* {@link Object#equals(Object)} and {@link Object#hashCode()}.
*
* @author Jonatan Ivanov
*/
public class EqualsAndHashCodeAssert {
/**
* Checks if equals is reflexive: for any non-null reference value x, x.equals(x)
* should return true.
* @param object the reference object to check
*/
public static void assertEqualsReflexivity(Object object) {
assertThat(object.equals(object)).isTrue();
}
/**
* Checks if equals is symmetric: for any non-null reference values x and y,
* x.equals(y) should return true if and only if y.equals(x) returns true The user of
* this method should call this at least twice: once with objects that are equal and
* once with objects that are not.
* @param objectOne a reference object to check
* @param objectTwo another reference object to check
*/
public static void assertEqualsSymmetricity(Object objectOne, Object objectTwo) {
assertThat(objectOne.equals(objectTwo)).isEqualTo(objectTwo.equals(objectOne));
}
/**
* Checks if equals is transitive: for any non-null reference values x, y, and z, if
* x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should
* return true.
* @param objectOne a reference object to check
* @param objectTwo another reference object to check
* @param objectThree and the third reference object to check
*/
public static void assertEqualsTransitivity(Object objectOne, Object objectTwo, Object objectThree) {
assertThat(objectOne.equals(objectTwo)).isTrue();
assertThat(objectTwo.equals(objectThree)).isTrue();
assertThat(objectOne.equals(objectThree)).isTrue();
}
/**
* Tries to check if equals is consistent: for any non-null reference values x and y,
* multiple invocations of x.equals(y) consistently return true or consistently return
* false. The user of this method should call this at least twice: once with objects
* that are equal and once with objects that are not.
* @param objectOne a reference object to check
* @param objectTwo another reference object to check
*/
public static void assertEqualsConsistency(Object objectOne, Object objectTwo) {
boolean equality = objectOne.equals(objectTwo);
for (int i = 0; i < 100; i++) {
assertThat(objectOne.equals(objectTwo)).isEqualTo(equality);
}
}
/**
* Tries to check if hashCode is consistent: whenever it is invoked on the same object
* more than once during an execution of a Java application, the hashCode method must
* consistently return the same integer.
* @param object the reference object to check
*/
public static void assertHashCodeConsistency(Object object) {
int hashCode = object.hashCode();
for (int i = 0; i < 100; i++) {
assertThat(object.hashCode()).isEqualTo(hashCode);
}
}
/**
* Checks if equals and hashCode are consistent to each other: if two objects are
* equal according to the equals method, then calling the hashCode method on each of
* the two objects must produce the same integer result.
* @param objectOne a reference object to check
* @param objectTwo another reference object to check
*/
public static void assertEqualsAndHashCodeConsistency(Object objectOne, Object objectTwo) {
assertThat(objectOne.equals(objectTwo)).isTrue();
assertThat(objectOne.hashCode()).isEqualTo(objectTwo.hashCode());
}
}

View File

@@ -34,3 +34,11 @@ feignClient:
methodLevelRequestMappingPath: /hello2
myPlaceholderHeader: myPlaceholderHeaderValue
management.endpoints.web.expose: '*'
---
spring.config.activate.on-profile: no-metrics
feign.metrics.enabled: false
---
spring.config.activate.on-profile: no-foo-metrics
feign.client.config.foo.metrics.enabled: false

View File

@@ -8,6 +8,7 @@ feign.client.config.default.loggerLevel=full
feign.client.config.default.errorDecoder=org.springframework.cloud.openfeign.FeignClientUsingPropertiesTests.DefaultErrorDecoder
feign.client.config.default.retryer=org.springframework.cloud.openfeign.FeignClientUsingPropertiesTests.NoRetryer
feign.client.config.default.decode404=true
feign.client.config.default.capabilities=org.springframework.cloud.openfeign.FeignClientUsingPropertiesTests.NoOpCapability
feign.client.config.foo.requestInterceptors[0]=org.springframework.cloud.openfeign.FeignClientUsingPropertiesTests.FooRequestInterceptor
feign.client.config.foo.requestInterceptors[1]=org.springframework.cloud.openfeign.FeignClientUsingPropertiesTests.BarRequestInterceptor
feign.client.config.singleValue.defaultRequestHeaders[singleValueHeaders]=header