Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Dynamic Routing Through Zuul With a REST API and Spring Boot Without Spring Config — Sub-Domain to Subpath Router

DZone's Guide to

Dynamic Routing Through Zuul With a REST API and Spring Boot Without Spring Config — Sub-Domain to Subpath Router

Learn about using Zuul with a REST API to route traffic from one service to another with no downtime.

· Microservices Zone ·
Free Resource

Containerized Microservices require new monitoring. Read the eBook that explores why a new APM approach is needed to even see containerized applications.

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:

Image title

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.

Image title

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

  1. Create a proxy route at runtime by exposing an API 

  2. Change PreFilter (a subclass of ZuulFilter) to pre-process the HTTP request

  3. Add Apache configuration to forward requests to Zuul Gateway

Discover how to automatically manage containers and microservices with better control and performance using Instana APM. Try it for yourself today.

Topics:
zuul ,rest api ,proxy services ,spring boot ,routing ,tutorial ,microservices

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}