Spring Cloud Gateway Dynamic Routes from Database

Faza Zulfika Permana Putra
Blibli.com Tech Blog
9 min readAug 27, 2021
image source : https://wstutorial.com/img/topics/apig/spring-cloud-gateway.png

After some time reading about spring cloud gateway, it is possible to configure routes through RouteLocatorBuilder class or through application.properties (application.yaml) file. Routes configuration defines gateway API path that will route user request path to corresponding backend API path of backend service.

Then I wonder, can I dynamically change or update routes when application still running ?

Right now, if we change or update routes through RouteLocatorBuilder, we need to update the code first, then rebuild and redeploy the application. So, if we change application.properties file, we need to rebuild and redeploy the application. Fortunately, I found the way and will share to you guys through this article.

Before we swim to our main topic, let’s get back to how we update routes through RouteLocatorBuilder and application.properties file.

  • To update routes using RouteLocatorBuilder, you need to build RouteLocator using RouteLocatorBuilder Bean. Here’s the example
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("get_route", r -> r.path("/get")
.method("GET")
.uri("http://httpbin.org"))
.route("post_route", r -> r.path("/post")
.method("POST")
.uri("http://httpbin.org"))
.build();
}
  • To update routes using application.properties file, you need to define some routes properties inside the application.properties or application.yaml file. Here’s the example
spring.cloud.gateway.routes[0].id=get_route
spring.cloud.gateway.routes[0].predicates[0].name=Path
spring.cloud.gateway.routes[0].predicates[0].args[patterns]=/get
spring.cloud.gateway.routes[0].predicates[1].name=Method
spring.cloud.gateway.routes[0].predicates[1].args[methods]=GET
spring.cloud.gateway.routes[0].uri=http://httpbin.org/
spring.cloud.gateway.routes[1].id=post_route
spring.cloud.gateway.routes[1].predicates[0].name=Path
spring.cloud.gateway.routes[1].predicates[0].args[patterns]=/post
spring.cloud.gateway.routes[1].predicates[1].name=Method
spring.cloud.gateway.routes[1].predicates[1].args[methods]=POST
spring.cloud.gateway.routes[1].uri=http://httpbin.org/

Database Design

For current example, we will use postgresql as database. If you are new to postgresql, and had difficulties on installing it, You can refer to my other article to install postgresql using docker.

Here’s the table design that we will use, currently we only use one table: api_route.

Here’s the SQL script to create table that we will use.

-- Table: public.api_route-- DROP TABLE public.api_route;CREATE TABLE public.api_route
(
id bigint NOT NULL DEFAULT nextval('api_route_id_seq'::regclass),
path character varying COLLATE pg_catalog."default" NOT NULL,
method character varying(10) COLLATE pg_catalog."default",
uri character varying COLLATE pg_catalog."default" NOT NULL,
CONSTRAINT api_route_pkey PRIMARY KEY (id)
)
TABLESPACE pg_default;ALTER TABLE public.api_route
OWNER to postgres;

Application Dependencies

We will use several spring dependencies to support our example.

  1. org.springframework.cloud:spring-cloud-starter-gateway → dependency for spring cloud gateway
  2. org.springframework.boot:spring-boot-starter-data-r2dbc → dependency for read-write data from database in reactive way
  3. io.r2dbc:r2dbc-postgresql → dependency for reactive postgresql database driver
  4. org.projectlombok:lombok → dependency to reduce method that we will write in our example

You can generate skeleton of project, using start.spring.io.

You can also put dependencies manually to your existing pom.xml file (please adjust some part from this snippet based on your needs).

pom.xml<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.0</version>
<relativePath/>
<!-- lookup parent from repository -->
</parent>
<groupId>com.faza.example</groupId>
<artifactId>spring-cloud-gateway-routes-from-database</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-cloud-gateway-routes-from-database</name>
<description>Demo project for Spring Boot Cloud Gateway Route from Database</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>2020.0.3</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<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>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

Database Integration

First code that we’ll write in our code is code to integrate with our database, especially to our api_route table.

To bind our table with our class, we need to create a model class. Here’s the code.

ApiRoute.javapackage com.faza.example.springcloudgatewayroutesfromdatabase.model.database;import com.faza.example.springcloudgatewayroutesfromdatabase.model.constant.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(TableName.API_ROUTE) // To bind our model class with a database table with defined name
public class ApiRoute {

@Id // Indicating that this field is primary key in our database table
private Long id;

private String path;
private String method;
private String uri;
}

Here code for TableName class, a constant class that saved all of our table name.

TableName.javapackage com.faza.example.springcloudgatewayroutesfromdatabase.model.constant;public class TableName {

public static final String API_ROUTE = "api_route";
}

Next step, we need to create a repository class to maintain our api_route data.

We can use ReactiveCrudRepository class from spring data r2dbc that contains common method (basic CRUD-CREATE-READ-UPDATE-DELETE) to maintain our api_route data.

Spring boot will automatically create this repository implementation when application start.

ApiRouteRepository.javapackage com.faza.example.springcloudgatewayroutesfromdatabase.repository;import com.faza.example.springcloudgatewayroutesfromdatabase.model.database.ApiRoute;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
public interface ApiRouteRepository extends ReactiveCrudRepository<ApiRoute, Long> {

}

After all of codes above is complete, you need to add several properties to make it work.

application.propertiesspring.r2dbc.url=r2dbc:postgresql://localhost:5432/postgres
spring.r2dbc.username=postgres
spring.r2dbc.password=postgres

Custom Gateway Route Locator

After we confirm our code integration with our database. Next, we create a new Gateway Route Locator.

As we can see in in previous section, spring use RouteLocator class to get Gateway Route configuration.

We need to make spring to use our custom RouteLocator that get Route configuration from our database.

Create a service class that get all routes data from database.

ApiRouteService.javapackage com.faza.example.springcloudgatewayroutesfromdatabase.service;import com.faza.example.springcloudgatewayroutesfromdatabase.model.database.ApiRoute;
import reactor.core.publisher.Flux;
public interface ApiRouteService {

Flux<ApiRoute> findApiRoutes();
}
--------------------------------------------------------------------ApiRouteServiceImpl.javapackage com.faza.example.springcloudgatewayroutesfromdatabase.service.impl;import com.faza.example.springcloudgatewayroutesfromdatabase.model.database.ApiRoute;
import com.faza.example.springcloudgatewayroutesfromdatabase.repository.ApiRouteRepository;
import com.faza.example.springcloudgatewayroutesfromdatabase.service.ApiRouteService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
@Service
public class ApiRouteServiceImpl implements ApiRouteService {

@Autowired
private ApiRouteRepository apiRouteRepository;
@Override
public Flux<ApiRoute> findApiRoutes() {
return apiRouteRepository.findAll();
}
}

Then let’s create our custom RouteLocator class, here’s the code:

ApiPathRouteLocatorImpl.javapackage com.faza.example.springcloudgatewayroutesfromdatabase.service.impl;import com.faza.example.springcloudgatewayroutesfromdatabase.model.database.ApiRoute;
import com.faza.example.springcloudgatewayroutesfromdatabase.service.ApiRouteService;
import lombok.AllArgsConstructor;
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.util.StringUtils;
import reactor.core.publisher.Flux;
@AllArgsConstructor
public class ApiPathRouteLocatorImpl implements RouteLocator {
private final ApiRouteService apiRouteService; private final RouteLocatorBuilder routeLocatorBuilder; @Override
public Flux<Route> getRoutes() {
RouteLocatorBuilder.Builder routesBuilder = routeLocatorBuilder.routes();
return apiRouteService.findApiRoutes()
.map(apiRoute -> routesBuilder.route(String.valueOf(apiRoute.getId()),
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());
}
}

Above, we can see that we get api routes data from database, then create a minimum configuration for Gateway Route.

We will proxy requests that match our paths and methods configuration, to our target configured urls.

Next, we will create a Bean from our custom RouteLocator for spring to get Gateway Route configuration.

GatewayConfiguration.javapackage com.faza.example.springcloudgatewayroutesfromdatabase.configuration;import com.faza.example.springcloudgatewayroutesfromdatabase.service.ApiRouteService;
import com.faza.example.springcloudgatewayroutesfromdatabase.service.impl.ApiPathRouteLocatorImpl;
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(ApiRouteService apiRouteService,
RouteLocatorBuilder routeLocatorBuilder) {
return new ApiPathRouteLocatorImpl(apiRouteService, routeLocatorBuilder);
}
}

