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));
+ }
+
+ }
+
+}