From e47255e79d730d4e7067671099fe4f022c9a9a94 Mon Sep 17 00:00:00 2001 From: Jasbir Date: Mon, 19 Apr 2021 21:57:28 +0530 Subject: [PATCH] Make feign.Request.Options as refreshable beans inside Feign clients (#526) --- docs/src/main/asciidoc/_configprops.adoc | 1 + .../main/asciidoc/spring-cloud-openfeign.adoc | 11 ++ .../openfeign/FeignClientFactoryBean.java | 47 ++++- .../openfeign/FeignClientsRegistrar.java | 31 +++ .../cloud/openfeign/FeignContext.java | 5 + .../cloud/openfeign/OptionsFactoryBean.java | 86 ++++++++ ...itional-spring-configuration-metadata.json | 6 + ...FeignClientWithRefreshableOptionsTest.java | 185 ++++++++++++++++++ .../cloud/openfeign/OptionsTestClient.java | 113 +++++++++++ .../feign-refreshable-properties.properties | 10 + 10 files changed, 486 insertions(+), 9 deletions(-) create mode 100644 spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/OptionsFactoryBean.java create mode 100644 spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientWithRefreshableOptionsTest.java create mode 100644 spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/OptionsTestClient.java create mode 100644 spring-cloud-openfeign-core/src/test/resources/feign-refreshable-properties.properties diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/src/main/asciidoc/_configprops.adoc index efbb3227..6501a9c3 100644 --- a/docs/src/main/asciidoc/_configprops.adoc +++ b/docs/src/main/asciidoc/_configprops.adoc @@ -7,6 +7,7 @@ |feign.client.decode-slash | `true` | Feign clients do not encode slash `/` characters by default. To change this behavior, set the `decodeSlash` to `false`. |feign.client.default-config | `default` | |feign.client.default-to-properties | `true` | +|feign.client.refresh-enabled | `false` | Enables options value refresh capability for Feign. |feign.compression.request.enabled | `false` | Enables the request sent by Feign to be compressed. |feign.compression.request.mime-types | `[text/xml, application/xml, application/json]` | The list of supported mime types. |feign.compression.request.min-request-size | `2048` | The minimum threshold content size. diff --git a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc index 7708bc24..3d1b58f3 100644 --- a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc +++ b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc @@ -717,6 +717,17 @@ You may consider enabling Jackson Modules for the support `org.springframework.d feign.autoconfiguration.jackson.enabled=true ---- +=== Spring `@RefreshScope` Support +If Feign client refresh is enabled, each feign client is created with `feign.Request.Options` as a refresh-scoped bean. This means properties such as `connectTimeout` and `readTimeout` can be refreshed against any Feign client instance through `POST /actuator/refresh`. + +By default, refresh behavior in Feign clients is disabled. Use the following property to enable refresh behavior: +[source,java] +---- +feign.client.refresh-enabled=true +---- +TIP: DO NOT annotate the `@FeignClient` interface with the `@RefreshScope` annotation. + + == Configuration properties To see the list of all Spring Cloud OpenFeign related configuration properties please check link:appendix.html[the Appendix page]. diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientFactoryBean.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientFactoryBean.java index 8def71dd..1bc64b15 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientFactoryBean.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientFactoryBean.java @@ -66,6 +66,7 @@ import org.springframework.util.StringUtils; * @author Marcin Grzejszczak * @author Jonatan Ivanov * @author Sam Kruglov + * @author Jasbir Singh */ public class FeignClientFactoryBean implements FactoryBean, InitializingBean, ApplicationContextAware, BeanFactoryAware { @@ -105,6 +106,8 @@ public class FeignClientFactoryBean private boolean followRedirects = new Request.Options().isFollowRedirects(); + private boolean refreshableClient = false; + private final List additionalCustomizers = new ArrayList<>(); @Override @@ -188,6 +191,10 @@ public class FeignClientFactoryBean } } Request.Options options = getInheritedAwareOptional(context, Request.Options.class); + if (options == null) { + options = getOptionsByName(context, contextId); + } + if (options != null) { builder.options(options); readTimeoutMillis = options.readTimeoutMillis(); @@ -231,12 +238,15 @@ public class FeignClientFactoryBean builder.logLevel(config.getLoggerLevel()); } - connectTimeoutMillis = config.getConnectTimeout() != null ? config.getConnectTimeout() : connectTimeoutMillis; - readTimeoutMillis = config.getReadTimeout() != null ? config.getReadTimeout() : readTimeoutMillis; - followRedirects = config.isFollowRedirects() != null ? config.isFollowRedirects() : followRedirects; + if (!refreshableClient) { + connectTimeoutMillis = config.getConnectTimeout() != null ? config.getConnectTimeout() + : connectTimeoutMillis; + readTimeoutMillis = config.getReadTimeout() != null ? config.getReadTimeout() : readTimeoutMillis; + followRedirects = config.isFollowRedirects() != null ? config.isFollowRedirects() : followRedirects; - builder.options(new Request.Options(connectTimeoutMillis, TimeUnit.MILLISECONDS, readTimeoutMillis, - TimeUnit.MILLISECONDS, followRedirects)); + builder.options(new Request.Options(connectTimeoutMillis, TimeUnit.MILLISECONDS, readTimeoutMillis, + TimeUnit.MILLISECONDS, followRedirects)); + } if (config.getRetryer() != null) { Retryer retryer = getOrInstantiate(config.getRetryer()); @@ -342,6 +352,20 @@ public class FeignClientFactoryBean "No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-loadbalancer?"); } + /** + * Meant to get Options bean from context with bean name. + * @param context context of Feign client + * @param contextId name of feign client + * @return returns Options found in context + */ + protected Request.Options getOptionsByName(FeignContext context, String contextId) { + if (refreshableClient) { + return context.getInstance(contextId, Request.Options.class.getCanonicalName() + "-" + contextId, + Request.Options.class); + } + return null; + } + @Override public Object getObject() { return getTarget(); @@ -504,6 +528,10 @@ public class FeignClientFactoryBean this.fallbackFactory = fallbackFactory; } + public void setRefreshableClient(boolean refreshableClient) { + this.refreshableClient = refreshableClient; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -520,13 +548,14 @@ public class FeignClientFactoryBean && Objects.equals(path, that.path) && Objects.equals(type, that.type) && Objects.equals(url, that.url) && Objects.equals(connectTimeoutMillis, that.connectTimeoutMillis) && Objects.equals(readTimeoutMillis, that.readTimeoutMillis) - && Objects.equals(followRedirects, that.followRedirects); + && Objects.equals(followRedirects, that.followRedirects) + && Objects.equals(refreshableClient, that.refreshableClient); } @Override public int hashCode() { return Objects.hash(applicationContext, beanFactory, decode404, inheritParentContext, fallback, fallbackFactory, - name, path, type, url, readTimeoutMillis, connectTimeoutMillis, followRedirects); + name, path, type, url, readTimeoutMillis, connectTimeoutMillis, followRedirects, refreshableClient); } @Override @@ -538,8 +567,8 @@ public class FeignClientFactoryBean .append(", ").append("beanFactory=").append(beanFactory).append(", ").append("fallback=") .append(fallback).append(", ").append("fallbackFactory=").append(fallbackFactory).append("}") .append("connectTimeoutMillis=").append(connectTimeoutMillis).append("}").append("readTimeoutMillis=") - .append(readTimeoutMillis).append("}").append("followRedirects=").append(followRedirects).append("}") - .toString(); + .append(readTimeoutMillis).append("}").append("followRedirects=").append(followRedirects) + .append("refreshableClient=").append(refreshableClient).append("}").toString(); } @Override diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsRegistrar.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsRegistrar.java index 7cd7d75b..c93fbf71 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsRegistrar.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsRegistrar.java @@ -29,6 +29,9 @@ import java.util.List; import java.util.Map; import java.util.Set; +import feign.Request; + +import org.springframework.aop.scope.ScopedProxyUtils; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition; @@ -63,6 +66,7 @@ import org.springframework.util.StringUtils; * @author Michal Domagala * @author Marcin Grzejszczak * @author Olga Maciaszek-Sharma + * @author Jasbir Singh */ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware { @@ -213,6 +217,7 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo factoryBean.setName(name); factoryBean.setContextId(contextId); factoryBean.setType(clazz); + factoryBean.setRefreshableClient(isClientRefreshEnabled()); BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> { factoryBean.setUrl(getUrl(beanFactory, attributes)); factoryBean.setPath(getPath(beanFactory, attributes)); @@ -249,6 +254,8 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers); BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); + + registerOptionsBeanDefinition(registry, contextId); } private void validate(Map attributes) { @@ -408,4 +415,28 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo this.environment = environment; } + /** + * This method is meant to create {@link Request.Options} beans definition with + * refreshScope. + * @param registry spring bean definition registry + * @param contextId name of feign client + */ + private void registerOptionsBeanDefinition(BeanDefinitionRegistry registry, String contextId) { + if (isClientRefreshEnabled()) { + String beanName = Request.Options.class.getCanonicalName() + "-" + contextId; + BeanDefinitionBuilder definitionBuilder = BeanDefinitionBuilder + .genericBeanDefinition(OptionsFactoryBean.class); + definitionBuilder.setScope("refresh"); + definitionBuilder.addPropertyValue("contextId", contextId); + BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(definitionBuilder.getBeanDefinition(), + beanName); + definitionHolder = ScopedProxyUtils.createScopedProxy(definitionHolder, registry, true); + BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry); + } + } + + private boolean isClientRefreshEnabled() { + return environment.getProperty("feign.client.refresh-enabled", Boolean.class, false); + } + } diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignContext.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignContext.java index 8488c0b8..0ed35979 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignContext.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignContext.java @@ -30,6 +30,7 @@ import org.springframework.lang.Nullable; * @author Spencer Gibb * @author Dave Syer * @author Matt King + * @author Jasbir Singh */ public class FeignContext extends NamedContextFactory { @@ -52,4 +53,8 @@ public class FeignContext extends NamedContextFactory return getContext(name).getBeansOfType(type); } + public T getInstance(String contextName, String beanName, Class type) { + return getContext(contextName).getBean(beanName, type); + } + } diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/OptionsFactoryBean.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/OptionsFactoryBean.java new file mode 100644 index 00000000..2b83b822 --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/OptionsFactoryBean.java @@ -0,0 +1,86 @@ +/* + * Copyright 2013-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.Objects; +import java.util.concurrent.TimeUnit; + +import feign.Request; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; + +/** + * This factory bean is meant to create {@link Request.Options} instance as per the + * applicable configurations. + * + * @author Jasbir Singh + */ +public class OptionsFactoryBean implements FactoryBean, ApplicationContextAware { + + private ApplicationContext applicationContext; + + private String contextId; + + private Request.Options options; + + @Override + public Class getObjectType() { + return Request.Options.class; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + @Override + public Request.Options getObject() throws Exception { + if (options != null) { + return options; + } + + options = new Request.Options(); + FeignClientProperties properties = applicationContext.getBean(FeignClientProperties.class); + options = createOptionsWithApplicableValues(properties.getConfig().get(properties.getDefaultConfig()), options); + options = createOptionsWithApplicableValues(properties.getConfig().get(contextId), options); + return options; + } + + public void setContextId(String contextId) { + this.contextId = contextId; + } + + private Request.Options createOptionsWithApplicableValues( + FeignClientProperties.FeignClientConfiguration clientConfiguration, Request.Options options) { + if (Objects.isNull(clientConfiguration)) { + return options; + } + + int connectTimeoutMillis = Objects.nonNull(clientConfiguration.getConnectTimeout()) + ? clientConfiguration.getConnectTimeout() : options.connectTimeoutMillis(); + int readTimeoutMillis = Objects.nonNull(clientConfiguration.getReadTimeout()) + ? clientConfiguration.getReadTimeout() : options.readTimeoutMillis(); + boolean followRedirects = Objects.nonNull(clientConfiguration.isFollowRedirects()) + ? clientConfiguration.isFollowRedirects() : options.isFollowRedirects(); + return new Request.Options(connectTimeoutMillis, TimeUnit.MILLISECONDS, readTimeoutMillis, + TimeUnit.MILLISECONDS, followRedirects); + } + +} diff --git a/spring-cloud-openfeign-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-openfeign-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 0ed20cd1..e97d8015 100644 --- a/spring-cloud-openfeign-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-cloud-openfeign-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -55,6 +55,12 @@ "type": "java.lang.Boolean", "description": "Enables metrics capability for Feign.", "defaultValue": "true" + }, + { + "name": "feign.client.refresh-enabled", + "type": "java.lang.Boolean", + "description": "Enables options value refresh capability for Feign.", + "defaultValue": "false" } ] } diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientWithRefreshableOptionsTest.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientWithRefreshableOptionsTest.java new file mode 100644 index 00000000..4de5a5d1 --- /dev/null +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientWithRefreshableOptionsTest.java @@ -0,0 +1,185 @@ +/* + * Copyright 2013-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.concurrent.TimeUnit; + +import feign.Request; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.context.scope.refresh.RefreshScope; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.context.support.GenericWebApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Jasbir Singh + */ +@SpringBootTest +@TestPropertySource("classpath:feign-refreshable-properties.properties") +@DirtiesContext +public class FeignClientWithRefreshableOptionsTest { + + @Autowired + private ApplicationContext applicationContext; + + @Autowired + private RefreshScope refreshScope; + + @Autowired + private Application.RefreshableClient refreshableClient; + + @Autowired + private Application.ReadTimeoutClient readTimeoutClient; + + @Autowired + private Application.ConnectTimeoutClient connectTimeoutClient; + + @Autowired + private Application.OverrideOptionsClient overrideOptionsClient; + + @Autowired + private FeignClientProperties clientProperties; + + @Test + public void overridedOptionsBeanShouldBePresentInsteadOfRefreshable() { + OptionsTestClient.OptionsResponseForTests options = overrideOptionsClient.override(); + assertConnectionAndReadTimeout(options, 1, 1); + } + + @Test + public void refreshScopeBeanDefinitionShouldBePresent() { + BeanDefinition beanDefinition = ((GenericWebApplicationContext) applicationContext) + .getBeanDefinition(Request.Options.class.getCanonicalName() + "-" + "refreshableClient"); + BeanDefinition originBeanDefinition = beanDefinition.getOriginatingBeanDefinition(); + assertThat(originBeanDefinition.getBeanClassName()).isEqualTo(OptionsFactoryBean.class.getCanonicalName()); + assertThat(originBeanDefinition.getScope()).isEqualTo("refresh"); + } + + @Test + public void withConfigDefaultConnectTimeoutAndReadTimeout() { + OptionsTestClient.OptionsResponseForTests options = refreshableClient.refreshable(); + assertConnectionAndReadTimeout(options, 5000, 5000); + } + + @Test + public void readTimeoutShouldWorkWhenConnectTimeoutNotSet() { + OptionsTestClient.OptionsResponseForTests options = readTimeoutClient.readTimeout(); + assertConnectionAndReadTimeout(options, 5000, 2000); + } + + @Test + public void connectTimeoutShouldWorkWhenReadTimeoutNotSet() { + OptionsTestClient.OptionsResponseForTests options = connectTimeoutClient.connectTimeout(); + assertConnectionAndReadTimeout(options, 2000, 5000); + } + + @Test + public void connectTimeoutShouldNotChangeWithoutContextRefresh() { + OptionsTestClient.OptionsResponseForTests options = connectTimeoutClient.connectTimeout(); + assertConnectionAndReadTimeout(options, 2000, 5000); + + clientProperties.getConfig().get("connectTimeout").setConnectTimeout(5000); + options = connectTimeoutClient.connectTimeout(); + assertConnectionAndReadTimeout(options, 2000, 5000); + } + + @Test + public void connectTimeoutShouldChangeAfterContextRefresh() { + OptionsTestClient.OptionsResponseForTests options = connectTimeoutClient.connectTimeout(); + assertConnectionAndReadTimeout(options, 2000, 5000); + + clientProperties.getConfig().get("connectTimeout").setConnectTimeout(5000); + refreshScope.refreshAll(); + options = connectTimeoutClient.connectTimeout(); + assertConnectionAndReadTimeout(options, 5000, 5000); + } + + private void assertConnectionAndReadTimeout(OptionsTestClient.OptionsResponseForTests options, + int expectedConnectTimeoutInMillis, int expectedReadTimeoutInMillis) { + assertThat(options.getConnectTimeout()).isEqualTo(expectedConnectTimeoutInMillis); + assertThat(options.getReadTimeout()).isEqualTo(expectedReadTimeoutInMillis); + } + + @Configuration + @EnableAutoConfiguration + @EnableConfigurationProperties(FeignClientProperties.class) + @EnableFeignClients(clients = { Application.OverrideOptionsClient.class, Application.RefreshableClient.class, + Application.ReadTimeoutClient.class, Application.ConnectTimeoutClient.class }) + protected static class Application { + + @Bean + OptionsTestClient client() { + return new OptionsTestClient(); + } + + @FeignClient(name = "overrideOptionsClient", configuration = OverrideConfig.class) + protected interface OverrideOptionsClient { + + @GetMapping("/override") + OptionsTestClient.OptionsResponseForTests override(); + + } + + @FeignClient(name = "refreshableClient") + protected interface RefreshableClient { + + @GetMapping("/refreshable") + OptionsTestClient.OptionsResponseForTests refreshable(); + + } + + @FeignClient(name = "readTimeout") + protected interface ReadTimeoutClient { + + @GetMapping("/readTimeout") + OptionsTestClient.OptionsResponseForTests readTimeout(); + + } + + @FeignClient(name = "connectTimeout") + protected interface ConnectTimeoutClient { + + @GetMapping("/connectTimeout") + OptionsTestClient.OptionsResponseForTests connectTimeout(); + + } + + @Configuration + protected class OverrideConfig { + + @Bean + public Request.Options options() { + return new Request.Options(1, TimeUnit.MILLISECONDS, 1, TimeUnit.MILLISECONDS, true); + } + + } + + } + +} diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/OptionsTestClient.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/OptionsTestClient.java new file mode 100644 index 00000000..cb925644 --- /dev/null +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/OptionsTestClient.java @@ -0,0 +1,113 @@ +/* + * Copyright 2013-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.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import feign.Client; +import feign.Request; +import feign.Response; + +/** + * @author Jasbir Singh + */ +public class OptionsTestClient implements Client { + + private static ObjectMapper mapper; + + static { + mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()).configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + @Override + public Response execute(Request request, Request.Options options) throws IOException { + return Response.builder().status(200).request(request).headers(headers()).body(prepareResponse(options)) + .build(); + } + + private Map> headers() { + Map> headers = new LinkedHashMap<>(); + headers.put("Content-Type", Collections.singletonList("application/json")); + return headers; + } + + private byte[] prepareResponse(Request.Options options) { + try { + + OptionsResponseForTests response = new OptionsResponseForTests(options.connectTimeoutMillis(), + TimeUnit.MILLISECONDS, options.readTimeoutMillis(), TimeUnit.MILLISECONDS); + return mapper.writeValueAsString(response).getBytes(); + } + catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + static class OptionsResponseForTests { + + private long connectTimeout; + + private TimeUnit connectTimeoutUnit; + + private long readTimeout; + + private TimeUnit readTimeoutUnit; + + OptionsResponseForTests(long connectTimeout, TimeUnit connectTimeoutUnit, long readTimeout, + TimeUnit readTimeoutUnit) { + this.connectTimeout = connectTimeout; + this.connectTimeoutUnit = connectTimeoutUnit; + this.readTimeout = readTimeout; + this.readTimeoutUnit = readTimeoutUnit; + } + + public long getConnectTimeout() { + return connectTimeout; + } + + public TimeUnit getConnectTimeoutUnit() { + return connectTimeoutUnit; + } + + public long getReadTimeout() { + return readTimeout; + } + + public TimeUnit getReadTimeoutUnit() { + return readTimeoutUnit; + } + + @Override + public String toString() { + return "OptionsResponseForTests{" + "connectTimeout=" + connectTimeout + ", connectTimeoutUnit=" + + connectTimeoutUnit + ", readTimeout=" + readTimeout + ", readTimeoutUnit=" + readTimeoutUnit + + '}'; + } + + } + +} diff --git a/spring-cloud-openfeign-core/src/test/resources/feign-refreshable-properties.properties b/spring-cloud-openfeign-core/src/test/resources/feign-refreshable-properties.properties new file mode 100644 index 00000000..a3f6bd0e --- /dev/null +++ b/spring-cloud-openfeign-core/src/test/resources/feign-refreshable-properties.properties @@ -0,0 +1,10 @@ +# This configuration used by test class FeignClientWithRefreshableOptionsTest +logging.level.org.springframework.cloud.openfeign=debug +feign.client.default-to-properties=true +feign.client.default-config=default +feign.client.refresh-enabled=true +feign.client.config.default.connectTimeout=5000 +feign.client.config.default.readTimeout=5000 +feign.client.config.default.loggerLevel=full +feign.client.config.connectTimeout.connectTimeout=2000 +feign.client.config.readTimeout.readTimeout=2000