For testing purposes, let’s insert some data to our api_route table. Here’s an example SQL that create route for path /get and method GET proxied to https://httpbin.org/get.

INSERT INTO public.api_route(
path, method, uri)
VALUES ('/get', 'GET', 'https://httpbin.org/get');

Start your application, and try to to hit path /get with GET method. It will bring back response from https://httpbin.org/get.

{
"args": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate, br",
"Cache-Control": "no-cache",
"Content-Length": "85",
"Content-Type": "application/json",
"Forwarded": "proto=http;host=\"localhost:8080\";for=\"0:0:0:0:0:0:0:1:56874\"",
"Host": "httpbin.org",
"Postman-Token": "5d15e198-d92d-4e7e-bd2b-5ca9ee534180",
"User-Agent": "PostmanRuntime/7.28.0",
"X-Amzn-Trace-Id": "Root=1-60c6f715-0ff7a5cf5dd8a6745d36c6e5",
"X-Forwarded-Host": "localhost:8080"
},
"origin": "0:0:0:0:0:0:0:1",
"url": "https://localhost:8080/get"
}

If you try to hit /get with another method, like POST, it will return 404 response.

{
"timestamp": "2021-06-14T06:28:46.722+00:00",
"path": "/get",
"status": 404,
"error": "Not Found",
"message": null,
"requestId": "3e2420c9-4"
}

Internal API to Maintain Our Api Route

Up to this point, we managed to get our api route data through database. However, if we modify our api route data in database while the system is running, those changes will not applied right away.

Please be patient, we will go and fix it one by one.

First, we will create several endpoints to maintain our api route data when our application is running.

We need to modify our ApiRouteService and ApiRouteServiceImpl to support other action, like Create, Update, and Delete.

ApiRouteService.javapackage com.faza.example.springcloudgatewayroutesfromdatabase.service;import com.faza.example.springcloudgatewayroutesfromdatabase.model.database.ApiRoute;
import com.faza.example.springcloudgatewayroutesfromdatabase.model.web.CreateOrUpdateApiRouteRequest;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface ApiRouteService {

Flux<ApiRoute> findApiRoutes();

Mono<ApiRoute> findApiRoute(Long id);

Mono<Void> createApiRoute(CreateOrUpdateApiRouteRequest createOrUpdateApiRouteRequest);

Mono<Void> updateApiRoute(Long id, CreateOrUpdateApiRouteRequest createOrUpdateApiRouteRequest);

Mono<Void> deleteApiRoute(Long id);
}
--------------------------------------------------------------------ApiRouteServiceImpl.javapackage com.faza.example.springcloudgatewayroutesfromdatabase.service.impl;import com.faza.example.springcloudgatewayroutesfromdatabase.model.database.ApiRoute;
import com.faza.example.springcloudgatewayroutesfromdatabase.model.web.CreateOrUpdateApiRouteRequest;
import com.faza.example.springcloudgatewayroutesfromdatabase.repository.ApiRouteRepository;
import com.faza.example.springcloudgatewayroutesfromdatabase.service.ApiRouteService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Service
public class ApiRouteServiceImpl implements ApiRouteService {

@Autowired
private ApiRouteRepository apiRouteRepository;
@Override
public Flux<ApiRoute> findApiRoutes() {
return apiRouteRepository.findAll();
}
@Override
public Mono<ApiRoute> findApiRoute(Long id) {
return findAndValidateApiRoute(id);
}
@Override
public Mono<Void> createApiRoute(CreateOrUpdateApiRouteRequest createOrUpdateApiRouteRequest) {
ApiRoute apiRoute = setNewApiRoute(new ApiRoute(), createOrUpdateApiRouteRequest);
return apiRouteRepository.save(apiRoute)
.then();
}
@Override
public Mono<Void> updateApiRoute(Long id,
CreateOrUpdateApiRouteRequest createOrUpdateApiRouteRequest) {
return findAndValidateApiRoute(id)
.map(apiRoute -> setNewApiRoute(apiRoute, createOrUpdateApiRouteRequest))
.flatMap(apiRouteRepository::save)
.then();
}
@Override
public Mono<Void> deleteApiRoute(Long id) {
return findAndValidateApiRoute(id)
.flatMap(apiRoute -> apiRouteRepository.deleteById(apiRoute.getId()));
}

private Mono<ApiRoute> findAndValidateApiRoute(Long id) {
return apiRouteRepository.findById(id)
.switchIfEmpty(Mono.error(
new RuntimeException(String.format("Api route with id %d not found", id))));
}

private ApiRoute setNewApiRoute(ApiRoute apiRoute,
CreateOrUpdateApiRouteRequest createOrUpdateApiRouteRequest) {
apiRoute.setPath(createOrUpdateApiRouteRequest.getPath());
apiRoute.setMethod(createOrUpdateApiRouteRequest.getMethod());
apiRoute.setUri(createOrUpdateApiRouteRequest.getUri());
return apiRoute;
}
}

