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

Persistent and Fault Tolerant Dynamic Routes Using Zuul, Redis, and REST API

DZone's Guide to

Persistent and Fault Tolerant Dynamic Routes Using Zuul, Redis, and REST API

Learn how to use REST APIs to register dynamic routes in Zuul server and make your dynamic routes fault tolerant with Redis cache.

· 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.

Purpose

We will create an application which provides REST APIs to create dynamic routes, display dynamic routes, deleting not needed routes, restoring all previously created dynamic routes from cache and database using Zuul, Spring boot Actuator, Redis. Although, this application demostrates more about dynamic routes, but it also displays the way to interact with Redis using spring-boot-starter-data-redis. Assuming Redis server is running locally at 6379 port.  Additionally, it showcases some of the URLs exposed by Spring boot actuator which are helpful for this application.

Steps

We will create a maven based Spring Boot project.

Project Structure:

Image title

The files are as follows:

  • pom.xml

<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.cfeindia.examples.zuul</groupId>
    <artifactId>zuul-route-redis</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.13.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>
        <spring-cloud.version>Edgware.SR3</spring-cloud.version>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
    <repositories>
        <repository>
            <id>spy</id>
            <name>Spy Repository</name>
            <layout>default</layout>
            <url>http://files.couchbase.com/maven2/</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>
    <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-data-redis</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>
                            <finalName>${project.artifactId}-${project.version}</finalName>
                            <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.mettl.gatewayservice.application.SpringBootWebApplication</mainClass>
                                </transformer>
                                <transformer implementation="com.github.edwgiz.mavenShadePlugin.log4j2CacheTransformer.PluginsCacheFileTransformer" />
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
                <dependencies>
                    <dependency>
                        <groupId>com.github.edwgiz</groupId>
                        <artifactId>maven-shade-plugin.log4j2-cachefile-transformer</artifactId>
                        <version>2.6.1</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>
</project>
  • Spring Boot application Java class with required annotations for Zuul, Redis, etc.:

package com.cfeindia.examples.zuul.application;

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;

@SpringBootApplication
@EnableAutoConfiguration(exclude = {
 RabbitAutoConfiguration.class
})
@EnableZuulProxy
@EnableRedisRepositories("com.cfeindia.examples.zuul")
@ComponentScan("com.cfeindia.examples.zuul")
public class SpringBootWebApplication {
 public static void main(String[] args) {
  SpringApplication.run(SpringBootWebApplication.class, args);
 }
}
  • Application config for bean definitions

package com.cfeindia.examples.zuul.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;

import com.cfeindia.examples.zuul.model.DynamicRoute;

import redis.clients.jedis.JedisPoolConfig;

@Configuration
public class AppConfig {

 private static final int REDIS_PORT = 6379;
 private static final String REDIS_HOST_NAME = "localhost";
 @Bean
 public RedisConnectionFactory redisConnectionFactory() {
  JedisPoolConfig poolConfig = new JedisPoolConfig();

  //Change the configuration as per requirement
  poolConfig.setMaxTotal(128);
  poolConfig.setTestOnBorrow(true);
  poolConfig.setTestOnReturn(true);
  poolConfig.setMinIdle(5);
  poolConfig.setMaxIdle(128);

  JedisConnectionFactory connectionFactory = new JedisConnectionFactory(poolConfig);
  connectionFactory.setUsePool(true);
  connectionFactory.setHostName(REDIS_HOST_NAME);
  connectionFactory.setPort(REDIS_PORT);

  return connectionFactory;
 }
 @Bean
 public RedisTemplate < String, DynamicRoute > redisTemplate() {
  RedisTemplate < String, DynamicRoute > redisTemplate = new RedisTemplate < > ();
  redisTemplate.setConnectionFactory(redisConnectionFactory());
  redisTemplate.setEnableTransactionSupport(false);
  return redisTemplate;
 }
}
  • Dynamic Route register and delete API exposing the controller class:

package com.cfeindia.examples.zuul.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import com.cfeindia.examples.zuul.model.DeleteRouteRequest;
import com.cfeindia.examples.zuul.model.DynamicRoute;
import com.cfeindia.examples.zuul.model.DynamicRouteResponse;
import com.cfeindia.examples.zuul.service.ZuulDynamicRoutingService;

