diff --git a/docs/src/main/asciidoc/spring-cloud-netflix.adoc b/docs/src/main/asciidoc/spring-cloud-netflix.adoc index 08b60f9f..6e435916 100644 --- a/docs/src/main/asciidoc/spring-cloud-netflix.adoc +++ b/docs/src/main/asciidoc/spring-cloud-netflix.adoc @@ -767,6 +767,67 @@ don't want to use Eureka, you can simply configure a list of servers in your external configuration (see <>). +[[spring-cloud-feign-overriding-defaults]] +=== Overriding Feign Defaults + +A central concept in Spring Cloud's Feign support is that of the named client. Each feign client is part of an ensemble of components that work together to contact a remote server on demand, and the ensemble has a name that you give it as an application developer using the `@FeignClient` annotation. Spring Cloud creates a new ensemble as an +`ApplicationContext` on demand for each named client using `FeignClientsConfiguration`. This contains (amongst other things) an `feign.Decoder`, a `feign.Encoder`, and a `feign.Contract`. + +Spring Cloud lets you take full control of the feign client by declaring additional configuration (on top of the `FeignClientsConfiguration`) using `@FeignClient`. Example: + +[source,java,indent=0] +---- + @FeignClient(name = "stores", configuration = FooConfiguration.class) + public interface StoreClient { + //.. + } +---- + +In this case the client is composed from the components already in `FeignClientsConfiguration` together with any in `FooConfiguration` (where the latter will override the former). + +WARNING: The `FooConfiguration` has to be `@Configuration` but take care that it is not in a `@ComponentScan` for the main application context, otherwise it will be shared by all the `@FeignClient`s. If you use `@ComponentScan` (or `@SpringBootApplication`) you need to take steps to avoid it being included (for instance put it in a separate, non-overlapping package, or specify the packages to scan explicitly in the `@ComponentScan`). + +NOTE: The `serviceId` attribute is now deprecated in favor of the `name` attribute. + +WARNING: Previously, using the `url` attribute, did not require the `name` attribute. Using `name` is now required. + +Spring Cloud Netflix provides the following beans by default for feign (`BeanType` beanName: `ClassName`): + +* `Decoder` feignDecoder: `ResponseEntityDecoder` (which wraps a `SpringDecoder`) +* `Encoder` feignEncoder: `SpringEncoder` +* `Logger` feignLogger: `Slf4jLogger` +* `Contract` feignContract: `SpringMvcContract` + +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: + +* `Logger.Level` +* `Retryer` +* `ErrorDecoder` +* `Request.Options` +* `Collection` + +Creating a bean of one of those type and placing it in a `@FeignClient` configuration (such as `FooConfiguration` above) allows you to override each one of the beans described. Example: + +[source,java,indent=0] +---- + @Configuration + public class FooConfiguration { + @Bean + public Contract feignContractg() { + return new feign.Contract.Default(); + } + + @Bean + public BasicAuthRequestInterceptor basicAuthRequestInterceptor() { + return new BasicAuthRequestInterceptor("user", "password"); + } + } +---- + +This replaces the `SpringMvcContract` with `feign.Contract.Default` and adds a `RequestInterceptor` to the collection of `RequestInterceptor`. + +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. + [[spring-cloud-feign-inheritance]] === Feign Inheritance Support diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/EnableFeignClients.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/EnableFeignClients.java index 6332a49e..32029a73 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/EnableFeignClients.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/EnableFeignClients.java @@ -37,7 +37,7 @@ import org.springframework.context.annotation.Import; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented -@Import({ FeignClientsConfiguration.class, FeignClientsRegistrar.class }) +@Import(FeignClientsRegistrar.class) public @interface EnableFeignClients { /** @@ -71,4 +71,18 @@ public @interface EnableFeignClients { */ Class[] basePackageClasses() default {}; + /** + * A custom @Configuration for all feign clients. Can contain override + * @Bean definition for the pieces that make up the client, for instance + * {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, {@link feign.Contract}. + * + * @see FeignClientsConfiguration for the defaults + */ + Class[] defaultConfiguration() default {}; + + /** + * List of classes annotated with @FeignClient. If not empty, disables classpath scanning. + * @return + */ + Class[] clients() default {}; } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignAutoConfiguration.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignAutoConfiguration.java index 6c4cd301..d8c470ff 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignAutoConfiguration.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignAutoConfiguration.java @@ -16,13 +16,14 @@ package org.springframework.cloud.netflix.feign; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.cloud.client.actuator.HasFeatures; -import org.springframework.cloud.netflix.archaius.ArchaiusAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; import feign.Feign; @@ -32,12 +33,20 @@ import feign.Feign; */ @Configuration @ConditionalOnClass(Feign.class) -@AutoConfigureAfter(ArchaiusAutoConfiguration.class) -@Import(FeignClientsConfiguration.class) public class FeignAutoConfiguration { + @Autowired(required = false) + private List configurations = new ArrayList<>(); + @Bean public HasFeatures feignFeature() { return HasFeatures.namedFeature("Feign", Feign.class); } + + @Bean + public FeignClientFactory feignClientFactory() { + FeignClientFactory factory = new FeignClientFactory(); + factory.setConfigurations(this.configurations); + return factory; + } } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClient.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClient.java index 46b28fa0..e6de8a2f 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClient.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClient.java @@ -22,6 +22,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.core.annotation.AliasFor; + /** * Annotation for interfaces declaring that a REST client with that interface should be * created (e.g. for autowiring into another component). If ribbon is available it will be @@ -39,17 +41,30 @@ public @interface FeignClient { * The serviceId with optional protocol prefix. Synonym for {@link #serviceId() * serviceId}. Either serviceId or url must be specified but not both. */ + @AliasFor("name") String value() default ""; /** * The serviceId with optional protocol prefix. Synonym for {@link #value() value}. * Either serviceId or url must be specified but not both. */ + @Deprecated String serviceId() default ""; + @AliasFor("value") + String name() default ""; + /** * An absolute URL or resolvable hostname (the protocol is optional). */ String url() default ""; + /** + * A custom @Configuration for the feign client. Can contain override + * @Bean definition for the pieces that make up the client, for instance + * {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, {@link feign.Contract}. + * + * @see FeignClientsConfiguration for the defaults + */ + Class[] configuration() default {}; } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientFactory.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientFactory.java new file mode 100644 index 00000000..590e4550 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientFactory.java @@ -0,0 +1,120 @@ +/* + * Copyright 2013-2015 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 + * + * http://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.netflix.feign; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.core.env.MapPropertySource; + +/** + * A factory that creates client, load balancer and client configuration instances. It + * creates a Spring ApplicationContext per client name, and extracts the beans that it + * needs from there. + * + * @author Spencer Gibb + * @author Dave Syer + */ +public class FeignClientFactory implements DisposableBean, ApplicationContextAware { + + private Map contexts = new ConcurrentHashMap<>(); + + private Map configurations = new ConcurrentHashMap<>(); + + private ApplicationContext parent; + + @Override + public void setApplicationContext(ApplicationContext parent) throws BeansException { + this.parent = parent; + } + + public void setConfigurations(List configurations) { + for (FeignClientSpecification client : configurations) { + this.configurations.put(client.getName(), client); + } + } + + @Override + public void destroy() { + Collection values = this.contexts.values(); + this.contexts.clear(); + for (AnnotationConfigApplicationContext context : values) { + context.close(); + } + } + + private AnnotationConfigApplicationContext getContext(String name) { + if (!this.contexts.containsKey(name)) { + synchronized (this.contexts) { + if (!this.contexts.containsKey(name)) { + this.contexts.put(name, createContext(name)); + } + } + } + return this.contexts.get(name); + } + + private AnnotationConfigApplicationContext createContext(String name) { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + if (this.configurations.containsKey(name)) { + for (Class configuration : this.configurations.get(name) + .getConfiguration()) { + context.register(configuration); + } + } + for (Entry entry : this.configurations + .entrySet()) { + if (entry.getKey().startsWith("default.")) { + for (Class configuration : entry.getValue().getConfiguration()) { + context.register(configuration); + } + } + } + context.register(PropertyPlaceholderAutoConfiguration.class, + FeignClientsConfiguration.class); + context.getEnvironment().getPropertySources() + .addFirst(new MapPropertySource("feign", + Collections. singletonMap( + "feign.client.name", name))); + if (this.parent != null) { + // Uses Environment from parent as well as beans + context.setParent(this.parent); + } + context.refresh(); + return context; + } + + public C getInstance(String name, Class type) { + AnnotationConfigApplicationContext context = getContext(name); + if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, type).length > 0) { + return context.getBean(type); + } + return null; + } + +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientFactoryBean.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientFactoryBean.java index 7661f110..9ef39df6 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientFactoryBean.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientFactoryBean.java @@ -24,7 +24,6 @@ import lombok.EqualsAndHashCode; 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.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.util.Assert; @@ -59,10 +58,7 @@ class FeignClientFactoryBean implements FactoryBean, InitializingBean, A @Override public void afterPropertiesSet() throws Exception { - if (StringUtils.hasText(this.name)) { - Assert.state(!StringUtils.hasText(this.url), - "Either value or url can be specified, but not both"); - } + Assert.hasText(this.name, "Name must be set"); } @Override @@ -70,30 +66,30 @@ class FeignClientFactoryBean implements FactoryBean, InitializingBean, A this.context = context; } - protected Feign.Builder feign() { + protected Feign.Builder feign(FeignClientFactory factory) { // @formatter:off Feign.Builder builder = Feign.builder() // required values - .logger(get(Logger.class)) - .encoder(get(Encoder.class)) - .decoder(get(Decoder.class)) - .contract(get(Contract.class)); + .logger(get(factory, Logger.class)) + .encoder(get(factory, Encoder.class)) + .decoder(get(factory, Decoder.class)) + .contract(get(factory, Contract.class)); // @formatter:on // optional values - Logger.Level level = getOptional(Logger.Level.class); + Logger.Level level = getOptional(factory, Logger.Level.class); if (level != null) { builder.logLevel(level); } - Retryer retryer = getOptional(Retryer.class); + Retryer retryer = getOptional(factory, Retryer.class); if (retryer != null) { builder.retryer(retryer); } - ErrorDecoder errorDecoder = getOptional(ErrorDecoder.class); + ErrorDecoder errorDecoder = getOptional(factory, ErrorDecoder.class); if (errorDecoder != null) { builder.errorDecoder(errorDecoder); } - Request.Options options = getOptional(Request.Options.class); + Request.Options options = getOptional(factory, Request.Options.class); if (options != null) { builder.options(options); } @@ -105,24 +101,23 @@ class FeignClientFactoryBean implements FactoryBean, InitializingBean, A return builder; } - protected T get(Class type) { - return this.context.getBean(type); - } - - protected T getOptional(Class type) { - try { - return this.context.getBean(type); - } catch (NoSuchBeanDefinitionException e) { - //ignore + protected T get(FeignClientFactory factory, Class type) { + T instance = factory.getInstance(this.name, type); + if (instance == null) { + throw new IllegalStateException("No bean found of type " + type + " for " + this.name); } - return null; + return instance; } - protected T loadBalance(Feign.Builder builder, Class type, String schemeName) { + protected T getOptional(FeignClientFactory factory, Class type) { + return factory.getInstance(this.name, type); + } + + protected T loadBalance(Feign.Builder builder, FeignClientFactory factory, Class type, String url) { builder.logger(new Slf4jLogger(type)); // TODO: how to have choice here? - Client client = getOptional(Client.class); + Client client = getOptional(factory, Client.class); if (client != null) { - return builder.client(client).target(type, schemeName); + return builder.client(client).target(type, url); } throw new IllegalStateException("No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-ribbon?"); @@ -130,16 +125,21 @@ class FeignClientFactoryBean implements FactoryBean, InitializingBean, A @Override public Object getObject() throws Exception { - if (StringUtils.hasText(this.name) && !this.name.startsWith("http")) { - this.name = "http://" + this.name; - } - if (StringUtils.hasText(this.name)) { - return loadBalance(feign(), this.type, this.name); + FeignClientFactory factory = context.getBean(FeignClientFactory.class); + + if (!StringUtils.hasText(this.url)) { + String url; + if (!this.name.startsWith("http")) { + url = "http://" + this.name; + } else { + url = this.name; + } + return loadBalance(feign(factory), factory, this.type, url); } if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) { this.url = "http://" + this.url; } - return feign().target(this.type, this.url); + return feign(factory).target(this.type, this.url); } @Override diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientSpecification.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientSpecification.java new file mode 100644 index 00000000..955dde42 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientSpecification.java @@ -0,0 +1,35 @@ +/* + * Copyright 2013-2015 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 + * + * http://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.netflix.feign; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author Dave Syer + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class FeignClientSpecification { + + private String name; + + private Class[] configuration; + +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientsConfiguration.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientsConfiguration.java index 8ccba6ee..f7c3c40c 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientsConfiguration.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientsConfiguration.java @@ -47,21 +47,25 @@ public class FeignClientsConfiguration { private ObjectFactory messageConverters; @Bean + @ConditionalOnMissingBean public Decoder feignDecoder() { return new ResponseEntityDecoder(new SpringDecoder(messageConverters)); } @Bean + @ConditionalOnMissingBean public Encoder feignEncoder() { return new SpringEncoder(messageConverters); } @Bean + @ConditionalOnMissingBean public Logger feignLogger() { return new Slf4jLogger(); } @Bean + @ConditionalOnMissingBean public Contract feignContract() { return new SpringMvcContract(); } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientsRegistrar.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientsRegistrar.java index 484eced9..4fdb8529 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientsRegistrar.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientsRegistrar.java @@ -16,10 +16,13 @@ package org.springframework.cloud.netflix.feign; +import java.io.IOException; import java.lang.annotation.Annotation; import java.net.URI; import java.net.URISyntaxException; +import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -36,7 +39,12 @@ import org.springframework.context.annotation.ClassPathScanningCandidateComponen import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.core.io.ResourceLoader; import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.ClassMetadata; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.filter.AbstractClassTestingTypeFilter; import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.core.type.filter.TypeFilter; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -49,6 +57,7 @@ public class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, BeanClassLoaderAware { // patterned after Spring Integration IntegrationComponentScanRegistrar + // and RibbonClientsConfigurationRegistgrar private ResourceLoader resourceLoader; @@ -68,15 +77,59 @@ public class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, } @Override - public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, + public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { + registerDefaultConfiguration(metadata, registry); + registerFeignClients(metadata, registry); + } - Set basePackages = getBasePackages(importingClassMetadata); + private void registerDefaultConfiguration(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { + Map defaultAttrs = metadata.getAnnotationAttributes( + EnableFeignClients.class.getName(), true); + if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) { + String name; + if (metadata.hasEnclosingClass()) { + name = "default." + metadata.getEnclosingClassName(); + } else { + name = "default." + metadata.getClassName(); + } + registerClientConfiguration(registry, name, + defaultAttrs.get("defaultConfiguration")); + } + } + + public void registerFeignClients(AnnotationMetadata metadata, + BeanDefinitionRegistry registry) { ClassPathScanningCandidateComponentProvider scanner = getScanner(); - scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class)); scanner.setResourceLoader(this.resourceLoader); + Set basePackages; + + Map attrs = metadata.getAnnotationAttributes( + EnableFeignClients.class.getName()); + AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(FeignClient.class); + if (attrs != null && attrs.containsKey("clients")) { + final Class[] clients = (Class[]) attrs.get("clients"); + final Set clientClasses = new HashSet<>(); + basePackages = new HashSet<>(); + for (Class clazz : clients) { + basePackages.add(ClassUtils.getPackageName(clazz)); + clientClasses.add(clazz.getCanonicalName()); + } + AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() { + @Override + protected boolean match(ClassMetadata metadata) { + String cleaned = metadata.getClassName().replaceAll("\\$", "."); + return clientClasses.contains(cleaned); + } + }; + scanner.addIncludeFilter(new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter))); + } else { + scanner.addIncludeFilter(annotationTypeFilter); + basePackages = getBasePackages(metadata); + } + for (String basePackage : basePackages) { Set candidateComponents = scanner .findCandidateComponents(basePackage); @@ -88,18 +141,21 @@ public class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, Assert.isTrue(annotationMetadata.isInterface(), "@FeignClient can only be specified on an interface"); - BeanDefinitionHolder holder = createBeanDefinition(annotationMetadata); - BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); + Map attributes = annotationMetadata + .getAnnotationAttributes(FeignClient.class.getCanonicalName()); + + String name = getClientName(attributes); + registerClientConfiguration(registry, name, attributes.get("configuration")); + + registerFeignClient(registry, annotationMetadata, attributes); } } } } - private BeanDefinitionHolder createBeanDefinition( - AnnotationMetadata annotationMetadata) { - Map attributes = annotationMetadata - .getAnnotationAttributes(FeignClient.class.getCanonicalName()); - + private void registerFeignClient( BeanDefinitionRegistry registry, + AnnotationMetadata annotationMetadata, + Map attributes) { String className = annotationMetadata.getClassName(); BeanDefinitionBuilder definition = BeanDefinitionBuilder .genericBeanDefinition(FeignClientFactoryBean.class); @@ -111,7 +167,8 @@ public class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, String beanName = StringUtils.uncapitalize(className.substring(className .lastIndexOf(".") + 1)); - return new BeanDefinitionHolder(definition.getBeanDefinition(), beanName); + BeanDefinitionHolder holder = new BeanDefinitionHolder(definition.getBeanDefinition(), beanName); + BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); } private void validate(Map attributes) { @@ -123,6 +180,9 @@ public class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, private String getServiceId(Map attributes) { String name = (String) attributes.get("serviceId"); + if (!StringUtils.hasText(name)) { + name = (String) attributes.get("name"); + } if (!StringUtils.hasText(name)) { name = (String) attributes.get("value"); } @@ -153,10 +213,10 @@ public class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, if (beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().getInterfaceNames().length == 1 && Annotation.class.getName().equals( - beanDefinition.getMetadata().getInterfaceNames()[0])) { + beanDefinition.getMetadata().getInterfaceNames()[0])) { try { Class target = ClassUtils.forName(beanDefinition - .getMetadata().getClassName(), + .getMetadata().getClassName(), FeignClientsRegistrar.this.classLoader); return !target.isAnnotation(); } @@ -200,4 +260,68 @@ public class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, return basePackages; } + private String getClientName(Map client) { + if (client == null) { + return null; + } + String value = (String) client.get("value"); + if (!StringUtils.hasText(value)) { + value = (String) client.get("name"); + } + if (!StringUtils.hasText(value)) { + value = (String) client.get("serviceId"); + } + if (StringUtils.hasText(value)) { + return value; + } + + throw new IllegalStateException( + "Either 'name' or 'value' must be provided in @" + FeignClient.class.getSimpleName()); + } + + private void registerClientConfiguration(BeanDefinitionRegistry registry, + Object name, Object configuration) { + BeanDefinitionBuilder builder = BeanDefinitionBuilder + .genericBeanDefinition(FeignClientSpecification.class); + builder.addConstructorArgValue(name); + builder.addConstructorArgValue(configuration); + registry.registerBeanDefinition(name + "." + FeignClientSpecification.class.getSimpleName(), + builder.getBeanDefinition()); + } + + /** + * Helper class to create a {@link TypeFilter} that matches if all the delegates match. + * + * @author Oliver Gierke + */ + private static class AllTypeFilter implements TypeFilter { + + private final List delegates; + + /** + * Creates a new {@link AllTypeFilter} to match if all the given delegates match. + * + * @param delegates must not be {@literal null}. + */ + public AllTypeFilter(List delegates) { + + Assert.notNull(delegates); + this.delegates = delegates; + } + + /* + * (non-Javadoc) + * @see org.springframework.core.type.filter.TypeFilter#match(org.springframework.core.type.classreading.MetadataReader, org.springframework.core.type.classreading.MetadataReaderFactory) + */ + public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException { + + for (TypeFilter filter : delegates) { + if (!filter.match(metadataReader, metadataReaderFactory)) { + return false; + } + } + + return true; + } + } } diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/EnableFeignClientsTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/EnableFeignClientsTests.java new file mode 100644 index 00000000..ccc7a610 --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/EnableFeignClientsTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2013-2015 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 + * + * http://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.netflix.feign; + +import feign.Contract; +import feign.Logger; +import feign.codec.Decoder; +import feign.codec.Encoder; +import feign.slf4j.Slf4jLogger; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.cloud.netflix.archaius.ArchaiusAutoConfiguration; +import org.springframework.cloud.netflix.feign.support.ResponseEntityDecoder; +import org.springframework.cloud.netflix.feign.support.SpringEncoder; +import org.springframework.cloud.netflix.feign.support.SpringMvcContract; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Spencer Gibb + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = EnableFeignClientsTests.PlainConfiguration.class) +@DirtiesContext +public class EnableFeignClientsTests { + + @Autowired + private FeignClientFactory factory; + + @Autowired + private ApplicationContext context; + + @Test + public void decoderDefaultCorrect() { + ResponseEntityDecoder.class.cast(this.factory.getInstance("foo", Decoder.class)); + } + + @Test + public void encoderDefaultCorrect() { + SpringEncoder.class.cast(this.factory.getInstance("foo", Encoder.class)); + } + + @Test + public void loggerDefaultCorrect() { + Slf4jLogger.class.cast(this.factory.getInstance("foo", Logger.class)); + } + + @Test + public void contractDefaultCorrect() { + SpringMvcContract.class.cast(this.factory.getInstance("foo", Contract.class)); + } + + @Configuration + @Import({ PropertyPlaceholderAutoConfiguration.class, + ArchaiusAutoConfiguration.class, FeignAutoConfiguration.class }) + protected static class PlainConfiguration { + } + +} diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/FeignClientFactoryTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/FeignClientFactoryTests.java new file mode 100644 index 00000000..225f07f3 --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/FeignClientFactoryTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2013-2015 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 + * + * http://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.netflix.feign; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; + +import java.util.Arrays; + +/** + * @author Spencer Gibb + */ +public class FeignClientFactoryTests { + + @Test + public void testChildContexts() { + AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext(); + parent.refresh(); + FeignClientFactory factory = new FeignClientFactory(); + factory.setApplicationContext(parent); + factory.setConfigurations(Arrays.asList(getSpec("foo", FooConfig.class), + getSpec("bar", BarConfig.class))); + + Foo foo = factory.getInstance("foo", Foo.class); + assertThat("foo was null", foo, is(notNullValue())); + + Bar bar = factory.getInstance("bar", Bar.class); + assertThat("bar was null", bar, is(notNullValue())); + + Bar foobar = factory.getInstance("foo", Bar.class); + assertThat("bar was not null", foobar, is(nullValue())); + } + + private FeignClientSpecification getSpec(String name, Class configClass) { + return new FeignClientSpecification(name, new Class[]{configClass}); + } + + static class FooConfig { + @Bean + Foo foo() { + return new Foo(); + } + + } + static class Foo{} + + static class BarConfig { + @Bean + Bar bar() { + return new Bar(); + } + } + static class Bar{} +} diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/FeignClientOverrideDefaultsTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/FeignClientOverrideDefaultsTests.java new file mode 100644 index 00000000..a1b5316d --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/FeignClientOverrideDefaultsTests.java @@ -0,0 +1,184 @@ +/* + * Copyright 2013-2015 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 + * + * http://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.netflix.feign; + +import static org.junit.Assert.*; + +import feign.Contract; +import feign.Logger; +import feign.Request; +import feign.RequestInterceptor; +import feign.Retryer; +import feign.auth.BasicAuthRequestInterceptor; +import feign.codec.Decoder; +import feign.codec.Encoder; +import feign.codec.ErrorDecoder; +import feign.slf4j.Slf4jLogger; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.cloud.netflix.archaius.ArchaiusAutoConfiguration; +import org.springframework.cloud.netflix.feign.support.ResponseEntityDecoder; +import org.springframework.cloud.netflix.feign.support.SpringEncoder; +import org.springframework.cloud.netflix.feign.support.SpringMvcContract; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.web.bind.annotation.RequestMapping; + +/** + * @author Spencer Gibb + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = FeignClientOverrideDefaultsTests.TestConfiguration.class) +@DirtiesContext +public class FeignClientOverrideDefaultsTests { + + @Autowired + private FeignClientFactory factory; + + @Test + public void overrideDecoder() { + Decoder.Default.class.cast(factory.getInstance("foo", Decoder.class)); + ResponseEntityDecoder.class.cast(factory.getInstance("bar", Decoder.class)); + } + + @Test + public void overrideEncoder() { + Encoder.Default.class.cast(factory.getInstance("foo", Encoder.class)); + SpringEncoder.class.cast(factory.getInstance("bar", Encoder.class)); + } + + @Test + public void overrideLogger() { + Logger.JavaLogger.class.cast(factory.getInstance("foo", Logger.class)); + Slf4jLogger.class.cast(factory.getInstance("bar", Logger.class)); + } + + @Test + public void overrideContract() { + Contract.Default.class.cast(factory.getInstance("foo", Contract.class)); + SpringMvcContract.class.cast(factory.getInstance("bar", Contract.class)); + } + + @Test + public void overrideLoggerLevel() { + assertNull(factory.getInstance("foo", Logger.Level.class)); + assertEquals(Logger.Level.HEADERS, factory.getInstance("bar", Logger.Level.class)); + } + + @Test + public void overrideRetryer() { + assertNull(factory.getInstance("foo", Retryer.class)); + Retryer.Default.class.cast(factory.getInstance("bar", Retryer.class)); + } + + @Test + public void overrideErrorDecoder() { + assertNull(factory.getInstance("foo", ErrorDecoder.class)); + ErrorDecoder.Default.class.cast(factory.getInstance("bar", ErrorDecoder.class)); + } + + @Test + public void overrideRequestOptions() { + assertNull(factory.getInstance("foo", Request.Options.class)); + Request.Options options = factory.getInstance("bar", Request.Options.class); + assertEquals(1, options.connectTimeoutMillis()); + assertEquals(1, options.readTimeoutMillis()); + } + + @Test + public void addRequestInterceptor() { + assertNull(factory.getInstance("foo", RequestInterceptor.class)); + BasicAuthRequestInterceptor.class.cast(factory.getInstance("bar", RequestInterceptor.class)); + } + + @Configuration + @EnableFeignClients(clients = {FooClient.class, BarClient.class}) + @Import({ PropertyPlaceholderAutoConfiguration.class, + ArchaiusAutoConfiguration.class, FeignAutoConfiguration.class}) + protected static class TestConfiguration { + } + + @FeignClient(value = "foo", configuration = FooConfiguration.class) + interface FooClient{ + @RequestMapping("/") + String get(); + + } + + @Configuration + public static class FooConfiguration { + @Bean + public Decoder feignDecoder() { + return new Decoder.Default(); + } + + @Bean + public Encoder feignEncoder() { + return new Encoder.Default(); + } + + @Bean + public Logger feignLogger() { + return new Logger.JavaLogger(); + } + + @Bean + public Contract feignContract() { + return new Contract.Default(); + } + } + + @FeignClient(value = "bar", configuration = BarConfiguration.class) + interface BarClient{ + @RequestMapping("/") + String get(); + } + + @Configuration + public static class BarConfiguration { + @Bean + Logger.Level feignLevel() { + return Logger.Level.HEADERS; + } + + @Bean + Retryer feignRetryer() { + return new Retryer.Default(); + } + + @Bean + ErrorDecoder feignErrorDecoder() { + return new ErrorDecoder.Default(); + } + + @Bean + Request.Options feignRequestOptions() { + return new Request.Options(1, 1); + } + + @Bean + RequestInterceptor feignRequestInterceptor() { + return new BasicAuthRequestInterceptor("user", "pass"); + } + } +} diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/SpringDecoderTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/SpringDecoderTests.java index 4ea33385..faba4236 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/SpringDecoderTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/SpringDecoderTests.java @@ -25,6 +25,7 @@ import lombok.NoArgsConstructor; import org.junit.Test; import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; @@ -54,11 +55,18 @@ import static org.junit.Assert.assertNotNull; @DirtiesContext public class SpringDecoderTests extends FeignClientFactoryBean { + @Autowired + FeignClientFactory factory; + @Value("${local.server.port}") private int port = 0; + public SpringDecoderTests() { + setName("test"); + } + public TestClient testClient() { - return feign().target(TestClient.class, "http://localhost:" + this.port); + return feign(factory).target(TestClient.class, "http://localhost:" + this.port); } @Test diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/encoding/Demo.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/encoding/Demo.java index 9a386af6..6507cb5c 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/encoding/Demo.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/encoding/Demo.java @@ -75,7 +75,7 @@ public class Demo { } - @EnableFeignClients + @EnableFeignClients(clients = InvoiceClient.class) @RibbonClient(name = "local", configuration = LocalRibbonClientConfiguration.class) @ComponentScan("org.springframework.cloud.netflix.feign.encoding.app") @EnableAutoConfiguration diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/encoding/FeignAcceptEncodingTest.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/encoding/FeignAcceptEncodingTest.java index a832d64f..b327a558 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/encoding/FeignAcceptEncodingTest.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/encoding/FeignAcceptEncodingTest.java @@ -72,7 +72,7 @@ public class FeignAcceptEncodingTest { } - @EnableFeignClients + @EnableFeignClients(clients = InvoiceClient.class) @RibbonClient(name = "local", configuration = LocalRibbonClientConfiguration.class) @ComponentScan("org.springframework.cloud.netflix.feign.encoding.app") @EnableAutoConfiguration diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/encoding/FeignContentEncodingTest.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/encoding/FeignContentEncodingTest.java index a79e482d..ba0cbd45 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/encoding/FeignContentEncodingTest.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/encoding/FeignContentEncodingTest.java @@ -75,7 +75,7 @@ public class FeignContentEncodingTest { } - @EnableFeignClients + @EnableFeignClients(clients = InvoiceClient.class) @RibbonClient(name = "local", configuration = LocalRibbonClientConfiguration.class) @ComponentScan("org.springframework.cloud.netflix.feign.encoding.app") @EnableAutoConfiguration diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/invalid/FeignClientValidationTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/invalid/FeignClientValidationTests.java index 2c152d8d..5aeea0ff 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/invalid/FeignClientValidationTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/invalid/FeignClientValidationTests.java @@ -20,9 +20,11 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.springframework.cloud.netflix.feign.EnableFeignClients; +import org.springframework.cloud.netflix.feign.FeignAutoConfiguration; import org.springframework.cloud.netflix.feign.FeignClient; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -46,7 +48,8 @@ public class FeignClientValidationTests { } @Configuration - @EnableFeignClients() + @Import(FeignAutoConfiguration.class) + @EnableFeignClients(clients = BadConfiguration.Client.class) protected static class BadConfiguration { @FeignClient("foo_bar") diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/ribbon/FeignRibbonClientRetryTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/ribbon/FeignRibbonClientRetryTests.java index 3c45b411..12911ba1 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/ribbon/FeignRibbonClientRetryTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/ribbon/FeignRibbonClientRetryTests.java @@ -83,7 +83,7 @@ public class FeignRibbonClientRetryTests { @Configuration @EnableAutoConfiguration @RestController - @EnableFeignClients + @EnableFeignClients(clients = TestClient.class) @RibbonClient(name = "localapp", configuration = LocalRibbonClientConfiguration.class) public static class Application { diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/valid/FeignClientTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/valid/FeignClientTests.java index 7e54e13c..dd6d3215 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/valid/FeignClientTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/valid/FeignClientTests.java @@ -113,7 +113,7 @@ public class FeignClientTests { @Configuration @EnableAutoConfiguration @RestController - @EnableFeignClients + @EnableFeignClients(clients = {TestClientServiceId.class, TestClient.class}) @RibbonClient(name = "localapp", configuration = LocalRibbonClientConfiguration.class) protected static class Application { diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/valid/FeignClientValidationTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/valid/FeignClientValidationTests.java index 3236552d..d0487d05 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/valid/FeignClientValidationTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/valid/FeignClientValidationTests.java @@ -18,11 +18,13 @@ package org.springframework.cloud.netflix.feign.valid; import org.junit.Test; import org.springframework.cloud.netflix.feign.EnableFeignClients; +import org.springframework.cloud.netflix.feign.FeignAutoConfiguration; import org.springframework.cloud.netflix.feign.FeignClient; import org.springframework.cloud.netflix.feign.ribbon.FeignRibbonClientAutoConfiguration; import org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -42,10 +44,11 @@ public class FeignClientValidationTests { } @Configuration - @EnableFeignClients + @Import(FeignAutoConfiguration.class) + @EnableFeignClients(clients = GoodUrlConfiguration.Client.class) protected static class GoodUrlConfiguration { - @FeignClient(url="http://example.com") + @FeignClient(name="example", url="http://example.com") interface Client { @RequestMapping(method = RequestMethod.GET, value = "/") @Deprecated @@ -65,7 +68,8 @@ public class FeignClientValidationTests { } @Configuration - @EnableFeignClients + @Import(FeignAutoConfiguration.class) + @EnableFeignClients(clients = GoodServiceIdConfiguration.Client.class) protected static class GoodServiceIdConfiguration { @FeignClient("foo") diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/valid/FeignHttpClientTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/valid/FeignHttpClientTests.java index 1ee1f1b0..be5f4b25 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/valid/FeignHttpClientTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/valid/FeignHttpClientTests.java @@ -100,7 +100,7 @@ public class FeignHttpClientTests { @Configuration @EnableAutoConfiguration @RestController - @EnableFeignClients + @EnableFeignClients(clients = {TestClient.class, UserClient.class}) @RibbonClient(name = "localapp", configuration = LocalRibbonClientConfiguration.class) protected static class Application implements UserService {