diff --git a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc index 08bf68a1..bb44b774 100644 --- a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc +++ b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc @@ -179,6 +179,7 @@ spring: client: config: feignName: + url: http://remote-service.com connectTimeout: 5000 readTimeout: 5000 loggerLevel: full @@ -794,7 +795,13 @@ spring.cloud.openfeign.autoconfiguration.jackson.enabled=false See `org.springframework.cloud.openfeign.FeignAutoConfiguration.FeignJacksonConfiguration` for details. === 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`. +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. +* A url wrapped under `org.springframework.cloud.openfeign.RefreshableUrl`. This means the URL of Feign client, if defined +with `spring.cloud.openfeign.client.config.{feignName}.url` property, can be refreshed against any Feign client instance. + +You can refresh these properties through `POST /actuator/refresh`. By default, refresh behavior in Feign clients is disabled. Use the following property to enable refresh behavior: [source,java] @@ -851,6 +858,35 @@ Alternatively, you can use `LoadBalancerFeignRequestTransformer.DEFAULT_ORDER` t spring.cloud.loadbalancer.x-forwarded.enabled=true ---- +=== Supported Ways To Provide URL To A Feign Client +You can provide a URL to a Feign client in any of the following ways: + +|=== +|Case |Example |Details + +|The URL is provided in the `@FeignClient` annotation. +|`@FeignClient(name="testClient", url="http://localhost:8081")` +|The URL is resolved from the `url` attribute of the annotation, without load-balancing. + +|The URL is provided in the `@FeignClient` annotation and in the +configuration properties. +|`@FeignClient(name="testClient", url="http://localhost:8081")` and the property defined in `application.yml` as +`spring.cloud.openfeign.client.config.testClient.url=http://localhost:8081` +|The URL is resolved from the `url` attribute of the annotation, without load-balancing. +The URL provided in the configuration properties remains unused. + +|The URL is not provided in the `@FeignClient` annotation but is provided in configuration properties. +| `@FeignClient(name="testClient")` and the property defined in `application.yml` as +`spring.cloud.openfeign.client.config.testClient.url=http://localhost:8081` +|The URL is resolved from configuration properties, without load-balancing. If +`spring.cloud.openfeign.client.refresh-enabled=true`, then the URL defined in configuration properties can be refreshed as described in <>. + +|The URL is neither provided in the `@FeignClient` annotation nor in configuration properties. +|`@FeignClient(name="testClient")` +|The URL is resolved from `name` attribute of annotation, with load balancing. + +|=== + == 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 4e40ea19..d6dbc52b 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 @@ -411,8 +411,7 @@ public class FeignClientFactoryBean FeignContext context = beanFactory != null ? beanFactory.getBean(FeignContext.class) : applicationContext.getBean(FeignContext.class); Feign.Builder builder = feign(context); - - if (!StringUtils.hasText(url)) { + if (!StringUtils.hasText(url) && !isUrlAvailableInConfig(contextId)) { if (LOG.isInfoEnabled()) { LOG.info("For '" + name + "' URL not provided. Will try picking an instance via load-balancing."); @@ -448,7 +447,7 @@ public class FeignClientFactoryBean applyBuildCustomizers(context, builder); Targeter targeter = get(context, Targeter.class); - return (T) targeter.target(this, builder, context, new HardCodedTarget<>(type, name, url)); + return targeter.target(this, builder, context, (HardCodedTarget) resolveTarget(context, contextId, url)); } private String cleanPath() { @@ -467,6 +466,38 @@ public class FeignClientFactoryBean return path; } + private HardCodedTarget resolveTarget(FeignContext context, String contextId, String url) { + if (StringUtils.hasText(url)) { + return new HardCodedTarget(type, name, url); + } + + if (refreshableClient) { + RefreshableUrl refreshableUrl = context.getInstance(contextId, + RefreshableUrl.class.getCanonicalName() + "-" + contextId, RefreshableUrl.class); + if (Objects.nonNull(refreshableUrl) && StringUtils.hasText(refreshableUrl.getUrl())) { + return new RefreshableHardCodedTarget<>(type, name, refreshableUrl); + } + } + FeignClientProperties.FeignClientConfiguration config = findConfigByKey(contextId); + if (Objects.isNull(config) || !StringUtils.hasText(config.getUrl())) { + throw new IllegalStateException( + "Provide Feign client URL either in @FeignClient() or in config properties."); + } + + return new HardCodedTarget(type, name, FeignClientsRegistrar.getUrl(config.getUrl())); + } + + private boolean isUrlAvailableInConfig(String contextId) { + FeignClientProperties.FeignClientConfiguration config = findConfigByKey(contextId); + return Objects.nonNull(config) && StringUtils.hasText(config.getUrl()); + } + + private FeignClientProperties.FeignClientConfiguration findConfigByKey(String configKey) { + FeignClientProperties properties = beanFactory != null ? beanFactory.getBean(FeignClientProperties.class) + : applicationContext.getBean(FeignClientProperties.class); + return properties.getConfig().get(configKey); + } + @Override public Class getObjectType() { return type; diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientProperties.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientProperties.java index ac35d22f..16ae326b 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientProperties.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientProperties.java @@ -42,6 +42,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; * @author Jonatan Ivanov * @author Olga Maciaszek-Sharma * @author Hyeonmin Park + * @author Jasbir Singh */ @ConfigurationProperties("spring.cloud.openfeign.client") public class FeignClientProperties { @@ -147,6 +148,12 @@ public class FeignClientProperties { private Boolean followRedirects; + /** + * Allows setting Feign client host URL. This value will only be taken into + * account if the url is not set in the @FeignClient annotation. + */ + private String url; + public Logger.Level getLoggerLevel() { return loggerLevel; } @@ -283,6 +290,14 @@ public class FeignClientProperties { this.followRedirects = followRedirects; } + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -303,14 +318,14 @@ public class FeignClientProperties { && Objects.equals(defaultQueryParameters, that.defaultQueryParameters) && Objects.equals(capabilities, that.capabilities) && Objects.equals(queryMapEncoder, that.queryMapEncoder) && Objects.equals(metrics, that.metrics) - && Objects.equals(followRedirects, that.followRedirects); + && Objects.equals(followRedirects, that.followRedirects) && Objects.equals(url, that.url); } @Override public int hashCode() { return Objects.hash(loggerLevel, connectTimeout, readTimeout, retryer, errorDecoder, requestInterceptors, dismiss404, encoder, decoder, contract, exceptionPropagationPolicy, defaultQueryParameters, - defaultRequestHeaders, capabilities, queryMapEncoder, metrics, followRedirects); + defaultRequestHeaders, capabilities, queryMapEncoder, metrics, followRedirects, url); } } 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 91c8f534..4f56d621 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 @@ -255,7 +255,8 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers); BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); - registerOptionsBeanDefinition(registry, contextId); + registerRefreshableBeanDefinition(registry, contextId, Request.Options.class, OptionsFactoryBean.class); + registerRefreshableBeanDefinition(registry, contextId, RefreshableUrl.class, RefreshableUrlFactoryBean.class); } private void validate(Map attributes) { @@ -420,16 +421,17 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo } /** - * This method is meant to create {@link Request.Options} beans definition with - * refreshScope. + * This method registers beans definition with refreshScope. * @param registry spring bean definition registry * @param contextId name of feign client + * @param beanType type of bean + * @param factoryBeanType points to a relevant bean factory */ - private void registerOptionsBeanDefinition(BeanDefinitionRegistry registry, String contextId) { + private void registerRefreshableBeanDefinition(BeanDefinitionRegistry registry, String contextId, Class beanType, + Class factoryBeanType) { if (isClientRefreshEnabled()) { - String beanName = Request.Options.class.getCanonicalName() + "-" + contextId; - BeanDefinitionBuilder definitionBuilder = BeanDefinitionBuilder - .genericBeanDefinition(OptionsFactoryBean.class); + String beanName = beanType.getCanonicalName() + "-" + contextId; + BeanDefinitionBuilder definitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(factoryBeanType); definitionBuilder.setScope("refresh"); definitionBuilder.addPropertyValue("contextId", contextId); BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(definitionBuilder.getBeanDefinition(), diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/RefreshableHardCodedTarget.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/RefreshableHardCodedTarget.java new file mode 100644 index 00000000..9846488a --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/RefreshableHardCodedTarget.java @@ -0,0 +1,41 @@ +/* + * 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; + +import feign.Target; + +/** + * This target provides url wrapped under {@link Target}. + * + * @author Jasbir Singh + * @since 4.0.0 + */ +public class RefreshableHardCodedTarget extends Target.HardCodedTarget { + + private RefreshableUrl refreshableUrl; + + public RefreshableHardCodedTarget(Class type, String name, RefreshableUrl refreshableUrl) { + super(type, name, refreshableUrl.getUrl()); + this.refreshableUrl = refreshableUrl; + } + + @Override + public String url() { + return refreshableUrl.getUrl(); + } + +} diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/RefreshableUrl.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/RefreshableUrl.java new file mode 100644 index 00000000..85878ae1 --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/RefreshableUrl.java @@ -0,0 +1,38 @@ +/* + * 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; + +/** + * This class wraps url inside an object so that relevant proxy instance can be created + * using {@link RefreshableUrlFactoryBean}. + * + * @author Jasbir Singh + * @since 4.0.0 + */ +public class RefreshableUrl { + + private final String url; + + public RefreshableUrl(String url) { + this.url = url; + } + + public String getUrl() { + return url; + } + +} diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/RefreshableUrlFactoryBean.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/RefreshableUrlFactoryBean.java new file mode 100644 index 00000000..e4800950 --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/RefreshableUrlFactoryBean.java @@ -0,0 +1,75 @@ +/* + * 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; + +import java.util.Objects; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.util.StringUtils; + +/** + * This factory bean creates {@link RefreshableUrl} instance as per the applicable + * configurations. + * + * @author Jasbir Singh + * @since 4.0.0 + */ +public class RefreshableUrlFactoryBean implements FactoryBean, ApplicationContextAware { + + private ApplicationContext applicationContext; + + private String contextId; + + private RefreshableUrl refreshableUrl; + + @Override + public Class getObjectType() { + return RefreshableUrl.class; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + @Override + public RefreshableUrl getObject() throws Exception { + if (refreshableUrl != null) { + return refreshableUrl; + } + + FeignClientProperties properties = applicationContext.getBean(FeignClientProperties.class); + if (Objects.isNull(properties.getConfig())) { + return new RefreshableUrl(null); + } + FeignClientProperties.FeignClientConfiguration configuration = properties.getConfig().get(contextId); + if (Objects.isNull(configuration) || !StringUtils.hasText(configuration.getUrl())) { + return new RefreshableUrl(null); + } + + refreshableUrl = new RefreshableUrl(FeignClientsRegistrar.getUrl(configuration.getUrl())); + return refreshableUrl; + } + + public void setContextId(String contextId) { + this.contextId = contextId; + } + +} diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/NonRefreshableFeignClientUrlTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/NonRefreshableFeignClientUrlTests.java new file mode 100644 index 00000000..171567f4 --- /dev/null +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/NonRefreshableFeignClientUrlTests.java @@ -0,0 +1,116 @@ +/* + * 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; + +import feign.Target; +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.TestPropertySource; +import org.springframework.web.bind.annotation.GetMapping; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Jasbir Singh + */ +@SpringBootTest +@TestPropertySource("classpath:feign-properties.properties") +@DirtiesContext +class NonRefreshableFeignClientUrlTests { + + @Autowired + private Application.FeignClientWithFixUrl feignClientWithFixUrl; + + @Autowired + private Application.ConfigBasedClient configBasedClient; + + @Autowired + private Application.NameBasedUrlClient nameBasedUrlClient; + + @Test + void shouldInstantiateFeignClientWhenUrlFromFeignClientUrl() { + UrlTestClient.UrlResponseForTests response = feignClientWithFixUrl.fixPath(); + assertThat(response.getUrl()).isEqualTo("http://localhost:8081/fixPath"); + assertThat(response.getTargetType()).isEqualTo(Target.HardCodedTarget.class); + } + + @Test + void shouldInstantiateFeignClientWhenUrlFromFeignClientUrlGivenPreferenceOverProperties() { + UrlTestClient.UrlResponseForTests response = feignClientWithFixUrl.fixPath(); + assertThat(response.getUrl()).isEqualTo("http://localhost:8081/fixPath"); + } + + @Test + public void shouldInstantiateFeignClientWhenUrlFromProperties() { + UrlTestClient.UrlResponseForTests response = configBasedClient.test(); + assertThat(response.getUrl()).isEqualTo("http://localhost:9999/test"); + assertThat(response.getTargetType()).isEqualTo(Target.HardCodedTarget.class); + } + + @Test + void shouldInstantiateFeignClientWhenUrlFromFeignClientName() { + UrlTestClient.UrlResponseForTests response = nameBasedUrlClient.test(); + assertThat(response.getUrl()).isEqualTo("http://nameBasedClient/test"); + assertThat(response.getTargetType()).isEqualTo(Target.HardCodedTarget.class); + } + + @Configuration + @EnableAutoConfiguration + @EnableConfigurationProperties(FeignClientProperties.class) + @EnableFeignClients(clients = { Application.FeignClientWithFixUrl.class, Application.ConfigBasedClient.class, + Application.NameBasedUrlClient.class }) + protected static class Application { + + @Bean + UrlTestClient client() { + return new UrlTestClient(); + } + + @FeignClient(name = "feignClientWithFixUrl", url = "http://localhost:8081") + protected interface FeignClientWithFixUrl { + + @GetMapping("/fixPath") + UrlTestClient.UrlResponseForTests fixPath(); + + } + + @FeignClient(name = "configBasedClient") + protected interface ConfigBasedClient { + + @GetMapping("/test") + UrlTestClient.UrlResponseForTests test(); + + } + + @FeignClient(name = "nameBasedClient") + protected interface NameBasedUrlClient { + + @GetMapping("/test") + UrlTestClient.UrlResponseForTests test(); + + } + + } + +} diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/RefreshableFeignClientUrlTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/RefreshableFeignClientUrlTests.java new file mode 100644 index 00000000..836fa1f6 --- /dev/null +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/RefreshableFeignClientUrlTests.java @@ -0,0 +1,150 @@ +/* + * 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; + +import feign.Target; +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.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 static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Jasbir Singh + */ +@SpringBootTest +@TestPropertySource("classpath:feign-refreshable-properties.properties") +@DirtiesContext +class RefreshableFeignClientUrlTests { + + @Autowired + private ApplicationContext applicationContext; + + @Autowired + private RefreshScope refreshScope; + + @Autowired + private RefreshableFeignClientUrlTests.Application.RefreshableClientWithFixUrl refreshableClientWithFixUrl; + + @Autowired + private RefreshableFeignClientUrlTests.Application.RefreshableUrlClient refreshableUrlClient; + + @Autowired + private Application.RefreshableUrlClientForContextRefreshCase refreshableUrlClientForContextRefreshCase; + + @Autowired + private Application.NameBasedUrlClient nameBasedUrlClient; + + @Autowired + private FeignClientProperties clientProperties; + + @Test + void shouldInstantiateFeignClientWhenUrlFromFeignClientUrl() { + UrlTestClient.UrlResponseForTests response = refreshableClientWithFixUrl.fixPath(); + assertThat(response.getUrl()).isEqualTo("http://localhost:8081/fixPath"); + assertThat(response.getTargetType()).isEqualTo(Target.HardCodedTarget.class); + } + + @Test + void shouldInstantiateFeignClientWhenUrlFromFeignClientUrlGivenPreferenceOverProperties() { + UrlTestClient.UrlResponseForTests response = refreshableClientWithFixUrl.fixPath(); + assertThat(response.getUrl()).isEqualTo("http://localhost:8081/fixPath"); + } + + @Test + public void shouldInstantiateFeignClientWhenUrlFromProperties() { + UrlTestClient.UrlResponseForTests response = refreshableUrlClient.refreshable(); + assertThat(response.getUrl()).isEqualTo("http://localhost:8082/refreshable"); + assertThat(response.getTargetType()).isEqualTo(RefreshableHardCodedTarget.class); + } + + @Test + void shouldInstantiateFeignClientWhenUrlFromPropertiesAndThenUpdateUrlWhenContextRefresh() { + UrlTestClient.UrlResponseForTests response = refreshableUrlClientForContextRefreshCase.refreshable(); + assertThat(response.getUrl()).isEqualTo("http://localhost:8080/refreshable"); + + clientProperties.getConfig().get("refreshableClient").setUrl("http://localhost:8888/"); + refreshScope.refreshAll(); + response = refreshableUrlClient.refreshable(); + assertThat(response.getUrl()).isEqualTo("http://localhost:8888/refreshable"); + } + + @Test + void shouldInstantiateFeignClientWhenUrlFromFeignClientName() { + UrlTestClient.UrlResponseForTests response = nameBasedUrlClient.nonRefreshable(); + assertThat(response.getUrl()).isEqualTo("http://nameBasedClient/nonRefreshable"); + assertThat(response.getTargetType()).isEqualTo(Target.HardCodedTarget.class); + } + + @Configuration + @EnableAutoConfiguration + @EnableConfigurationProperties(FeignClientProperties.class) + @EnableFeignClients(clients = { Application.RefreshableUrlClient.class, Application.NameBasedUrlClient.class, + Application.RefreshableClientWithFixUrl.class, + Application.RefreshableUrlClientForContextRefreshCase.class }) + protected static class Application { + + @Bean + UrlTestClient client() { + return new UrlTestClient(); + } + + @FeignClient(name = "refreshableClientWithFixUrl", url = "http://localhost:8081") + protected interface RefreshableClientWithFixUrl { + + @GetMapping("/fixPath") + UrlTestClient.UrlResponseForTests fixPath(); + + } + + @FeignClient(name = "refreshableClient") + protected interface RefreshableUrlClient { + + @GetMapping("/refreshable") + UrlTestClient.UrlResponseForTests refreshable(); + + } + + @FeignClient(name = "refreshableClientForContextRefreshCase") + protected interface RefreshableUrlClientForContextRefreshCase { + + @GetMapping("/refreshable") + UrlTestClient.UrlResponseForTests refreshable(); + + } + + @FeignClient(name = "nameBasedClient") + protected interface NameBasedUrlClient { + + @GetMapping("/nonRefreshable") + UrlTestClient.UrlResponseForTests nonRefreshable(); + + } + + } + +} diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/UrlTestClient.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/UrlTestClient.java new file mode 100644 index 00000000..5606824c --- /dev/null +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/UrlTestClient.java @@ -0,0 +1,105 @@ +/* + * 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; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +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 UrlTestClient implements Client { + + protected 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(request)) + .build(); + } + + private Map> headers() { + Map> headers = new LinkedHashMap<>(); + headers.put("Content-Type", Collections.singletonList("application/json")); + return headers; + } + + private byte[] prepareResponse(Request request) { + try { + UrlResponseForTests response = new UrlResponseForTests(request.url(), + request.requestTemplate().feignTarget().getClass()); + return mapper.writeValueAsString(response).getBytes(); + } + catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + static class UrlResponseForTests { + + private String url; + + private Class targetType; + + UrlResponseForTests() { + } + + UrlResponseForTests(String url, Class targetType) { + this.url = url; + this.targetType = targetType; + } + + void setUrl(String url) { + this.url = url; + } + + public String getUrl() { + return url; + } + + public Class getTargetType() { + return targetType; + } + + public void setTargetType(Class targetType) { + this.targetType = targetType; + } + + @Override + public String toString() { + return "UrlResponseForTests{" + "url='" + url + '\'' + ", targetType=" + targetType + '}'; + } + + } + +} diff --git a/spring-cloud-openfeign-core/src/test/resources/feign-properties.properties b/spring-cloud-openfeign-core/src/test/resources/feign-properties.properties index 1c5cbf6a..3e7f380e 100644 --- a/spring-cloud-openfeign-core/src/test/resources/feign-properties.properties +++ b/spring-cloud-openfeign-core/src/test/resources/feign-properties.properties @@ -25,3 +25,5 @@ spring.cloud.openfeign.client.config.unwrap.exceptionPropagationPolicy=unwrap spring.cloud.openfeign.client.config.readTimeout.readTimeout=1000 spring.cloud.openfeign.client.config.connectTimeout.connectTimeout=1000 spring.cloud.openfeign.client.config.default.followRedirects=false +spring.cloud.openfeign.client.config.feignClientWithFixUrl.url=http://localhost:8888 +spring.cloud.openfeign.client.config.configBasedClient.url=http://localhost:9999 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 index 5c55048e..e79a9a6e 100644 --- a/spring-cloud-openfeign-core/src/test/resources/feign-refreshable-properties.properties +++ b/spring-cloud-openfeign-core/src/test/resources/feign-refreshable-properties.properties @@ -8,3 +8,6 @@ spring.cloud.openfeign.client.config.default.readTimeout=5000 spring.cloud.openfeign.client.config.default.loggerLevel=full spring.cloud.openfeign.client.config.connectTimeout.connectTimeout=2000 spring.cloud.openfeign.client.config.readTimeout.readTimeout=2000 +spring.cloud.openfeign.client.config.refreshableClient.url=http://localhost:8082 +spring.cloud.openfeign.client.config.refreshableClientWithFixUrl.url=http://localhost:8888 +spring.cloud.openfeign.client.config.refreshableClientForContextRefreshCase.url=http://localhost:8080