From d3cba4d80ecfde246465b414c4618e452468f0c3 Mon Sep 17 00:00:00 2001 From: Venil Noronha Date: Sat, 23 Apr 2016 19:42:08 +0530 Subject: [PATCH] Adds path parameter to @FeignClient. Fixes gh-966 --- .../cloud/netflix/feign/FeignClient.java | 10 +- .../netflix/feign/FeignClientFactoryBean.java | 22 ++- .../netflix/feign/FeignClientsRegistrar.java | 18 +- .../ribbon/FeignRibbonClientPathTests.java | 184 ++++++++++++++++++ 4 files changed, 230 insertions(+), 4 deletions(-) create mode 100644 spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/ribbon/FeignRibbonClientPathTests.java 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 8c585eb7..1de287c1 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 @@ -1,5 +1,5 @@ /* - * Copyright 2013-2015 the original author or authors. + * Copyright 2013-2016 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. @@ -31,6 +31,7 @@ import org.springframework.core.annotation.AliasFor; * using a @RibbonClient with the same name (i.e. value) as the feign client. * * @author Spencer Gibb + * @author Venil Noronha */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @@ -81,4 +82,11 @@ public @interface FeignClient { * implement the interface annotated by this annotation and be a valid spring bean. */ Class fallback() default void.class; + + /** + * Path prefix to be used by all method-level mappings. Can be used with or without + * @RibbonClient. + */ + String path() default ""; + } 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 eac0abb2..624d0132 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 @@ -1,5 +1,5 @@ /* - * Copyright 2013-2015 the original author or authors. + * Copyright 2013-2016 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. @@ -44,6 +44,7 @@ import lombok.EqualsAndHashCode; /** * @author Spencer Gibb + * @author Venil Noronha */ @Data @EqualsAndHashCode(callSuper = false) @@ -70,6 +71,8 @@ class FeignClientFactoryBean implements FactoryBean, InitializingBean, private String url; + private String path; + private boolean decode404; private ApplicationContext applicationContext; @@ -170,14 +173,29 @@ class FeignClientFactoryBean implements FactoryBean, InitializingBean, else { url = this.name; } + url += cleanPath(); return loadBalance(builder, context, new HardCodedTarget<>(this.type, this.name, url)); } if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) { this.url = "http://" + this.url; } + String url = this.url + cleanPath(); return targeter.target(this, builder, context, new HardCodedTarget<>( - this.type, this.name, this.url)); + this.type, this.name, url)); + } + + private String cleanPath() { + String path = this.path.trim(); + if (StringUtils.hasLength(path)) { + if (!path.startsWith("/")) { + path = "/" + path; + } + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + } + return path; } @Override 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 eb868f0a..24564433 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 @@ -1,5 +1,5 @@ /* - * Copyright 2013-2015 the original author or authors. + * Copyright 2013-2016 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. @@ -55,6 +55,7 @@ import org.springframework.util.StringUtils; /** * @author Spencer Gibb * @author Jakub Narloch + * @author Venil Noronha */ public class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, BeanClassLoaderAware { @@ -171,6 +172,7 @@ public class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, .genericBeanDefinition(FeignClientFactoryBean.class); validate(attributes); definition.addPropertyValue("url", getUrl(attributes)); + definition.addPropertyValue("path", getPath(attributes)); String name = getName(attributes); definition.addPropertyValue("name", name); definition.addPropertyValue("type", className); @@ -247,6 +249,20 @@ public class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, return url; } + private String getPath(Map attributes) { + String path = resolve((String) attributes.get("path")); + if (StringUtils.hasText(path)) { + path = path.trim(); + if (!path.startsWith("/")) { + path = "/" + path; + } + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + } + return path; + } + protected ClassPathScanningCandidateComponentProvider getScanner() { return new ClassPathScanningCandidateComponentProvider(false) { diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/ribbon/FeignRibbonClientPathTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/ribbon/FeignRibbonClientPathTests.java new file mode 100644 index 00000000..1a8d45cc --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/ribbon/FeignRibbonClientPathTests.java @@ -0,0 +1,184 @@ +/* + * Copyright 2016 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.ribbon; + +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.SpringApplicationConfiguration; +import org.springframework.boot.test.WebIntegrationTest; +import org.springframework.cloud.netflix.feign.EnableFeignClients; +import org.springframework.cloud.netflix.feign.FeignClient; +import org.springframework.cloud.netflix.ribbon.RibbonClient; +import org.springframework.cloud.netflix.ribbon.StaticServerList; +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.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import com.netflix.loadbalancer.Server; +import com.netflix.loadbalancer.ServerList; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * @author Venil Noronha + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = FeignRibbonClientPathTests.Application.class) +@WebIntegrationTest( + randomPort = true, + value = { + "spring.application.name=feignribbonclientpathtest", + "feign.okhttp.enabled=false", + "feign.httpclient.enabled=false", + "feign.hystrix.enabled=false", + "test.path.prefix=/base/path" // For pathWithPlaceholder test + } +) +@DirtiesContext +public class FeignRibbonClientPathTests { + + @Value("${local.server.port}") + private int port = 0; + + @Autowired + private TestClient1 testClient1; + + @Autowired + private TestClient2 testClient2; + + @Autowired + private TestClient3 testClient3; + + @Autowired + private TestClient4 testClient4; + + @Autowired + private TestClient5 testClient5; + + protected interface TestClient { + + @RequestMapping(method = RequestMethod.GET, value = "/hello") + Hello getHello(); + + } + + @FeignClient(name = "localapp", path = "/base/path") + protected interface TestClient1 extends TestClient { } + + @FeignClient(name = "localapp", path = "base/path") + protected interface TestClient2 extends TestClient { } + + @FeignClient(name = "localapp", path = "base/path/") + protected interface TestClient3 extends TestClient { } + + @FeignClient(name = "localapp", path = "/base/path/") + protected interface TestClient4 extends TestClient { } + + @FeignClient(name = "localapp", path = "${test.path.prefix}") + protected interface TestClient5 extends TestClient { } + + @Configuration + @EnableAutoConfiguration + @RestController + @RequestMapping("/base/path") + @EnableFeignClients(clients = { + TestClient1.class, TestClient2.class, TestClient3.class, TestClient4.class, + TestClient5.class + }) + @RibbonClient(name = "localapp", configuration = LocalRibbonClientConfiguration.class) + public static class Application { + + @RequestMapping(method = RequestMethod.GET, value = "/hello") + public Hello getHello() { + return new Hello("hello world"); + } + + public static void main(String[] args) throws InterruptedException { + new SpringApplicationBuilder(Application.class).properties( + "spring.application.name=feignribbonclientpathtest", + "management.contextPath=/admin" + ).run(args); + } + + } + + @Test + public void pathWithLeadingButNotTrailingSlash() { + testClientPath(this.testClient1); + } + + @Test + public void pathWithoutLeadingAndTrailingSlash() { + testClientPath(this.testClient2); + } + + @Test + public void pathWithoutLeadingButTrailingSlash() { + testClientPath(this.testClient3); + } + + @Test + public void pathWithLeadingAndTrailingSlash() { + testClientPath(this.testClient4); + } + + @Test + public void pathWithPlaceholder() { + testClientPath(this.testClient5); + } + + private void testClientPath(TestClient testClient) { + Hello hello = testClient.getHello(); + assertNotNull("Object returned was null", hello); + assertEquals("Response object value didn't match", "hello world", + hello.getMessage()); + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class Hello { + private String message; + } + + @Configuration + public static class LocalRibbonClientConfiguration { + + @Value("${local.server.port}") + private int port = 0; + + @Bean + public ServerList ribbonServerList() { + return new StaticServerList<>(new Server("localhost", this.port)); + } + + } + +}