@RestController
public class DynamicRouteController {


 private static final Logger logger = LoggerFactory.getLogger(DynamicRouteController.class);

 @Autowired
 ZuulDynamicRoutingService zuulDynamicRoutingService;

 @RequestMapping(value = "/proxyurl", method = RequestMethod.POST)
 public @ResponseBody DynamicRouteResponse getProxyURL(@RequestBody DynamicRoute dynamicRoute) {
  logger.debug("request received to add {}", dynamicRoute);
  DynamicRouteResponse dynamicRouteResponse = zuulDynamicRoutingService.addDynamicRoute(dynamicRoute);
  logger.debug("response sent {}", dynamicRouteResponse);
  return dynamicRouteResponse;
 }

 @RequestMapping(value = "/proxyurl", method = RequestMethod.DELETE)
 public @ResponseBody Boolean deleteProxyURL(@RequestBody DeleteRouteRequest deleteRouteRequest) {
  logger.debug("request received to delete {}", deleteRouteRequest);
  Boolean response = zuulDynamicRoutingService.removeDynamicRoute(deleteRouteRequest.getRequestURIUniqueKey());
  logger.debug("response sent for delete {}", response);
  return response;
 }
}
  • Request and Response POJOs for Rest APIs

DynamicRoute.java: 

Dynamic route request class whose objects are getting saved in Redis. Check annotations @RedisHash and @Id, which are required to successfully save, retrieve and delete objects of DynamicRoute class.
It is also used in Rest API request to convert Json in Object.

package com.cfeindia.examples.zuul.model;

import java.io.Serializable;

import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;

/**
 * Dynamic route request object saved in Redis
 * It is also used in Rest API request to convert Json in Object
 * @author vikasanand
 *
 */
@RedisHash("Gateway_Service_Dynamic_Route")
public class DynamicRoute implements Serializable {

 private static final long serialVersionUID = -6719150427066586659 L;

 /**
  * This can be a unique key different for each route registration. It should be different 
  * for each requestURI to be forwarded.
  * i.e. asad121-sadas-hjjhhd
  */
 @Id
 public String requestURIUniqueKey;

 /**
  * This can be of format "/api1"
  * It should be sub-path URI which needs to forwarded to different proxy
  */
 private String requestURI;

 /**
  * Target Host name or IP
  * i.e. https://adomain.com
  */
 private String targetURLHost;

 /**
  * Target Port to forward
  * i.e. 80
  */
 private int targetURLPort;

 /**
  * Target URI to forward
  * /proxy-api1
  */
 private String targetURIPath;

 public String getRequestURIUniqueKey() {
  return requestURIUniqueKey;
 }

 public void setRequestURIUniqueKey(String requestURIUniqueKey) {
  this.requestURIUniqueKey = requestURIUniqueKey;
 }

 public String getRequestURI() {
  return requestURI;
 }

 public void setRequestURI(String requestURI) {
  this.requestURI = requestURI;
 }

 public String getTargetURLHost() {
  return targetURLHost;
 }

 public void setTargetURLHost(String targetURLHost) {
  this.targetURLHost = targetURLHost;
 }

 public int getTargetURLPort() {
  return targetURLPort;
 }

 public void setTargetURLPort(int targetURLPort) {
  this.targetURLPort = targetURLPort;
 }

 public String getTargetURIPath() {
  return targetURIPath;
 }

 public void setTargetURIPath(String targetURIPath) {
  this.targetURIPath = targetURIPath;
 }

 @Override
 public String toString() {
  StringBuilder builder = new StringBuilder();
  builder.append("DynamicRoute [requestURIUniqueKey=");
  builder.append(requestURIUniqueKey);
  builder.append(", requestURI=");
  builder.append(requestURI);
  builder.append(", targetURLHost=");
  builder.append(targetURLHost);
  builder.append(", targetURLPort=");
  builder.append(targetURLPort);
  builder.append(", targetURIPath=");
  builder.append(targetURIPath);
  builder.append("]");
  return builder.toString();
 }

}

Other POJOs:

package com.cfeindia.examples.zuul.model;

/**
 * Response of dynamic route creation and saving
 * @author vikasanand
 *
 */
public class DynamicRouteResponse {

