How To Implement a Gateway With Spring Cloud
Learn how to implement a gateway component for a microservice system with the Spring Cloud Gateway package.
Join the DZone community and get the full member experience.
Join For FreeA microservice system could have a high number of components with complex interactions. It is important to reduce this complexity, at least from the standpoint of the clients interacting with the system. A gateway hides the microservices from the external world. It represents a unique entrance and implements common cross requirements. In this article, you will learn how to configure a gateway component for a Spring Boot application, using the Spring Cloud Gateway package.
Spring Cloud Gateway
Spring Cloud provides a gateway implementation by the Spring Cloud Gateway project. It is based on Spring Boot, Spring WebFlux, and Reactor. Since it is based on Spring WebFlux, it must run on a Netty environment, not a usual servlet container.
The main function of a gateway is to route requests from external clients to the microservices. Its main components are:
- Route: This is the basic entity. It is configured with an ID, a destination URI, one or more predicates, and filters.
- Predicate: This is based on a Java function predicate. It represents a condition that must be matched on head or request parameters.
- Filter: It allows you to change the request or the response.
We can identify the following sequence of events:
- A client makes a call through the gateway.
- The gateway decides if the request matches a configured route.
- If there is a match, the request is sent to a gateway web handler.
- The web handler sends the request to a chain of filters that can execute logic relative to the request or the response, and operate changes on them.
- The target service is executed.
Spring Cloud Gateway Dependencies
To implement our Spring Boot application as a gateway we must first provide the spring-cloud-starter-gateway
dependency after having defined the release train as in the configuration fragment below:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2023.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
...
</dependencies>
Spring Cloud Gateway Configuration
We can configure our gateway component using the application.yaml file. We can specify fully expanded arguments or shortcuts to define predicates and filters. In the first case, we define a name
and an args
field. The args
field can contain one or more key-value pairs:
spring:
cloud:
gateway:
routes:
- id: route-example
uri: https://example.com
predicates:
- name: PredicateExample
args:
name: predicateName
regexp: predicateRegexpValue
In the example above, we define a route with an ID value of "route-example
", a destination URI "https://example.com
," and a predicate with two args
, "name
" and "regexp
."
With the shortcut mode, we write the predicate name followed by the "=
" character, and then a list of names and values separated by commas. We can rewrite the previous example by the following:
spring:
cloud:
gateway:
routes:
- id: route-example
uri: https://example.com
predicates:
- Cookie=predicateName,predicateRegexpValue
A specific factory class implements each predicate and filter type. There are several built-in predicate and filter factories available. The Cookie
predicate shown above is an example. We will list some of them in the following sub-sections.
Predicate Built-In Factories
- The
After Predicate Factory
matches requests that happen after a specific time.
After=2007-12-03T10:15:30+01:00 Europe/Paris
- The
Before Predicate Factory
matches requests that happen before a specific time.
Before=2007-12-03T10:15:30+01:00 Europe/Paris
- The
Method Route Predicate Factory
specifies the HTTP method types to match.
Method=GET,POST
Filter Built-In Factories
- The
AddRequestHeader GatewayFilter Factory
allows the addition of an HTTP header to the request by its name and value.- AddRequestHeader=X-Request-Foo, Bar
- The
AddRequestParameter GatewayFilter Factory
allows the addition of a parameter to the request by its name and value.- AddRequestParameter=foo, bar
- The
AddResponseHeader GatewayFilter Factory
allows the addition of an HTTP header to the request by its name and value.- AddResponseHeader=X-Response-Foo, Bar
To implement a custom predicate or filter factory, we have to provide an implementation of a specific factory interface. The following sections show how.
Custom Predicate Factories
To create a custom predicate factory we can extend the AbstractRoutePredicateFactory
, an abstract implementation of the RoutePredicateFactory
interface. In the example below we define an inner static class Configuration
, to pass its properties to the apply method and compare them to the request.
@Component
public class CustomPredicateFactory extends AbstractRoutePredicateFactory<CustomPredicateFactory.Configuration> {
public CustomPredicateFactory() {
super(Configuration.class);
}
@Override
public Predicate<ServerWebExchange> apply(Configurationconfig) {
return exchange -> {
ServerHttpRequest request = exchange.getRequest();
//compare request with config properties
return matches(config, request);
};
}
private boolean matches(Configuration config, ServerHttpRequest request) {
//implement matching logic
return false;
}
public static class Configuration{
//implement custom configuration properties
}
}
Custom Filter Factories
To create a custom filter factory we can extend the AbstractGatewayFilterFactory
, an abstract implementation of the GatewayFilterFactory
interface. In the examples below, you can see a filter factory that modifies the request and another one that changes the response, using the properties passed by a Configuration
object.
@Component
public class PreCustomFilterFactory extends AbstractGatewayFilterFactory<PreCustomFilterFactory.Configuration> {
public PreCustomFilterFactory() {
super(Configuration.class);
}
@Override
public GatewayFilter apply(Configuration config) {
return (exchange, chain) -> {
ServerHttpRequest.Builder builder = exchange.getRequest().mutate();
//use builder to modify the request
return chain.filter(exchange.mutate().request(builder.build()).build());
};
}
public static class Configuration {
//implement the configuration properties
}
}
@Component
public class PostCustomFilterFactory extends AbstractGatewayFilterFactory<PostCustomFilterFactory.Configuration> {
public PostCustomFilterFactory() {
super(Configuration.class);
}
@Override
public GatewayFilter apply(Configuration config) {
return (exchange, chain) -> {
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
//Change the response
}));
};
}
public static class Configuration {
//implement the configuration properties
}
}
Spring Cloud Gateway Example
We will show a practical and simple example to see how the gateway works in a real scenario. You will find a link to the source code at the end of the article. The example is based on the following stack:
- Spring Boot: 3.2.1
- Spring Cloud: 2023.0.0
- Java 17
We consider a minimal microservice system that implements a library with only two services: a book
service and an author
service. The book
service calls the author
service to retrieve an author's information by passing an authorName
parameter. The implementation of the two applications is based on an embedded in-memory H2 database and uses JPA ORM to map and query the Book
and Author
tables. From the standpoint of this demonstration, the most important part is the /getAuthor
REST endpoint exposed by a BookController
class of the Book
service:
@RestController
@RequestMapping("/library")
public class BookController {
Logger logger = LoggerFactory.getLogger(BookService.class);
@Autowired
private BookService bookService;
@GetMapping(value = "/getAuthor", params = { "authorName" })
public Optional<Author> getAuthor(@RequestParam("authorName") String authorName) {
return bookService.getAuthor(authorName);
}
}
The two applications register themselves in an Eureka discovery server and are configured as discovery clients. The final component is the gateway. The gateway should not register itself with the service discovery server. This is because it is called only by external clients, not internal microservices. On the other hand, it can be configured as a discovery client to fetch the other services automatically and implement a more dynamic routing. We don't do it here, though, to keep things simple.
In this example, we want to show two things:
- See how the routing mechanism by the predicate value works
- Show how to modify the request by a filter, adding a header
The gateway's configuration is the following:
spring:
application:
name: gateway-service
cloud:
gateway:
routes:
- id: add_request_header_route
uri: http://localhost:8082
predicates:
- Path=/library/**
filters:
- AddRequestHeader=X-Request-red, red
We have defined a route with id "add_request_header_route
," and a URI value of "http://localhost:8082
," the base URI of the book service. We then have a Path
predicate with a "/library/**
" value. Every call starting with "http://localhost:8080/library/
" will be matched and routed toward the book's service with URIs starting with "http://localhost:8082/library/
."
Running the Example
To run the example you can start each component by executing "mvn spring-boot:run
" command from the component's base directory. You can then test it by executing the "http://localhost:8080/library/getAuthor?authorName=Goethe
" URI. The result will be a JSON value containing info about the author. If you check the browser developer tools you will also find that an X-Request-red
header has been added to the request with a value of "red
."
Conclusion
Implementing a gateway with the Spring Cloud Gateway package is the natural choice in the Spring Boot framework. It reduces the complexity of a microservice environment by placing a single facade component in front of it. It also gives you great flexibility in implementing cross-cutting concerns like authentication, authorization, aggregate logging, tracing, and monitoring.
You can find the source code of the example of this article on GitHub.
Published at DZone with permission of Mario Casari. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments