Building a Gateway

This guide walks you through how to use the Spring Cloud Gateway

What You Will Build

You will build a gateway using Spring Cloud Gateway.

What You Need

How to complete this guide

Like most Spring Getting Started guides, you can start from scratch and complete each step or you can bypass basic setup steps that are already familiar to you. Either way, you end up with working code.

To start from scratch, move on to Starting with Spring Initializr.

To skip the basics, do the following:

When you finish, you can check your results against the code in gs-gateway/complete.

Starting with Spring Initializr

You can use this pre-initialized project and click Generate to download a ZIP file. This project is configured to fit the examples in this tutorial.

To manually initialize the project:

  1. Navigate to https://start.spring.io. This service pulls in all the dependencies you need for an application and does most of the setup for you.

  2. Choose either Gradle or Maven and the language you want to use. This guide assumes that you chose Java.

  3. Click Dependencies and select Gateway, Resilience4J, and Contract Stub Runner.

  4. Click Generate.

  5. Download the resulting ZIP file, which is an archive of a web application that is configured with your choices.

If your IDE has the Spring Initializr integration, you can complete this process from your IDE.
You can also fork the project from Github and open it in your IDE or other editor.

Creating A Simple Route

The Spring Cloud Gateway uses routes to process requests to downstream services. In this guide, we route all of our requests to HTTPBin. Routes can be configured a number of ways, but, for this guide, we use the Java API provided by the Gateway.

To get started, create a new Bean of type RouteLocator in Application.java.

src/main/java/gateway/Application.java

@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
    return builder.routes().build();
}

The myRoutes method takes in a RouteLocatorBuilder that can be used to create routes. In addition to creating routes, RouteLocatorBuilder lets you add predicates and filters to your routes so that you can route handle based on certain conditions as well as alter the request/response as you see fit.

Now we can create a route that routes a request to https://httpbin.org/get when a request is made to the Gateway at /get. In our configuration of this route, we add a filter that adds the Hello request header with a value of World to the request before it is routed:

src/main/java/gateway/Application.java

@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
    return builder.routes()
        .route(p -> p
            .path("/get")
            .filters(f -> f.addRequestHeader("Hello", "World"))
            .uri("http://httpbin.org:80"))
        .build();
}

To test our simple Gateway, we can run Application.java on port 8080. Once the application is running, make a request to http://localhost:8080/get. You can do so by using the following cURL command in your terminal:

$ curl http://localhost:8080/get

You should receive a response back that looks similar to the following output:

{
  "args": {},
  "headers": {
    "Accept": "*/*",
    "Connection": "close",
    "Forwarded": "proto=http;host=\"localhost:8080\";for=\"0:0:0:0:0:0:0:1:56207\"",
    "Hello": "World",
    "Host": "httpbin.org",
    "User-Agent": "curl/7.54.0",
    "X-Forwarded-Host": "localhost:8080"
  },
  "origin": "0:0:0:0:0:0:0:1, 73.68.251.70",
  "url": "http://localhost:8080/get"
}

Note that HTTPBin shows that the Hello header with a value of World was sent in the request.

Using Spring Cloud CircuitBreaker

Now we can do something a little more interesting. Since the services behind the Gateway could potentially behave poorly and affect our clients, we might want to wrap the routes we create in circuit breakers. You can do so in the Spring Cloud Gateway by using the Resilience4J Spring Cloud CircuitBreaker implementation. This is implemented through a simple filter that you can add to your requests. We can create another route to demonstrate this.

To use this filter you need to add the reactive Resilience4J CircuitBreaker dependency to your classpath.

pom.xml

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
    </dependency>

build.gradle

  implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-reactor-resilience4j'

In the next example, we use HTTPBin’s delay API, which waits a certain number of seconds before sending a response. Since this API could potentially take a long time to send its response, we can wrap the route that uses this API in a circuit breaker. The following listing adds a new route to our RouteLocator object:

src/main/java/gateway/Application.java

@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
    return builder.routes()
        .route(p -> p
            .path("/get")
            .filters(f -> f.addRequestHeader("Hello", "World"))
            .uri("http://httpbin.org:80"))
        .route(p -> p
            .host("*.circuitbreaker.com")
            .filters(f -> f.circuitBreaker(config -> config.setName("mycmd")))
            .uri("http://httpbin.org:80")).
        build();
}

There are some differences between this new route configuration and the previous one we created. For one, we use the host predicate instead of the path predicate. This means that, as long as the host is circuitbreaker.com, we route the request to HTTPBin and wrap that request in a circuit breaker. We do so by applying a filter to the route. We can configure the circuit breaker filter by using a configuration object. In this example, we give the circuit breaker a name of mycmd.

Now we can test this new route. To do so, we need to start the application, but, this time, we are going to make a request to /delay/3. It is also important that we include a Host header that has a host of circuitbreaker.com. Otherwise, the request is not routed. We can use the following cURL command:

$ curl --dump-header - --header 'Host: www.circuitbreaker.com' http://localhost:8080/delay/3
We use --dump-header to see the response headers. The - after --dump-header tells cURL to print the headers to stdout.

After using this command, you should see the following in your terminal:

HTTP/1.1 504 Gateway Timeout
content-length: 0

As you can see the circuit breaker timed out while waiting for the response from HTTPBin. When the circuit breaker times out, we can optionally provide a fallback so that clients do not receive a 504 but something more meaningful. In a production scenario, you might return some data from a cache, for example, but, in our simple example, we return a response with the body fallback instead.

To do so, we can modify our circuit breaker filter to provide a URL to call in the case of a timeout:

src/main/java/gateway/Application.java

