diff --git a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc index 464bcf92..1ea15b0c 100644 --- a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc +++ b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc @@ -118,7 +118,7 @@ If none of them is in the classpath, the default feign client is used. The OkHttpClient and ApacheHttpClient feign clients can be used by setting `feign.okhttp.enabled` or `feign.httpclient.enabled` to `true`, respectively, and having them on the classpath. You can customize the HTTP client used by providing a bean of either `org.apache.http.impl.client.CloseableHttpClient` when using Apache or `okhttp3.OkHttpClient` when using OK HTTP. -Spring Cloud Netflix _does not_ provide the following beans by default for feign, but still looks up beans of these types from the application context to create the feign client: +Spring Cloud OpenFeign _does not_ provide the following beans by default for feign, but still looks up beans of these types from the application context to create the feign client: * `Logger.Level` * `Retryer` @@ -232,6 +232,29 @@ public interface BarClient { } ---- +It is also possible to configure FeignClient not to inherit beans from the parent context. +You can do this by overriding the `inheritParentConfiguration()` in a `FeignClientConfigurer` +bean to return `false`: + +[source,java,indent=0] +---- +@Configuration +public class CustomConfiguration{ + +@Bean +public FeignClientConfigurer feignClientConfigurer() { + return new FeignClientConfigurer() { + + @Override + public boolean inheritParentConfiguration() { + return false; + } + }; + + } +} +---- + === Creating Feign Clients Manually In some cases it might be necessary to customize your Feign Clients in a way that is not @@ -277,6 +300,9 @@ NOTE: The Feign `Contract` object defines what annotations and values are valid autowired `Contract` bean provides supports for SpringMVC annotations, instead of the default Feign native annotations. +You can also use the `Builder`to configure FeignClient not to inherit beans from the parent context. +You can do this by overriding calling `inheritParentContext(false)` on the `Builder`. + [[spring-cloud-feign-hystrix]] === Feign Hystrix Support diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientBuilder.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientBuilder.java index 90e84524..7a523768 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientBuilder.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientBuilder.java @@ -67,6 +67,7 @@ public class FeignClientBuilder { this.feignClientFactoryBean.setType(type); this.feignClientFactoryBean.setName(FeignClientsRegistrar.getName(name)); this.feignClientFactoryBean.setContextId(FeignClientsRegistrar.getName(name)); + this.feignClientFactoryBean.setInheritParentContext(true); // preset default values - these values resemble the default values on the // FeignClient annotation this.url("").path("").decode404(false); @@ -92,6 +93,11 @@ public class FeignClientBuilder { return this; } + public Builder inheritParentContext(final boolean inheritParentContext) { + this.feignClientFactoryBean.setInheritParentContext(inheritParentContext); + return this; + } + public Builder fallback(final Class fallback) { FeignClientsRegistrar.validateFallback(fallback); this.feignClientFactoryBean.setFallback(fallback); 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 2696e948..ba17d806 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 @@ -38,6 +38,7 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.cloud.openfeign.clientconfig.FeignClientConfigurer; import org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -72,6 +73,8 @@ public class FeignClientFactoryBean private boolean decode404; + private boolean inheritParentContext = true; + private ApplicationContext applicationContext; private Class fallback = void.class; @@ -118,7 +121,12 @@ public class FeignClientFactoryBean protected void configureFeign(FeignContext context, Feign.Builder builder) { FeignClientProperties properties = this.applicationContext .getBean(FeignClientProperties.class); - if (properties != null) { + + FeignClientConfigurer feignClientConfigurer = getOptional(context, + FeignClientConfigurer.class); + setInheritParentContext(feignClientConfigurer.inheritParentConfiguration()); + + if (properties != null && inheritParentContext) { if (properties.isDefaultToProperties()) { configureUsingConfiguration(context, builder); configureUsingProperties( @@ -143,15 +151,16 @@ public class FeignClientFactoryBean protected void configureUsingConfiguration(FeignContext context, Feign.Builder builder) { - Logger.Level level = getOptional(context, Logger.Level.class); + Logger.Level level = getInheritedAwareOptional(context, Logger.Level.class); if (level != null) { builder.logLevel(level); } - Retryer retryer = getOptional(context, Retryer.class); + Retryer retryer = getInheritedAwareOptional(context, Retryer.class); if (retryer != null) { builder.retryer(retryer); } - ErrorDecoder errorDecoder = getOptional(context, ErrorDecoder.class); + ErrorDecoder errorDecoder = getInheritedAwareOptional(context, + ErrorDecoder.class); if (errorDecoder != null) { builder.errorDecoder(errorDecoder); } @@ -163,24 +172,26 @@ public class FeignClientFactoryBean builder.errorDecoder(factoryErrorDecoder); } } - Request.Options options = getOptional(context, Request.Options.class); + Request.Options options = getInheritedAwareOptional(context, + Request.Options.class); if (options != null) { builder.options(options); } - Map requestInterceptors = context - .getInstances(this.contextId, RequestInterceptor.class); + Map requestInterceptors = getInheritedAwareInstances( + context, RequestInterceptor.class); if (requestInterceptors != null) { builder.requestInterceptors(requestInterceptors.values()); } - QueryMapEncoder queryMapEncoder = getOptional(context, QueryMapEncoder.class); + QueryMapEncoder queryMapEncoder = getInheritedAwareOptional(context, + QueryMapEncoder.class); if (queryMapEncoder != null) { builder.queryMapEncoder(queryMapEncoder); } if (this.decode404) { builder.decode404(); } - ExceptionPropagationPolicy exceptionPropagationPolicy = getOptional(context, - ExceptionPropagationPolicy.class); + ExceptionPropagationPolicy exceptionPropagationPolicy = getInheritedAwareOptional( + context, ExceptionPropagationPolicy.class); if (exceptionPropagationPolicy != null) { builder.exceptionPropagationPolicy(exceptionPropagationPolicy); } @@ -266,6 +277,25 @@ public class FeignClientFactoryBean return context.getInstance(this.contextId, type); } + protected T getInheritedAwareOptional(FeignContext context, Class type) { + if (inheritParentContext) { + return getOptional(context, type); + } + else { + return context.getInstanceWithoutAncestors(this.contextId, type); + } + } + + protected Map getInheritedAwareInstances(FeignContext context, + Class type) { + if (inheritParentContext) { + return context.getInstances(this.contextId, type); + } + else { + return context.getInstancesWithoutAncestors(this.contextId, type); + } + } + protected T loadBalance(Feign.Builder builder, FeignContext context, HardCodedTarget target) { Client client = getOptional(context, Client.class); @@ -393,6 +423,14 @@ public class FeignClientFactoryBean this.decode404 = decode404; } + public boolean isInheritParentContext() { + return inheritParentContext; + } + + public void setInheritParentContext(boolean inheritParentContext) { + this.inheritParentContext = inheritParentContext; + } + public ApplicationContext getApplicationContext() { return this.applicationContext; } @@ -429,6 +467,7 @@ public class FeignClientFactoryBean FeignClientFactoryBean that = (FeignClientFactoryBean) o; return Objects.equals(this.applicationContext, that.applicationContext) && this.decode404 == that.decode404 + && this.inheritParentContext == that.inheritParentContext && Objects.equals(this.fallback, that.fallback) && Objects.equals(this.fallbackFactory, that.fallbackFactory) && Objects.equals(this.name, that.name) @@ -439,8 +478,9 @@ public class FeignClientFactoryBean @Override public int hashCode() { - return Objects.hash(this.applicationContext, this.decode404, this.fallback, - this.fallbackFactory, this.name, this.path, this.type, this.url); + return Objects.hash(this.applicationContext, this.decode404, + this.inheritParentContext, this.fallback, this.fallbackFactory, this.name, + this.path, this.type, this.url); } @Override @@ -449,10 +489,12 @@ public class FeignClientFactoryBean .append(this.type).append(", ").append("name='").append(this.name) .append("', ").append("url='").append(this.url).append("', ") .append("path='").append(this.path).append("', ").append("decode404=") - .append(this.decode404).append(", ").append("applicationContext=") - .append(this.applicationContext).append(", ").append("fallback=") - .append(this.fallback).append(", ").append("fallbackFactory=") - .append(this.fallbackFactory).append("}").toString(); + .append(this.decode404).append(", ").append("inheritParentContext=") + .append(this.inheritParentContext).append(", ") + .append("applicationContext=").append(this.applicationContext) + .append(", ").append("fallback=").append(this.fallback).append(", ") + .append("fallbackFactory=").append(this.fallbackFactory).append("}") + .toString(); } } diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java index 80a507d6..9b1c453c 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java @@ -38,6 +38,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.data.web.SpringDataWebProperties; import org.springframework.boot.autoconfigure.http.HttpMessageConverters; +import org.springframework.cloud.openfeign.clientconfig.FeignClientConfigurer; import org.springframework.cloud.openfeign.support.AbstractFormWriter; import org.springframework.cloud.openfeign.support.PageJacksonModule; import org.springframework.cloud.openfeign.support.PageableSpringEncoder; @@ -150,6 +151,13 @@ public class FeignClientsConfiguration { return new PageJacksonModule(); } + @Bean + @ConditionalOnMissingBean(FeignClientConfigurer.class) + public FeignClientConfigurer feignClientConfigurer() { + return new FeignClientConfigurer() { + }; + } + private Encoder springEncoder(ObjectProvider formWriterProvider) { AbstractFormWriter formWriter = formWriterProvider.getIfAvailable(); 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 235ec8cc..8488c0b8 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 @@ -16,7 +16,12 @@ package org.springframework.cloud.openfeign; +import java.util.Map; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.cloud.context.named.NamedContextFactory; +import org.springframework.lang.Nullable; /** * A factory that creates instances of feign classes. It creates a Spring @@ -24,6 +29,7 @@ import org.springframework.cloud.context.named.NamedContextFactory; * * @author Spencer Gibb * @author Dave Syer + * @author Matt King */ public class FeignContext extends NamedContextFactory { @@ -31,4 +37,19 @@ public class FeignContext extends NamedContextFactory super(FeignClientsConfiguration.class, "feign", "feign.client.name"); } + @Nullable + public T getInstanceWithoutAncestors(String name, Class type) { + try { + return BeanFactoryUtils.beanOfType(getContext(name), type); + } + catch (BeansException ex) { + return null; + } + } + + @Nullable + public Map getInstancesWithoutAncestors(String name, Class type) { + return getContext(name).getBeansOfType(type); + } + } diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/clientconfig/FeignClientConfigurer.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/clientconfig/FeignClientConfigurer.java new file mode 100644 index 00000000..18ef8b18 --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/clientconfig/FeignClientConfigurer.java @@ -0,0 +1,45 @@ +/* + * Copyright 2013-2019 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.clientconfig; + +/** + * Additional Feign Client configuration that are not included in + * {@link org.springframework.cloud.openfeign.FeignClient}. + * + * @author Matt King + */ +public interface FeignClientConfigurer { + + /** + * @return whether to mark the feign proxy as a primary bean. Defaults to true. + */ + default boolean primary() { + return true; + } + + /** + * FALSE will only apply configurations from classes listed in + * configuration(). Will still use parent instance of + * {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, and + * {@link feign.Contract} if none are provided. + * @return weather to inherit parent context for client configuration. + */ + default boolean inheritParentConfiguration() { + return true; + } + +} diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientUsingConfigurerTest.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientUsingConfigurerTest.java new file mode 100644 index 00000000..5b82ddf0 --- /dev/null +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientUsingConfigurerTest.java @@ -0,0 +1,149 @@ +/* + * Copyright 2013-2019 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.lang.reflect.Field; +import java.util.List; + +import feign.Feign; +import feign.Logger; +import feign.RequestInterceptor; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.openfeign.clientconfig.FeignClientConfigurer; +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.junit4.SpringJUnit4ClassRunner; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author matt king + */ +@DirtiesContext +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest(classes = FeignClientUsingConfigurerTest.Application.class, value = { + "feign.client.config.default.loggerLevel=full", + "feign.client.config.default.requestInterceptors[0]=org.springframework.cloud.openfeign.FeignClientUsingPropertiesTests.FooRequestInterceptor", + "feign.client.config.default.requestInterceptors[1]=org.springframework.cloud.openfeign.FeignClientUsingPropertiesTests.BarRequestInterceptor" }) +public class FeignClientUsingConfigurerTest { + + private static final String BEAN_NAME_PREFIX = "&org.springframework.cloud.openfeign.FeignClientUsingConfigurerTest$"; + + @Autowired + private ApplicationContext applicationContext; + + @Autowired + private FeignContext context; + + @Test + public void testFeignClient() { + FeignClientFactoryBean factoryBean = (FeignClientFactoryBean) applicationContext + .getBean(BEAN_NAME_PREFIX + "TestFeignClient"); + Feign.Builder builder = factoryBean.feign(context); + + List 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); + } + + private Object getBuilderValue(Feign.Builder builder, String member) { + Field builderField = ReflectionUtils.findField(Feign.Builder.class, member); + ReflectionUtils.makeAccessible(builderField); + + return ReflectionUtils.getField(builderField, builder); + } + + @Test + public void testNoInheritFeignClient() { + FeignClientFactoryBean factoryBean = (FeignClientFactoryBean) applicationContext + .getBean(BEAN_NAME_PREFIX + "NoInheritFeignClient"); + Feign.Builder builder = factoryBean.feign(context); + + List interceptors = (List) getBuilderValue(builder, + "requestInterceptors"); + + assertThat(interceptors).as("interceptors not set").isEmpty(); + assertThat(factoryBean.isInheritParentContext()) + .as("is inheriting from parent configuration").isFalse(); + } + + @Test + public void testNoInheritFeignClient_ignoreProperties() { + FeignClientFactoryBean factoryBean = (FeignClientFactoryBean) applicationContext + .getBean(BEAN_NAME_PREFIX + "NoInheritFeignClient"); + Feign.Builder builder = factoryBean.feign(context); + + assertThat(getBuilderValue(builder, "logLevel")).as("log level not set") + .isEqualTo(Logger.Level.HEADERS); + } + + @EnableAutoConfiguration + @Configuration(proxyBeanMethods = false) + @EnableFeignClients(clients = { TestFeignClient.class, NoInheritFeignClient.class }) + protected static class Application { + + @Bean + public RequestInterceptor requestInterceptor() { + return requestTemplate -> { + }; + } + + } + + public static class NoInheritConfiguration { + + @Bean + public Logger.Level logLevel() { + return Logger.Level.HEADERS; + } + + @Bean + public FeignClientConfigurer feignClientConfigurer() { + return new FeignClientConfigurer() { + + @Override + public boolean inheritParentConfiguration() { + return false; + } + }; + + } + + } + + @FeignClient("testFeignClient") + interface TestFeignClient { + + } + + @FeignClient(name = "noInheritFeignClient", + configuration = NoInheritConfiguration.class) + interface NoInheritFeignClient { + + } + +} diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignContextTest.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignContextTest.java new file mode 100644 index 00000000..e2f9b60e --- /dev/null +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignContextTest.java @@ -0,0 +1,126 @@ +/* + * Copyright 2013-2019 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 feign.Logger; +import feign.RequestInterceptor; +import org.assertj.core.util.Lists; +import org.junit.Test; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +import static org.assertj.core.api.Assertions.assertThat; + +public class FeignContextTest { + + @Test + public void getInstanceWithoutAncestors_verifyNullForMissing() { + AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext(); + parent.refresh(); + + FeignContext feignContext = new FeignContext(); + feignContext.setApplicationContext(parent); + feignContext.setConfigurations( + Lists.newArrayList(getSpec("empty", EmptyConfiguration.class))); + + Logger.Level level = feignContext.getInstanceWithoutAncestors("empty", + Logger.Level.class); + + assertThat(level).as("Logger was not null").isNull(); + } + + private FeignClientSpecification getSpec(String name, Class configClass) { + return new FeignClientSpecification(name, new Class[] { configClass }); + } + + @Test + public void getInstancesWithoutAncestors_verifyEmptyForMissing() { + AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext(); + parent.refresh(); + + FeignContext feignContext = new FeignContext(); + feignContext.setApplicationContext(parent); + feignContext.setConfigurations( + Lists.newArrayList(getSpec("empty", EmptyConfiguration.class))); + + Collection interceptors = feignContext + .getInstancesWithoutAncestors("empty", RequestInterceptor.class).values(); + + assertThat(interceptors).as("Interceptors is not empty").isEmpty(); + } + + @Test + public void getInstanceWithoutAncestors() { + AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext(); + parent.refresh(); + + FeignContext feignContext = new FeignContext(); + feignContext.setApplicationContext(parent); + feignContext.setConfigurations( + Lists.newArrayList(getSpec("demo", DemoConfiguration.class))); + + Logger.Level level = feignContext.getInstanceWithoutAncestors("demo", + Logger.Level.class); + + assertThat(level).isEqualTo(Logger.Level.FULL); + } + + @Test + public void getInstancesWithoutAncestors() { + AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext(); + parent.refresh(); + + FeignContext feignContext = new FeignContext(); + feignContext.setApplicationContext(parent); + feignContext.setConfigurations( + Lists.newArrayList(getSpec("demo", DemoConfiguration.class))); + + Collection interceptors = feignContext + .getInstancesWithoutAncestors("demo", RequestInterceptor.class).values(); + + assertThat(interceptors.size()).isEqualTo(1); + } + + @Configuration(proxyBeanMethods = false) + @Import(FeignClientsConfiguration.class) + protected static class EmptyConfiguration { + + } + + @Configuration(proxyBeanMethods = false) + @Import(FeignClientsConfiguration.class) + protected static class DemoConfiguration { + + @Bean + public Logger.Level loggerLevel() { + return Logger.Level.FULL; + } + + @Bean + public RequestInterceptor requestInterceptor() { + return (requestTemplate) -> { + }; + } + + } + +}