 /**
  * statusCode is 0 for success.
  * if statusCode is not 0 then check for errorMessage for details.
  */
 private int statusCode;
 private String errorMessage;

 public DynamicRouteResponse() {
  super();
 }

 public DynamicRouteResponse(int statusCode, String errorMessage) {
  super();
  this.statusCode = statusCode;
  this.errorMessage = errorMessage;
 }

 public int getStatusCode() {
  return statusCode;
 }

 public String getErrorMessage() {
  return errorMessage;
 }

 @Override
 public String toString() {
  StringBuilder builder = new StringBuilder();
  builder.append("DynamicRouteResponse [statusCode=");
  builder.append(statusCode);
  builder.append(", errorMessage=");
  builder.append(errorMessage);
  builder.append("]");
  return builder.toString();
 }

}
package com.cfeindia.examples.zuul.model;

public class DeleteRouteRequest {

 public String requestURIUniqueKey;

 public String getRequestURIUniqueKey() {
  return requestURIUniqueKey;
 }

 public void setRequestURIUniqueKey(String requestURIUniqueKey) {
  this.requestURIUniqueKey = requestURIUniqueKey;
 }

}
  • A DAO or repository class to interact with the Redis server. Using theSpring Data Redis library makes it very simple to do CRUD operations. We just need to create an interface extending the interface CrudRepository and annotate it with @Repository. Please keep the types as DynamicRoute and String, first for the value and second for the key type.

package com.cfeindia.examples.zuul.dao;

import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

import com.cfeindia.examples.zuul.model.DynamicRoute;

@Repository
public interface DynamicRouteRedisRepository extends CrudRepository < DynamicRoute, String > {}
  • Filter classes to be used by Zuul. These classes are default classes.

package com.cfeindia.examples.zuul.filter;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UrlPathHelper;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.http.HttpServletRequestWrapper;

@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();
  return !(requestURL.contains("proxyurl") || requestURL.contains("/admin/"));
 }

 @Override
 public Object run() {
  RequestContext ctx = RequestContext.getCurrentContext();
  HttpServletRequest request = ctx.getRequest();
  log.info("PreFilter: " +
   String.format("%s request to %s", request.getMethod(), request.getRequestURL().toString()));
  return null;
 }
}
package com.cfeindia.examples.zuul.filter;
import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;

@Component
public class RouteFilter extends ZuulFilter {
 private static Logger log = LoggerFactory.getLogger(RouteFilter.class);

 @Override
 public String filterType() {
  return "route";
 }

 @Override
 public int filterOrder() {
  return 1;
 }

 @Override
 public boolean shouldFilter() {
  RequestContext ctx = RequestContext.getCurrentContext();
  String requestURL = ctx.getRequest().getRequestURL().toString();
  return !(requestURL.contains("proxyurl") || requestURL.contains("/admin/"));
 }

 @Override
 public Object run() {

  RequestContext ctx = RequestContext.getCurrentContext();
  HttpServletRequest request = ctx.getRequest();

  log.info("RouteFilter: " + String.format("%s request to %s", request.getMethod(), request.getRequestURL().toString()));

  return null;
 }
}
package com.cfeindia.examples.zuul.filter;

import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;

@Component
public class PostFilter extends ZuulFilter {
 private static Logger log = LoggerFactory.getLogger(PostFilter.class);

 @Override
 public String filterType() {
  return "post";
 }

 @Override
 public int filterOrder() {
  return 1;
 }

 @Override
 public boolean shouldFilter() {
  RequestContext ctx = RequestContext.getCurrentContext();
  String requestURL = ctx.getRequest().getRequestURL().toString();
  return !(requestURL.contains("proxyurl") || requestURL.contains("/admin/"));
 }

 @Override
 public Object run() {
  HttpServletResponse response = RequestContext.getCurrentContext().getResponse();
  log.info("PostFilter: " + String.format("response is %s", response));
  return null;
 }
}
package com.cfeindia.examples.zuul.filter;

import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;

@Component
public class ErrorFilter extends ZuulFilter {
 private static Logger log = LoggerFactory.getLogger(PostFilter.class);

 @Override
 public String filterType() {
  return "error";
 }

 @Override
 public int filterOrder() {
  return 1;
 }

 @Override
 public boolean shouldFilter() {
  RequestContext ctx = RequestContext.getCurrentContext();
  String requestURL = ctx.getRequest().getRequestURL().toString();
  return !(requestURL.contains("proxyurl") || requestURL.contains("/admin/"));
 }

 @Override
 public Object run() {
  HttpServletResponse response = RequestContext.getCurrentContext().getResponse();
  log.info("ErrorFilter: " + String.format("response is %s", response));
  return null;
 }
}

Spring Boot application.yml with Zuul and actuator configurations:

server:
  port: ${appPort:8071}

# Actuator endpoint path (/admin/info, /admin/health, ...)
server.servlet-path: /
management.context-path: /admin
management.security.enabled: false
endpoints.health.sensitive: false

# ribbon.eureka.enabled: false
zuul:
  ignoredPatterns: /**/admin/**, /proxyurl
  routes:
    zuulDemo1:
      path: /**
      url: http://localhost/admin/health
      # stripPrefix set to true if context path is set to /
      stripPrefix: true

Now the service class has different methods for different purposes. Create the class ZuulDynamicRoutingService with the necessary dependencies, as follows:

package com.cfeindia.examples.zuul.service;

import java.util.HashSet;

import javax.annotation.PostConstruct;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties.ZuulRoute;
import org.springframework.cloud.netflix.zuul.web.ZuulHandlerMapping;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import com.cfeindia.examples.zuul.dao.DynamicRouteRedisRepository;
import com.cfeindia.examples.zuul.model.DynamicRoute;
import com.cfeindia.examples.zuul.model.DynamicRouteResponse;


@Service
public class ZuulDynamicRoutingService {

 private static final Logger logger = LoggerFactory.getLogger(ZuulDynamicRoutingService.class);
 private static final String HTTP_PROTOCOL = "http://";

 private final ZuulProperties zuulProperties;
 private final ZuulHandlerMapping zuulHandlerMapping;

 private final DynamicRouteRedisRepository dynamicRouteRedisRepository;

 @Autowired
 public ZuulDynamicRoutingService(final ZuulProperties zuulProperties, final ZuulHandlerMapping zuulHandlerMapping,
   final DynamicRouteRedisRepository dynamicRouteRedisRepository) {
   this.zuulProperties = zuulProperties;
   this.zuulHandlerMapping = zuulHandlerMapping;
   this.dynamicRouteRedisRepository = dynamicRouteRedisRepository;
  }
  .......


Add methods for creating the dynamic routes. Here is the sample request JSON:

{
"requestURIUniqueKey" : "api1UniqueKey",
"requestURI": "/api1",
"targetURLHost": "localhost",
"targetURLPort": "8081",
"targetURIPath": "/proxy-api1"
}

ZuulRouteMapping requires a unique key to add the route in its map, so the API client should always send a different unique key for different request UTIs and target details; otherwise, the new route will override the previous one. 

dynamicRoute.requestURIUniqueKey should be different in each request. It can be any string. The requestURI and target URI path should start with "/" to make the route work properly.

Method Detail to Add a Route in ZuulDynamicRoutingService

These lines are important when adding a route:

 zuulProperties.getRoutes().put(dynamicRoute.getRequestURIUniqueKey(),new ZuulRoute(dynamicRoute.getRequestURIUniqueKey(), dynamicRoute.getRequestURI() + "/**",null, url, true, false, new HashSet<>()));  zuulHandlerMapping.setDirty(true); 

public DynamicRouteResponse addDynamicRoute(DynamicRoute dynamicRoute) {
 logger.debug("request received in service to add {}", dynamicRoute);
 addDynamicRouteInZuul(dynamicRoute);

 logger.debug("going to add in cache {}", dynamicRoute);
 addToCache(dynamicRoute);
 logger.debug("added in cache {}", dynamicRoute);
 zuulHandlerMapping.setDirty(true);

 DynamicRouteResponse dynamicRouteResponse = new DynamicRouteResponse();
 logger.debug("response sent {}", dynamicRouteResponse);
 return dynamicRouteResponse;
}

private void addDynamicRouteInZuul(DynamicRoute dynamicRoute) {
 String url = createTargetURL(dynamicRoute);
 zuulProperties.getRoutes().put(dynamicRoute.getRequestURIUniqueKey(),
  new ZuulRoute(dynamicRoute.getRequestURIUniqueKey(), dynamicRoute.getRequestURI() + "/**",
   null, url, true, false, new HashSet < > ()));
}

private String createTargetURL(DynamicRoute dynamicRoute) {
 StringBuilder sb = new StringBuilder(HTTP_PROTOCOL);
 sb.append(dynamicRoute.getTargetURLHost()).append(":").append(dynamicRoute.getTargetURLPort());
 if (StringUtils.isEmpty(dynamicRoute.getTargetURIPath())) {
  sb.append("");
 } else {
  sb.append(dynamicRoute.getTargetURIPath());
 }
 String url = sb.toString();
 return url;
}

Additionally, we need to add the route details in the Redis cache:

private void addToCache(final DynamicRoute dynamicRoute) {
 DynamicRoute dynamicRouteSaved = dynamicRouteRedisRepository.save(dynamicRoute);
 logger.debug("Added in cache {}", dynamicRouteSaved);
}

Deleting the route can be done as follows, both from Zuul Mapping and Redis cache.

These lines are important while removing a route:

 ZuulRoute zuulRoute = zuulProperties.getRoutes().remove(requestURIUniqueKey);  zuulHandlerMapping.setDirty(true); 

Complete code block:

public Boolean removeDynamicRoute(final String requestURIUniqueKey) {
 DynamicRoute dynamicRoute = new DynamicRoute();
 //Removal from redis will be done from unique key. No need for other params. So create object
 //with just unique key
 dynamicRoute.setRequestURIUniqueKey(requestURIUniqueKey);
 if (zuulProperties.getRoutes().containsKey(requestURIUniqueKey)) {
  ZuulRoute zuulRoute = zuulProperties.getRoutes().remove(requestURIUniqueKey);
  logger.debug("removed the zuul route {}", zuulRoute);
  //Removal from redis will be done from unique key. No need for other params
  removeFromCache(dynamicRoute);
  zuulHandlerMapping.setDirty(true);
  return Boolean.TRUE;
 }
 return Boolean.FALSE;
}

private void removeFromCache(final DynamicRoute dynamicRoute) {
 logger.debug("removing the dynamic route {}", dynamicRoute);
 //Removal from redis will be done from unique key. No need for other params
 dynamicRouteRedisRepository.delete(dynamicRoute);
}

Restoring the routes from Redis cache at server start up can be done like this:

/**
 * Load all routes from redis cache to restore the existing routes while restarting the zuul server
 */
