Spring Cloud Gateway — Dynamic Route Configuration and Loading from the Datastore
Spring Cloud Gateway is the successor of the Spring Cloud Zuul API Gateway. Spring Cloud Gateway is built on the reactive programming model using Project Reactor and the Spring WebFlux framework. This enables it to handle many concurrent connections efficiently, making it well-suited for reactive and non-blocking applications. The Spring Cloud Gateway provides more dynamic configuration options where routes and filters can be configured dynamically without the need to restart the application.
In the article, we would like to explore how we can manage (add, update, and delete) the route configurations of an API Gateway from the data store. This enables you to build the API Gateway across the multiple APIs across your organization and offer API Gateway as an enablement for your teams.
As we mentioned in our previous article, API Gateway routes can be configured from the application configuration (using the YAML or properties file) or through a programmatic approach (Route Locator Builder).
Yaml Configuration example
spring:
cloud:
gateway:
routes:
- id: order-service
uri: http://localhost:8081
predicates:
- Path=/orders/** # Path predicate for URI path matching
Programmatic Approach Example
package com.techmonks.apigateway.configuration;
import com.techmonks.apigateway.filters.RequestAndResponseLogGlobalFilter;
import com.techmonks.apigateway.service.RouteService;
import com.techmonks.apigateway.service.impl.ApiRouteLocatorImpl;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class GatewayConfiguration {
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder routeLocatorBuilder) {
return routeLocatorBuilder.routes().route("order-service",
route -> route.path("/orders/**")
.filters(filter -> {
filter.addResponseHeader("res-header", "res-header-value");
return filter;
})
.uri("http://localhost:8081"))
.build();
}
}
In either of the above-mentioned approaches, you need to redeploy the application in case of a code change.
By moving the route configurations to the database, we can make our API Gateway more flexible and configurable.
Implementation Approach
Before delving into the implementation details, let us look at a few important aspects of Spring Cloud Gateway.
- RouteLocator: It is an interface provided by Spring Cloud Gateway that defines a contract for classes responsible for locating and defining routes.
- RouteLocatorBuilder: It is a builder class provided by Spring Cloud Gateway to facilitate the construction of routes fluently and programmatically. It is often used in combination with the RouteLocator interface to define the routing configuration for the API gateway.
In this strategy, we persist route configurations in MongoDB. We create a CustomRouteLocator by implementing the RouteLocator interface to retrieve routes from the database dynamically. Routes are then registered with RouteLocatorBuilder on demand.
One other important aspect is reactive programming. Having awareness about reactive programming immensely helps when customizing the spring cloud gateway as per your organization's needs.
Let us get started with the implementation.
Step 1: Let us define a simple entity to store the route configurations.
package com.techmonks.apigateway.entity;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@Getter
@Setter
@Document("apiRoutes")
public class ApiRoute {
@Id
private String id;
private String routeIdentifier;
private String uri;
private String method;
private String path;
}
Step 2: Implement a repository to manage the route configurations.
package com.techmonks.apigateway.repository;
import com.techmonks.apigateway.entity.ApiRoute;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface RouteRepository extends ReactiveCrudRepository<ApiRoute, String> {
}
Step 3: Implement a service class to access the repository methods.
package com.techmonks.apigateway.service.impl;
import com.techmonks.apigateway.entity.ApiRoute;
import com.techmonks.apigateway.repository.RouteRepository;
import com.techmonks.apigateway.service.RouteService;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Service
public class RouteServiceImpl implements RouteService {
private final RouteRepository routeRepository;
public RouteServiceImpl(RouteRepository routeRepository) {
this.routeRepository = routeRepository;
}
@Override
public Flux<ApiRoute> getAll() {
return this.routeRepository.findAll();
}
@Override
public Mono<ApiRoute> create(ApiRoute apiRoute) {
Mono<ApiRoute> route = this.routeRepository.save(apiRoute);
return route;
}
@Override
public Mono<ApiRoute> getById(String id) {
return this.routeRepository.findById(id);
}
}
Step 4: Implement CustomRouteLocator to load the API Routes from the database and register with RouteLocatorBuilder.
package com.techmonks.apigateway.service.impl;
import com.techmonks.apigateway.service.RouteService;
import lombok.RequiredArgsConstructor;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.BooleanSpec;
import org.springframework.cloud.gateway.route.builder.Buildable;
import org.springframework.cloud.gateway.route.builder.PredicateSpec;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import com.techmonks.apigateway.entity.ApiRoute;
import reactor.core.publisher.Flux;
import java.util.Map;
@RequiredArgsConstructor
@Service
public class ApiRouteLocatorImpl implements RouteLocator {
private final RouteLocatorBuilder routeLocatorBuilder;
private final RouteService routeService;
@Override
public Flux<Route> getRoutes() {
RouteLocatorBuilder.Builder routesBuilder = routeLocatorBuilder.routes();
return routeService.getAll()
.map(apiRoute -> routesBuilder.route(String.valueOf(apiRoute.getRouteIdentifier()),
predicateSpec -> setPredicateSpec(apiRoute, predicateSpec)))
.collectList()
.flatMapMany(builders -> routesBuilder.build()
.getRoutes());
}
private Buildable<Route> setPredicateSpec(ApiRoute apiRoute, PredicateSpec predicateSpec) {
BooleanSpec booleanSpec = predicateSpec.path(apiRoute.getPath());
if (!StringUtils.isEmpty(apiRoute.getMethod())) {
booleanSpec.and()
.method(apiRoute.getMethod());
}
return booleanSpec.uri(apiRoute.getUri());
}
@Override
public Flux<Route> getRoutesByMetadata(Map<String, Object> metadata) {
return vRouteLocator.super.getRoutesByMetadata(metadata);
}
}
Step 5: Let us add the below configuration to load our customRouteLocator.
package com.techmonks.apigateway.configuration;
import com.techmonks.apigateway.filters.RequestAndResponseLogGlobalFilter;
import com.techmonks.apigateway.service.RouteService;
import com.techmonks.apigateway.service.impl.ApiRouteLocatorImpl;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class GatewayConfiguration {
private RequestAndResponseLogGlobalFilter requestAndResponseLogGlobalFilter;
@Bean
public RouteLocator routeLocator(RouteService routeService, RouteLocatorBuilder routeLocationBuilder) {
return new ApiRouteLocatorImpl(routeLocationBuilder, routeService);
}
}
Step 5: Spring WebFlux offers annotation-based and functional programming models. In this article, we will use a functional-based programming model. As part of it, let us define the handler.
package com.techmonks.apigateway.handler;
import com.techmonks.apigateway.configuration.GatewayRoutesRefresher;
import com.techmonks.apigateway.entity.ApiRoute;
import com.techmonks.apigateway.service.RouteService;
import lombok.RequiredArgsConstructor;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import static org.springframework.web.reactive.function.BodyInserters.fromValue;
@RequiredArgsConstructor
@Component
public class ApiRouteHandler {
private final RouteService routeService;
private final RouteLocator routeLocator;
private final GatewayRoutesRefresher gatewayRoutesRefresher;
public Mono<ServerResponse> create(ServerRequest serverRequest) {
Mono<ApiRoute> apiRoute = serverRequest.bodyToMono(ApiRoute.class);
return apiRoute.flatMap(route ->
ServerResponse.status(HttpStatus.OK)
.contentType(MediaType.APPLICATION_JSON)
.body(routeService.create(route), ApiRoute.class));
}
public Mono<ServerResponse> getById(ServerRequest serverRequest) {
final String apiId = serverRequest.pathVariable("routeId");
Mono<ApiRoute> apiRoute = routeService.getById(apiId);
return apiRoute.flatMap(route -> ServerResponse.ok()
.body(fromValue(route)))
.switchIfEmpty(ServerResponse.notFound()
.build());
}
public Mono<ServerResponse> refreshRoutes(ServerRequest serverRequest) {
gatewayRoutesRefresher.refreshRoutes();
return ServerResponse.ok().body(BodyInserters.fromObject("Routes reloaded successfully"));
}
}
Step 6: Implement a Router to expose the endpoints. Below is a sample.
package com.techmonks.apigateway.router;
import com.techmonks.apigateway.handler.ApiRouteHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
@Configuration
public class ApiRouteRouter {
@Bean
public RouterFunction<ServerResponse> route(ApiRouteHandler apiRouteHandler) {
return RouterFunctions.route(POST("/routes")
.and(accept(MediaType.APPLICATION_JSON)), apiRouteHandler::create)
.andRoute(GET("/routes/:routeId")
.and(accept(MediaType.APPLICATION_JSON)), apiRouteHandler::getById)
.andRoute(GET("/routes/refresh-routes")
.and(accept(MediaType.APPLICATION_JSON)), apiRouteHandler::refreshRoutes);
}
}
Now, APIs are ready to create the routes dynamically through REST endpoints.
Reload the Route Configurations
To reload the route configurations dynamically, you need to implement ‘ApplicationPublisherAware’ and trigger the event to refresh routes when needed. Below is the sample implementation.
package com.techmonks.apigateway.configuration;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Component;
@Component
public class GatewayRoutesRefresher implements ApplicationEventPublisherAware {
private ApplicationEventPublisher applicationEventPublisher;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
/**
* Refresh the routes to load from data store
*/
public void refreshRoutes() {
applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));
}
}
After defining the class, you can call the refreshRoutes method to reload the routes dynamically. The refresh events endpoint is added to the above implementation.
You can create a REST endpoint to refresh the routes whenever needed.
Next Steps: If you closely look at the refresh routes implementation, it may not work as expected when multiple instances are running in the distributed environment. This is because routes are managed internally by the running instance. To extend it to a distributed environment, one approach would be to implement a scheduler that verifies the version of the routes loaded and refreshes whenever there is a change in the version of the routes.
As always, you can find the complete source code on GitHub.
That’s all for today!
Thank you for taking the time to read this article. I hope you have enjoyed it. If you enjoyed it and would like to stay updated on various technology topics, please consider subscribing for more insightful content.