Then, we need to create a controller for all of our actions.

InternalApiRouteController.javapackage com.faza.example.springcloudgatewayroutesfromdatabase.controller;import com.faza.example.springcloudgatewayroutesfromdatabase.model.constant.ApiPath;
import com.faza.example.springcloudgatewayroutesfromdatabase.model.database.ApiRoute;
import com.faza.example.springcloudgatewayroutesfromdatabase.model.web.ApiRouteResponse;
import com.faza.example.springcloudgatewayroutesfromdatabase.model.web.CreateOrUpdateApiRouteRequest;
import com.faza.example.springcloudgatewayroutesfromdatabase.service.ApiRouteService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.util.List;@RestController
@RequestMapping(path = ApiPath.INTERNAL_API_ROUTES)
public class InternalApiRouteController {

@Autowired
private ApiRouteService apiRouteService;

@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
public Mono<List<ApiRouteResponse>> findApiRoutes() {
return apiRouteService.findApiRoutes()
.map(this::convertApiRouteIntoApiRouteResponse)
.collectList()
.subscribeOn(Schedulers.boundedElastic());
}

@GetMapping(path = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public Mono<ApiRouteResponse> findApiRoute(@PathVariable Long id) {
return apiRouteService.findApiRoute(id)
.map(this::convertApiRouteIntoApiRouteResponse)
.subscribeOn(Schedulers.boundedElastic());
}

@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.CREATED)
public Mono<?> createApiRoute(
@RequestBody @Validated CreateOrUpdateApiRouteRequest createOrUpdateApiRouteRequest) {
return apiRouteService.createApiRoute(createOrUpdateApiRouteRequest)
.subscribeOn(Schedulers.boundedElastic());
}

@PutMapping(path = "/{id}",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.NO_CONTENT)
public Mono<?> updateApiRoute(@PathVariable Long id,
@RequestBody @Validated CreateOrUpdateApiRouteRequest createOrUpdateApiRouteRequest) {
return apiRouteService.updateApiRoute(id, createOrUpdateApiRouteRequest)
.subscribeOn(Schedulers.boundedElastic());
}

@DeleteMapping(path = "/{id}",
produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.NO_CONTENT)
public Mono<?> deleteApiRoute(@PathVariable Long id) {
return apiRouteService.deleteApiRoute(id)
.subscribeOn(Schedulers.boundedElastic());
}

private ApiRouteResponse convertApiRouteIntoApiRouteResponse(ApiRoute apiRoute) {
return ApiRouteResponse.builder()
.id(apiRoute.getId())
.path(apiRoute.getPath())
.method(apiRoute.getMethod())
.uri(apiRoute.getUri())
.build();
}
}

In our controller, we need a constant class for our api path, here’s the ApiPath class.

ApiPath.javapackage com.faza.example.springcloudgatewayroutesfromdatabase.model.constant;public class ApiPath {

public static final String INTERNAL = "/internal";
public static final String INTERNAL_API_ROUTES = INTERNAL + "/api-routes";
}

And in our controller and service, we need some extra model beside our database model.

CreateOrUpdateApiRouteRequest.javapackage com.faza.example.springcloudgatewayroutesfromdatabase.model.web;import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;@Data
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class CreateOrUpdateApiRouteRequest {
@NotBlank
private String path;
private String method; @NotBlank
private String uri;
}
--------------------------------------------------------------------ApiRouteResponse.javapackage com.faza.example.springcloudgatewayroutesfromdatabase.model.web;import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class ApiRouteResponse {
private Long id; private String path;
private String method;
private String uri;
}

Now, we can maintain our api route data dynamically when application is running.

But still, after we update our api route data, our Gateway Route configuration do not updated automatically.

We will take this in next section.

