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 new file mode 100644 index 00000000..9209a6d3 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClient.java @@ -0,0 +1,14 @@ +package org.springframework.cloud.netflix.feign; + +import java.lang.annotation.*; + +/** + * @author Spencer Gibb + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface FeignClient { + String value(); + boolean loadbalance() default true; +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientScan.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientScan.java new file mode 100644 index 00000000..862ab9d0 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientScan.java @@ -0,0 +1,53 @@ +package org.springframework.cloud.netflix.feign; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Import; + +/** + * Configures component scanning directives for use with @{@link org.springframework.context.annotation.Configuration} classes. + * Scan Spring Integration specific components. + * + * @author Artem Bilan + * @since 4.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Documented +@Import(FeignClientScanRegistrar.class) +public @interface FeignClientScan { + + /** + * Alias for the {@link #basePackages()} attribute. + * Allows for more concise annotation declarations e.g.: + * {@code @ComponentScan("org.my.pkg")} instead of + * {@code @ComponentScan(basePackages="org.my.pkg")}. + * + * @return the array of 'basePackages'. + */ + String[] value() default {}; + + /** + * Base packages to scan for annotated components. + *

{@link #value()} is an alias for (and mutually exclusive with) this attribute. + *

Use {@link #basePackageClasses()} for a type-safe alternative to String-based package names. + * + * @return the array of 'basePackages'. + */ + String[] basePackages() default {}; + + /** + * Type-safe alternative to {@link #basePackages()} for specifying the packages + * to scan for annotated components. The package of each class specified will be scanned. + *

Consider creating a special no-op marker class or interface in each package + * that serves no purpose other than being referenced by this attribute. + * + * @return the array of 'basePackageClasses'. + */ + Class[] basePackageClasses() default {}; + +} \ No newline at end of file diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientScanRegistrar.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientScanRegistrar.java new file mode 100644 index 00000000..6af944f1 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientScanRegistrar.java @@ -0,0 +1,155 @@ +package org.springframework.cloud.netflix.feign; + +import java.lang.annotation.Annotation; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import lombok.Data; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * @author Spencer Gibb + * patterned after Spring Integration IntegrationComponentScanRegistrar + */ +public class FeignClientScanRegistrar extends FeignConfiguration + implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, BeanClassLoaderAware { + + private ResourceLoader resourceLoader; + + private ClassLoader classLoader; + + public FeignClientScanRegistrar() { + } + + @Override + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + Map componentScan = importingClassMetadata + .getAnnotationAttributes(FeignClientScan.class.getCanonicalName()); + + Set basePackages = new HashSet(); + for (String pkg : (String[]) componentScan.get("value")) { + if (StringUtils.hasText(pkg)) { + basePackages.add(pkg); + } + } + for (String pkg : (String[]) componentScan.get("basePackages")) { + if (StringUtils.hasText(pkg)) { + basePackages.add(pkg); + } + } + for (Class clazz : (Class[]) componentScan.get("basePackageClasses")) { + basePackages.add(ClassUtils.getPackageName(clazz)); + } + + if (basePackages.isEmpty()) { + basePackages.add(ClassUtils.getPackageName(importingClassMetadata.getClassName())); + } + + ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false) { + + @Override + protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { + if (beanDefinition.getMetadata().isIndependent()) { + // TODO until SPR-11711 will be resolved + if (beanDefinition.getMetadata().isInterface() && + beanDefinition.getMetadata().getInterfaceNames().length == 1 && + Annotation.class.getName().equals(beanDefinition.getMetadata().getInterfaceNames()[0])) { + try { + Class target = ClassUtils.forName(beanDefinition.getMetadata().getClassName(), classLoader); + return !target.isAnnotation(); + } + catch (Exception e) { + logger.error("Could not load target class: " + beanDefinition.getMetadata().getClassName(), e); + + } + } + return true; + } + return false; + } + }; + + scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class)); + + scanner.setResourceLoader(resourceLoader); + + for (String basePackage : basePackages) { + Set candidateComponents = scanner.findCandidateComponents(basePackage); + for (BeanDefinition candidateComponent : candidateComponents) { + if (candidateComponent instanceof AnnotatedBeanDefinition) { + AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent; + BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignFactoryBean.class); + AnnotationMetadata annotationMetadata = beanDefinition.getMetadata(); + Assert.isTrue(annotationMetadata.isInterface(), "@FeignClient can only be specified on an interface"); + + Map attributes = annotationMetadata.getAnnotationAttributes(FeignClient.class.getCanonicalName()); + + String className = annotationMetadata.getClassName(); + definition.addPropertyValue("loadbalance", attributes.get("loadbalance")); + definition.addPropertyValue("type", className); + definition.addPropertyValue("schemeName", attributes.get("value")); + + String beanName = StringUtils.uncapitalize(className.substring(className.lastIndexOf(".")+1)); + BeanDefinitionHolder holder = new BeanDefinitionHolder(definition.getBeanDefinition(), beanName); + BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); + } + } + } + } + + @Data + public static class FeignFactoryBean extends FeignConfiguration implements FactoryBean { + + boolean loadbalance; + Class type; + String schemeName; + + @Override + public Object getObject() throws Exception { + if (!schemeName.startsWith("http")) { + schemeName = "http://"+schemeName; + } + if (loadbalance) { + return loadBalance(type, schemeName); + } + return feign().target(type, schemeName); + } + + @Override + public Class getObjectType() { + return type; + } + + @Override + public boolean isSingleton() { + return true; + } + } +} diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/FeignClientTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/FeignClientTests.java new file mode 100644 index 00000000..ef3d32bb --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/FeignClientTests.java @@ -0,0 +1,114 @@ +package org.springframework.cloud.netflix.feign; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Data; +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; +import org.springframework.boot.test.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Spencer Gibb + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = FeignClientTests.Application.class) +@WebAppConfiguration +@IntegrationTest({ "server.port=0", "spring.application.name=feignclienttest", "spring.jmx.enabled=true" }) +public class FeignClientTests extends FeignConfiguration { + + @Value("${local.server.port}") + private int port = 0; + + @Autowired + TestClient testClient; + + //@FeignClient(value = "http://localhost:9876", loadbalance = false) + @FeignClient("feignclienttest") + protected static interface TestClient { + @RequestMapping(method = RequestMethod.GET, value = "/hello") + public Hello getHello(); + + @RequestMapping(method = RequestMethod.GET, value = "/hellos") + public List getHellos(); + + @RequestMapping(method = RequestMethod.GET, value = "/hellostrings") + public List getHelloStrings(); + } + + @Configuration + @EnableAutoConfiguration + @RestController + @FeignClientScan + protected static class Application { + + @RequestMapping(method = RequestMethod.GET, value = "/hello") + public Hello getHello() { + return new Hello("hello world 1"); + } + + @RequestMapping(method = RequestMethod.GET, value = "/hellos") + public List getHellos() { + ArrayList hellos = new ArrayList<>(); + hellos.add(new Hello("hello world 1")); + hellos.add(new Hello("oi terra 2")); + return hellos; + } + + @RequestMapping(method = RequestMethod.GET, value = "/hellostrings") + public List getHelloStrings() { + ArrayList hellos = new ArrayList<>(); + hellos.add("hello world 1"); + hellos.add("oi terra 2"); + return hellos; + } + + public static void main(String[] args) { + new SpringApplicationBuilder(Application.class).properties( + "spring.application.name=feignclienttest", "management.contextPath=/admin") + .run(args); + } + } + + @Test + public void testClient() { + assertNotNull("testClient was null", testClient); + assertTrue("testClient is not a java Proxy", Proxy.isProxyClass(testClient.getClass())); + InvocationHandler invocationHandler = Proxy.getInvocationHandler(testClient); + assertNotNull("invocationHandler was null", invocationHandler); + } + + //TODO: only works if port is hardcoded cant resolve ${local.server.port} in annotation + /*@Test + public void testSimpleType() { + Hello hello = testClient.getHello(); + assertNotNull("hello was null", hello); + assertEquals("first hello didn't match", new Hello("hello world 1"), hello); + }*/ + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class Hello { + private String message; + } +}