@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
    return builder.routes()
        .route(p -> p
            .path("/get")
            .filters(f -> f.addRequestHeader("Hello", "World"))
            .uri("http://httpbin.org:80"))
        .route(p -> p
            .host("*.circuitbreaker.com")
            .filters(f -> f.circuitBreaker(config -> config
                .setName("mycmd")
                .setFallbackUri("forward:/fallback")))
            .uri("http://httpbin.org:80"))
        .build();
}

Now, when the circuit breaker wrapped route times out, it calls /fallback in the Gateway application. Now we can add the /fallback endpoint to our application.

In Application.java, we add the @RestController class level annotation and then add the following @RequestMapping to the class:

src/main/java/gateway/Application.java

@RequestMapping("/fallback")
public Mono<String> fallback() {
  return Mono.just("fallback");
}

To test this new fallback functionality, restart the application and again issue the following cURL command

$ curl --dump-header - --header 'Host: www.circuitbreaker.com' http://localhost:8080/delay/3

With the fallback in place, we now see that we get a 200 back from the Gateway with the response body of fallback.

HTTP/1.1 200 OK
transfer-encoding: chunked
Content-Type: text/plain;charset=UTF-8

fallback

Writing Tests

As a good developer, we should write some tests to make sure our Gateway is doing what we expect it should. In most cases, we want to limit our dependencies on outside resources, especially in unit tests, so we should not depend on HTTPBin. One solution to this problem is to make the URI in our routes configurable, so we can change the URI if we need to.

To do so, in Application.java, we can create a new class called UriConfiguration:

@ConfigurationProperties
class UriConfiguration {
  
  private String httpbin = "http://httpbin.org:80";

  public String getHttpbin() {
    return httpbin;
  }

  public void setHttpbin(String httpbin) {
    this.httpbin = httpbin;
  }
}

To enable ConfigurationProperties, we need to also add a class-level annotation to Application.java.

@EnableConfigurationProperties(UriConfiguration.class)

With our new configuration class in place, we can use it in the myRoutes method:

src/main/java/gateway/Application.java

@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder, UriConfiguration uriConfiguration) {
  String httpUri = uriConfiguration.getHttpbin();
  return builder.routes()
    .route(p -> p
      .path("/get")
      .filters(f -> f.addRequestHeader("Hello", "World"))
      .uri(httpUri))
    .route(p -> p
      .host("*.circuitbreaker.com")
      .filters(f -> f
        .circuitBreaker(config -> config
          .setName("mycmd")
          .setFallbackUri("forward:/fallback")))
      .uri(httpUri))
    .build();
}

Instead of hardcoding the URL to HTTPBin, we instead get the URL from our new configuration class.

The following listing shows the complete contents of Application.java:

src/main/java/gateway/Application.java

@SpringBootApplication
@EnableConfigurationProperties(UriConfiguration.class)
@RestController
public class Application {

  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }

  @Bean
  public RouteLocator myRoutes(RouteLocatorBuilder builder, UriConfiguration uriConfiguration) {
    String httpUri = uriConfiguration.getHttpbin();
    return builder.routes()
      .route(p -> p
        .path("/get")
        .filters(f -> f.addRequestHeader("Hello", "World"))
        .uri(httpUri))
      .route(p -> p
        .host("*.circuitbreaker.com")
        .filters(f -> f
          .circuitBreaker(config -> config
            .setName("mycmd")
            .setFallbackUri("forward:/fallback")))
        .uri(httpUri))
      .build();
  }

  @RequestMapping("/fallback")
  public Mono<String> fallback() {
    return Mono.just("fallback");
  }
}

@ConfigurationProperties
class UriConfiguration {
  
  private String httpbin = "http://httpbin.org:80";

  public String getHttpbin() {
    return httpbin;
  }

  public void setHttpbin(String httpbin) {
    this.httpbin = httpbin;
  }
}

Now we can create a new class called ApplicationTest in src/main/test/java/gateway. In the new class, we add the following content:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
    properties = {"httpbin=http://localhost:${wiremock.server.port}"})
@AutoConfigureWireMock(port = 0)
public class ApplicationTest {

  @Autowired
  private WebTestClient webClient;

  @Test
  public void contextLoads() throws Exception {
    //Stubs
    stubFor(get(urlEqualTo("/get"))
        .willReturn(aResponse()
          .withBody("{\"headers\":{\"Hello\":\"World\"}}")
          .withHeader("Content-Type", "application/json")));
    stubFor(get(urlEqualTo("/delay/3"))
      .willReturn(aResponse()
        .withBody("no fallback")
        .withFixedDelay(3000)));

    webClient
      .get().uri("/get")
      .exchange()
      .expectStatus().isOk()
      .expectBody()
      .jsonPath("$.headers.Hello").isEqualTo("World");

    webClient
      .get().uri("/delay/3")
      .header("Host", "www.circuitbreaker.com")
      .exchange()
      .expectStatus().isOk()
      .expectBody()
      .consumeWith(
        response -> assertThat(response.getResponseBody()).isEqualTo("fallback".getBytes()));
  }
}

Our test takes advantage of WireMock from Spring Cloud Contract stand up a server that can mock the APIs from HTTPBin. The first thing to notice is the use of @AutoConfigureWireMock(port = 0). This annotation starts WireMock on a random port for us.

Next, note that we take advantage of our UriConfiguration class and set the httpbin property in the @SpringBootTest annotation to the WireMock server running locally. Within the test, we then setup “stubs” for the HTTPBin APIs we call through the Gateway and mock the behavior we expect. Finally, we use WebTestClient to make requests to the Gateway and validate the responses.

Summary

Congratulations! You have just built your first Spring Cloud Gateway application!

Want to write a new guide or contribute to an existing one? Check out our contribution guidelines.

All guides are released with an ASLv2 license for the code, and an Attribution, NoDerivatives creative commons license for the writing.

Get the Code