Gateway Route Refresher

In this section we will update our Gateway Route configuration after we successfully update our api route data through API.

To trigger the route update, we need to publish RefreshRoutesEvent from spring ApplicationEventPublisher.

Create a new service class to publish RefreshRoutesEvent.

GatewayRouteService.javapackage com.faza.example.springcloudgatewayroutesfromdatabase.service;public interface GatewayRouteService {

void refreshRoutes();
}
--------------------------------------------------------------------GatewayRouteServiceImpl.javapackage com.faza.example.springcloudgatewayroutesfromdatabase.service.impl;import com.faza.example.springcloudgatewayroutesfromdatabase.service.GatewayRouteService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
@Service
public class GatewayRouteServiceImpl implements GatewayRouteService {

@Autowired
private ApplicationEventPublisher applicationEventPublisher;
@Override
public void refreshRoutes() {
applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));
}
}

Call refreshRoutes method from our GatewayRouteService, in our ApiRouteServiceImpl to refresh api route data from database.

ApiRouteServiceImpl.javapackage com.faza.example.springcloudgatewayroutesfromdatabase.service.impl;import com.faza.example.springcloudgatewayroutesfromdatabase.model.database.ApiRoute;
import com.faza.example.springcloudgatewayroutesfromdatabase.model.web.CreateOrUpdateApiRouteRequest;
import com.faza.example.springcloudgatewayroutesfromdatabase.repository.ApiRouteRepository;
import com.faza.example.springcloudgatewayroutesfromdatabase.service.ApiRouteService;
import com.faza.example.springcloudgatewayroutesfromdatabase.service.GatewayRouteService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Service
public class ApiRouteServiceImpl implements ApiRouteService {

@Autowired
private ApiRouteRepository apiRouteRepository;

@Autowired
private GatewayRouteService gatewayRouteService;
@Override
public Flux<ApiRoute> findApiRoutes() {
return apiRouteRepository.findAll();
}
@Override
public Mono<ApiRoute> findApiRoute(Long id) {
return findAndValidateApiRoute(id);
}
@Override
public Mono<Void> createApiRoute(CreateOrUpdateApiRouteRequest createOrUpdateApiRouteRequest) {
ApiRoute apiRoute = setNewApiRoute(new ApiRoute(), createOrUpdateApiRouteRequest);
return apiRouteRepository.save(apiRoute)
.doOnSuccess(obj -> gatewayRouteService.refreshRoutes())
.then();
}
@Override
public Mono<Void> updateApiRoute(Long id,
CreateOrUpdateApiRouteRequest createOrUpdateApiRouteRequest) {
return findAndValidateApiRoute(id)
.map(apiRoute -> setNewApiRoute(apiRoute, createOrUpdateApiRouteRequest))
.flatMap(apiRouteRepository::save)
.doOnSuccess(obj -> gatewayRouteService.refreshRoutes())
.then();
}
@Override
public Mono<Void> deleteApiRoute(Long id) {
return findAndValidateApiRoute(id)
.flatMap(apiRoute -> apiRouteRepository.deleteById(apiRoute.getId()))
.doOnSuccess(obj -> gatewayRouteService.refreshRoutes());
}

private Mono<ApiRoute> findAndValidateApiRoute(Long id) {
return apiRouteRepository.findById(id)
.switchIfEmpty(Mono.error(
new RuntimeException(String.format("Api route with id %d not found", id))));
}

private ApiRoute setNewApiRoute(ApiRoute apiRoute,
CreateOrUpdateApiRouteRequest createOrUpdateApiRouteRequest) {
apiRoute.setPath(createOrUpdateApiRouteRequest.getPath());
apiRoute.setMethod(createOrUpdateApiRouteRequest.getMethod());
apiRoute.setUri(createOrUpdateApiRouteRequest.getUri());
return apiRoute;
}
}

Finaly, we now have automatically update routes on database change. You can try to update api route data through api, and your Gateway Route configuration will be updated.

Conclusion

From this article, we are able to update route source for Gateway Route by creating custom RouteLocator class to be used by spring Gateway Route configuration.

To refresh our Gateway Route configuration, we need to publish RefreshRoutesEvent from spring ApplicationEventPublisher.

You can find full codes of this example in my github repository.

Next article, Spring Cloud Gateway custom Api Limiter.

--

--