@PostConstruct
public void initialize() {
 try {
  dynamicRouteRedisRepository.findAll().forEach(dynamicRoute -> {
   addDynamicRouteInZuul(dynamicRoute);
  });
  zuulHandlerMapping.setDirty(true);
 } catch (Exception e) {
  logger.error("Exception in loading any previous route while restarting zuul routes.", e);
 }
}

Demo

Here is the list of snapshots demonstrating different actions and their impact on the server data. Spring Boot exposes the URLs to check the Zuul routes with the URI  /admin/routes.

The complete URL on local is http://localhost:8071/admin/routes.

Add Route:

Image title

Show added routes:

Image title

Add another route:

Image title

Show the added route again. Check the number of routes which have gone up:

Image title

Delete a route:

Image title

Show routes again after deleting the route:

Image title

To-Do

Stop the Zuul server by adding some routes. Restart the server and check the routes if they are loaded from Redis through http://localhost:8071/admin/routes.

Summary

This article explained using REST APIs to register dynamic routes in Zuul server at runtime. It saves the route details in Redis cache.

We showed how to make it fault tolerant and how restarting the Zuul server restores the previous routes from Redis cache.

This example includes saving and retrieving data using Redis. This example also demonstrates how to load data at server startup in Spring Boot/Spring MVC.

Further, another database like Mongo can be used instead of Redis to not lose the routes in better ways.

Download the project from GitHub from https://github.com/vikasanandgit/zuul-route-redis.

Here's another article about using Zuul where requests coming on a subdomain are routed to the subpath and the subdomain to subpath routes can be registered dynamically.

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

Topics:
zuul ,redis ,rest api ,tutorial ,microservices ,fault tolerance ,routing

Opinions expressed by DZone contributors are their own.

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

{{ parent.tldr }}

{{ parent.urlSource.name }}