Spencer Gibb
9 years ago
10 changed files with 394 additions and 17 deletions
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
package org.springframework.cloud.netflix.zuul.filters; |
||||
|
||||
/** |
||||
* @author Stéphane LEROY |
||||
* |
||||
* Provide a way to apply convention between routes and discovered services name. |
||||
* |
||||
*/ |
||||
public interface ServiceRouteMapper { |
||||
|
||||
/** |
||||
* Take a service Id (its discovered name) and return a route path. |
||||
* |
||||
* @param serviceId service discovered name |
||||
* @return route path |
||||
*/ |
||||
String apply(String serviceId); |
||||
} |
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
package org.springframework.cloud.netflix.zuul.filters; |
||||
|
||||
/** |
||||
* @author Stéphane Leroy |
||||
* |
||||
* A simple passthru service route mapper. |
||||
*/ |
||||
public class SimpleServiceRouteMapper implements ServiceRouteMapper { |
||||
@Override |
||||
public String apply(String serviceId) { |
||||
return serviceId; |
||||
} |
||||
} |
@ -0,0 +1,70 @@
@@ -0,0 +1,70 @@
|
||||
package org.springframework.cloud.netflix.zuul.filters.regex; |
||||
|
||||
import java.util.regex.Matcher; |
||||
import java.util.regex.Pattern; |
||||
|
||||
import org.springframework.cloud.netflix.zuul.filters.ServiceRouteMapper; |
||||
import org.springframework.util.StringUtils; |
||||
|
||||
/** |
||||
* @author Stéphane Leroy |
||||
* |
||||
* This service route mapper use Java 7 RegEx named group feature to rewrite a discovered |
||||
* service Id into a route. |
||||
* |
||||
* Ex : If we want to map service Id [rest-service-v1] to /v1/rest-service/** route |
||||
* service pattern : "(?<name>.*)-(?<version>v.*$)" route pattern : "${version}/${name}" |
||||
* |
||||
* /!\ This implementation use Matcher.replaceFirst so only one match will be replace. |
||||
*/ |
||||
public class RegExServiceRouteMapper implements ServiceRouteMapper { |
||||
|
||||
/** |
||||
* A RegExp Pattern that extract needed information from a service ID. Ex : |
||||
* "(?<name>.*)-(?<version>v.*$)" |
||||
*/ |
||||
private Pattern servicePattern; |
||||
/** |
||||
* A RegExp that refer to named groups define in servicePattern. Ex : |
||||
* "${version}/${name}" |
||||
*/ |
||||
private String routePattern; |
||||
|
||||
public RegExServiceRouteMapper(String servicePattern, String routePattern) { |
||||
this.servicePattern = Pattern.compile(servicePattern); |
||||
this.routePattern = routePattern; |
||||
} |
||||
|
||||
/** |
||||
* Use servicePattern to extract groups and routePattern to construct the route. |
||||
* |
||||
* If there is no matches, the serviceId is returned. |
||||
* |
||||
* @param serviceId service discovered name |
||||
* @return route path |
||||
*/ |
||||
@Override |
||||
public String apply(String serviceId) { |
||||
Matcher matcher = servicePattern.matcher(serviceId); |
||||
String route = matcher.replaceFirst(routePattern); |
||||
route = cleanRoute(route); |
||||
return (StringUtils.hasText(route) ? route : serviceId); |
||||
} |
||||
|
||||
/** |
||||
* Route with regex and replace can be a bit messy when used with conditional named |
||||
* group. We clean here first and trailing '/' and remove multiple consecutive '/' |
||||
* @param route |
||||
* @return |
||||
*/ |
||||
private String cleanRoute(final String route) { |
||||
String routeToClean = route.replaceAll("/{2,}", "/"); |
||||
if (routeToClean.startsWith("/")) { |
||||
routeToClean = routeToClean.substring(1); |
||||
} |
||||
if (routeToClean.endsWith("/")) { |
||||
routeToClean = routeToClean.substring(0, routeToClean.length() - 1); |
||||
} |
||||
return routeToClean; |
||||
} |
||||
} |
@ -0,0 +1,127 @@
@@ -0,0 +1,127 @@
|
||||
package org.springframework.cloud.netflix.zuul.filters.regex; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
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.SpringApplication; |
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; |
||||
import org.springframework.boot.test.SpringApplicationConfiguration; |
||||
import org.springframework.boot.test.TestRestTemplate; |
||||
import org.springframework.boot.test.WebIntegrationTest; |
||||
import org.springframework.cloud.client.discovery.DiscoveryClient; |
||||
import org.springframework.cloud.netflix.ribbon.RibbonClient; |
||||
import org.springframework.cloud.netflix.ribbon.StaticServerList; |
||||
import org.springframework.cloud.netflix.zuul.EnableZuulProxy; |
||||
import org.springframework.cloud.netflix.zuul.RoutesEndpoint; |
||||
import org.springframework.cloud.netflix.zuul.filters.ProxyRouteLocator; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.http.HttpEntity; |
||||
import org.springframework.http.HttpMethod; |
||||
import org.springframework.http.HttpStatus; |
||||
import org.springframework.http.ResponseEntity; |
||||
import org.springframework.test.annotation.DirtiesContext; |
||||
import org.springframework.test.context.TestPropertySource; |
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; |
||||
import org.springframework.web.bind.annotation.PathVariable; |
||||
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 static org.junit.Assert.assertEquals; |
||||
import static org.mockito.Mockito.mock; |
||||
import static org.mockito.Mockito.when; |
||||
import static org.springframework.cloud.netflix.zuul.filters.regex.RegExServiceRouteMapperIntegrationTests.SERVICE_ID; |
||||
|
||||
/** |
||||
* @author Stéphane Leroy |
||||
*/ |
||||
@RunWith(SpringJUnit4ClassRunner.class) |
||||
@SpringApplicationConfiguration(classes = SampleCustomZuulProxyApplication.class) |
||||
@WebIntegrationTest(value = { "spring.application.name=regex-test-application", |
||||
"spring.jmx.enabled=true" }, randomPort = true) |
||||
@TestPropertySource(properties = { "eureka.client.enabled=false", |
||||
"zuul.regexMapper.enabled=true", |
||||
"zuul.regexMapper.servicePattern=(?<domain>^.+)-(?<name>.+)-(?<version>v.+$)", |
||||
"zuul.regexMapper.routePattern=${version}/${domain}/${name}" }) |
||||
@DirtiesContext |
||||
public class RegExServiceRouteMapperIntegrationTests { |
||||
|
||||
protected static final String SERVICE_ID = "domain-service-v1"; |
||||
|
||||
@Value("${local.server.port}") |
||||
private int port; |
||||
|
||||
@Autowired |
||||
private ProxyRouteLocator routes; |
||||
|
||||
@Autowired |
||||
private RoutesEndpoint endpoint; |
||||
|
||||
@Test |
||||
public void getRegexMappedService() { |
||||
endpoint.reset(); |
||||
ResponseEntity<String> result = new TestRestTemplate().exchange( |
||||
"http://localhost:" + this.port + "/v1/domain/service/get/1", |
||||
HttpMethod.GET, new HttpEntity<>((Void) null), String.class); |
||||
assertEquals(HttpStatus.OK, result.getStatusCode()); |
||||
assertEquals("Get 1", result.getBody()); |
||||
} |
||||
|
||||
@Test |
||||
public void getStaticRoute() { |
||||
this.routes.addRoute("/self/**", "http://localhost:" + this.port); |
||||
endpoint.reset(); |
||||
ResponseEntity<String> result = new TestRestTemplate().exchange( |
||||
"http://localhost:" + this.port + "/self/get/1", HttpMethod.GET, |
||||
new HttpEntity<>((Void) null), String.class); |
||||
assertEquals(HttpStatus.OK, result.getStatusCode()); |
||||
assertEquals("Get 1", result.getBody()); |
||||
} |
||||
|
||||
} |
||||
|
||||
@Configuration |
||||
@EnableAutoConfiguration |
||||
@RestController |
||||
@EnableZuulProxy |
||||
@RibbonClient(value = SERVICE_ID, configuration = SimpleRibbonClientConfiguration.class) |
||||
class SampleCustomZuulProxyApplication { |
||||
|
||||
@Bean |
||||
public DiscoveryClient discoveryClient() { |
||||
DiscoveryClient discoveryClient = mock(DiscoveryClient.class); |
||||
List<String> services = new ArrayList<>(); |
||||
services.add(SERVICE_ID); |
||||
when(discoveryClient.getServices()).thenReturn(services); |
||||
return discoveryClient; |
||||
} |
||||
|
||||
@RequestMapping(value = "/get/{id}", method = RequestMethod.GET) |
||||
public String get(@PathVariable String id) { |
||||
return "Get " + id; |
||||
} |
||||
|
||||
public static void main(String[] args) { |
||||
SpringApplication.run(SampleCustomZuulProxyApplication.class, args); |
||||
} |
||||
} |
||||
|
||||
@Configuration |
||||
class SimpleRibbonClientConfiguration { |
||||
|
||||
@Value("${local.server.port}") |
||||
private int port = 0; |
||||
|
||||
@Bean |
||||
public ServerList<Server> ribbonServerList() { |
||||
return new StaticServerList<>(new Server("localhost", this.port)); |
||||
} |
||||
} |
@ -0,0 +1,48 @@
@@ -0,0 +1,48 @@
|
||||
package org.springframework.cloud.netflix.zuul.filters.regex; |
||||
|
||||
import org.junit.Test; |
||||
|
||||
import static org.junit.Assert.assertEquals; |
||||
|
||||
/** |
||||
* @author Stéphane Leroy |
||||
*/ |
||||
public class RegExServiceRouteMapperTests { |
||||
|
||||
/** |
||||
* Service pattern that follow convention {domain}-{name}-{version}. The name is |
||||
* optional |
||||
*/ |
||||
public static final String SERVICE_PATTERN = "(?<domain>^\\w+)(-(?<name>\\w+)-|-)(?<version>v\\d+$)"; |
||||
public static final String ROUTE_PATTERN = "${version}/${domain}/${name}"; |
||||
|
||||
@Test |
||||
public void test_return_mapped_route_if_serviceid_matches() { |
||||
RegExServiceRouteMapper toTest = new RegExServiceRouteMapper(SERVICE_PATTERN, |
||||
ROUTE_PATTERN); |
||||
|
||||
assertEquals("service version convention", "v1/rest/service", |
||||
toTest.apply("rest-service-v1")); |
||||
} |
||||
|
||||
@Test |
||||
public void test_return_serviceid_if_no_matches() { |
||||
RegExServiceRouteMapper toTest = new RegExServiceRouteMapper(SERVICE_PATTERN, |
||||
ROUTE_PATTERN); |
||||
|
||||
// No version here
|
||||
assertEquals("No matches for this service id", "rest-service", |
||||
toTest.apply("rest-service")); |
||||
} |
||||
|
||||
@Test |
||||
public void test_route_should_be_cleaned_before_returned() { |
||||
// Messy patterns
|
||||
RegExServiceRouteMapper toTest = new RegExServiceRouteMapper(SERVICE_PATTERN |
||||
+ "(?<nevermatch>.)?", "/${version}/${nevermatch}/${domain}/${name}/"); |
||||
assertEquals("No matches for this service id", "v1/domain/service", |
||||
toTest.apply("domain-service-v1")); |
||||
assertEquals("No matches for this service id", "v1/domain", |
||||
toTest.apply("domain-v1")); |
||||
} |
||||
} |
Loading…
Reference in new issue