Dynamic Routing Through Zuul With a REST API and Spring Boot Without Spring Config — Sub-Domain to Subpath Router
Problem Statement
Say there was a requirement to forward all the traffic coming to *.adomain.com to adomain.com/*. For Example: a.adomain.com/**/* should be forwarded to adomain.com/a/**/*. Additionally, the proxy gateway should be up all the time and new subdomains need to be registered in the gateway without any downtime. This problem was a very specific one and needed some research to achieve the same.
Research Around Proxies
We tried a couple of things, including node http-proxy, the Java throo library (internally using Zuul), and Zuul itself. All the examples were based on prefetched configuration from a properties file for the proxy creation, except advance Zuul examples, which talk about refreshable configuration using Spring refreshable cloud configuration and RabbitMQ. There was a need for a runtime proxy creator service, preferably exposing a POST API taking some parameters and registering a new proxy at runtime. Even a delete API was required to get rid of proxies no longer being used. Being a Java developer, the natural choice was Zuul.
Pre-Requisites
Knowledge of Java, APIs, proxy, Apache, the gateway concept, and Spring Boot is needed to fully grasp the idea presented here.
Solution
While going through Zuul provided by Netflix, now added in Spring Cloud distribution, there is a nice example of how to do it by using Eureka, Zuul Server, Spring Cloud Server, RabbitMQ, and Git. Although the whole example is great and provides great flexibility, it involves many frameworks and lots of setup to be done. I could not use it as I was looking to create a very simple solution to solve our problem, considering the tradeoff of not being HA.
The architecture is as follows:
Coding
There are a couple of things needed to make the application work:
APIs
To take the parameter in POST for creating the route.
Zuul-Related Changes
- Pom changes: It contains actuator, zuul, and spring-cloud related dependencies.
<project
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>gateway-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.7.RELEASE</version>
<relativePath />
<!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<!-- Dependencies -->
<spring-cloud.version>Camden.SR7</spring-cloud.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.handlers</resource>
</transformer>
<transformer
implementation="org.springframework.boot.maven.PropertiesMergingResourceTransformer">
<resource>META-INF/spring.factories</resource>
</transformer>
<transformer
implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.schemas</resource>
</transformer>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.example.application.SpringBootWebApplication</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Spring Boot Application class:
package com.mettl.gatewayservice.application;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@EnableAutoConfiguration(exclude = { RabbitAutoConfiguration.class })
@EnableZuulProxy
@ComponentScan("com.example.gatewayservice")
public class SpringBootWebApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootWebApplication.class, args);
}
}
application.yml
server:
port: ${appPort:80}
info.app.version: @project.version@
# Actuator endpoint path (/admin/info, /admin/health, ...)
server.servlet-path: /
management.context-path: /admin
# ribbon.eureka.enabled: false
zuul:
ignoredPatterns: /**/admin/**, /proxyurl
routes:
zuulDemo1:
path: /**
url: http://localhost:8000/
# stripPrefix set to true if context path is set to /
stripPrefix: true
There are two parts:
1. Registration of Proxy Routes
This part is done by creating a service class by auto-wiring two dependencies, like this:
@Service
public class ZuulDynamicRoutingService {
private static final String HTTP = "http://";
private final ZuulProperties zuulProperties;
private final ZuulHandlerMapping zuulHandlerMapping;
......
Adding the new route is achieved by the following code.
Creation of uuid can be done by following any standard way. The only thing is that it should be a unique key, as it is going to be used in the map used by zuulProperties.getRoutes()
.
String uuid = GenerateUID.getUID();
if (StringUtils.isEmpty(dynamicRouteRequest.getSubpath())) {
dynamicRouteRequest.setSubpath("");
}
String url = "http://" + dynamicRouteRequest.getHost() + ":" + dynamicRouteRequest.getPort() + dynamicRouteRequest.getSubpath();
zuulProperties.getRoutes().put(uuid, new ZuulRoute(uuid, "/" + uuid + "/**", null, url, true, false, new HashSet<>()));
zuulHandlerMapping.setDirty(true);
This service is injected in a controller to receive the post request and forward the details to the service class to get the proxy created.
It can be checked with the URL /admin/routes.
2. Forwarding HTTP Requests to the Destination With Subdomain to Subpath Conversion
This requires a PreFilter class extending ZuulFilter:
@Component
public class PreFilter extends ZuulFilter {
private static Logger log = LoggerFactory.getLogger(PreFilter.class);
private UrlPathHelper urlPathHelper = new UrlPathHelper();
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
String requestURL = ctx.getRequest().getRequestURL().toString();
//Here we only require to filter those URLs which contains "proxyurl" and "/admin/".
return !(requestURL.contains("proxyurl") || requestURL.contains("/admin/"));
}
//The actual part where the subdomain to subpath conversion happens is as follows:
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
String remoteHost = ctx.getRequest().getRemoteHost();
String requestURL = ctx.getRequest().getRequestURL().toString();
if (!requestURL.contains("proxyurl")) {
log.info("remoteHost {} requestURL {}", new Object[]{remoteHost, requestURL});
String originatingRequestUri = this.urlPathHelper.getOriginatingRequestUri(ctx.getRequest());
final String requestURI = this.urlPathHelper.getPathWithinApplication(ctx.getRequest());
log.info("URI {} original URI {}", new Object[]{requestURI, originatingRequestUri});
String protocol = requestURL.substring(0, requestURL.indexOf("//") + 2);
String urlWithoutProtocol = requestURL.substring(requestURL.indexOf("//") + 2);
String[] split = urlWithoutProtocol.substring(0, urlWithoutProtocol.indexOf("/")).split("\\.");
String subPath = split[0];
final String newURL = protocol + "." + split[1] + "." + split[2];
//Here the main thing is to create a HttpServletRequestWrapper and override the request coming from the actual request
HttpServletRequestWrapper httpServletRequestWrapper = new HttpServletRequestWrapper(ctx.getRequest()) {
public String getRequestURI() {
if (requestURI != null && !requestURI.equals("/")) {
if (!StringUtils.isEmpty(subPath)) {
return "/" + subPath + requestURI;
} else {
return requestURI;
}
}
if (!StringUtils.isEmpty(subPath)) {
return "/" + subPath;
} else {
return "/";
}
}
public StringBuffer getRequestURL() {
return new StringBuffer(newURL);
}
};
ctx.setRequest(httpServletRequestWrapper);
HttpServletRequest request = ctx.getRequest();
log.info("PreFilter: " + String.format("%s request to %s", request.getMethod(), request.getRequestURL().toString()));
}
return null;
}
}
Now you can run the application by adding the missing part with mvn springboot:run
or java -jar gateway-service.jar
.
Now, use Postman to submit the request, like below.
To Be Done on Your Own
The Apache part is missing in this session. Add a configuration to forward all requests coming to *.adomain.com to the IP hosting the Zuul server with the port configured when starting the Zuul server.
Applications where the requests need to be up when creating proxy routes to actually see what is happening.
Summary
We learned to
Create a proxy route at runtime by exposing an API
Change PreFilter (a subclass of ZuulFilter) to pre-process the HTTP request
Add Apache configuration to forward requests to Zuul Gateway