Lab - Implementing API Gateway with Spring Cloud Gateway
Objective
Integrate Spring Cloud Gateway into our microservices system from the previous module and implement circuit breaker patterns to improve resilience.
Prerequisites
- Completed Module 7 lab with the following microservices:
- Book Service (port 8081)
- User Service (port 8082)
- Analytics Service (port 8083)
- Eureka Server (port 8761)
Step 1: Create the API Gateway Project
- Create a new Spring Boot project:
- Group: com.example
- Artifact: api-gateway
- Dependencies:
- Spring Cloud Gateway
- Eureka Discovery Client
- Spring Boot Actuator
- Spring Cloud Circuit Breaker Resilience4j
- Configure the application.yml file:
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true
routes:
- id: book-service
uri: lb://book-service
predicates:
- Path=/api/books/**
filters:
- name: CircuitBreaker
args:
name: bookService
fallbackUri: forward:/fallback/books
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/users/**
filters:
- name: CircuitBreaker
args:
name: userService
fallbackUri: forward:/fallback/users
- id: analytics-service
uri: lb://analytics-service
predicates:
- Path=/api/analytics/**
filters:
- name: CircuitBreaker
args:
name: analyticsService
fallbackUri: forward:/fallback/analytics
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
instance:
preferIpAddress: true
management:
endpoints:
web:
exposure:
include: gateway,health,info
endpoint:
gateway:
enabled: true
health:
show-details: always
Step 2: Implement Fallback Controller
Create a fallback controller to handle circuit-breaker fallbacks:
package com.example.apigateway.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/fallback")
public class FallbackController {
@GetMapping("/books")
public Mono<Map<String, String>> bookServiceFallback() {
Map<String, String> response = new HashMap<>();
response.put("status", "error");
response.put("message", "Book Service is currently unavailable. Please try again later.");
return Mono.just(response);
}
@GetMapping("/users")
public Mono<Map<String, String>> userServiceFallback() {
Map<String, String> response = new HashMap<>();
response.put("status", "error");
response.put("message", "User Service is currently unavailable. Please try again later.");
return Mono.just(response);
}
@GetMapping("/analytics")
public Mono<Map<String, String>> analyticsServiceFallback() {
Map<String, String> response = new HashMap<>();
response.put("status", "error");
response.put("message", "Analytics Service is currently unavailable. Please try again later.");
return Mono.just(response);
}
}
Step 3: Configure Circuit Breaker
Create a configuration class for Resilience4j circuit breaker:
package com.example.apigateway.config;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.timelimiter.TimeLimiterConfig;
import org.springframework.cloud.circuitbreaker.resilience4j.ReactiveResilience4JCircuitBreakerFactory;
import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JConfigBuilder;
import org.springframework.cloud.client.circuitbreaker.Customizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
@Configuration
public class CircuitBreakerConfiguration {
@Bean
public Customizer<ReactiveResilience4JCircuitBreakerFactory> defaultCustomizer() {
return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
.circuitBreakerConfig(CircuitBreakerConfig.custom()
.slidingWindowSize(10)
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofSeconds(10))
.permittedNumberOfCallsInHalfOpenState(5)
.slowCallRateThreshold(50)
.slowCallDurationThreshold(Duration.ofSeconds(2))
.build())
.timeLimiterConfig(TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofSeconds(3))
.build())
.build());
}
}
Step 4: Add Rate Limiting
Implement a request rate limiter using Redis:
- Add Redis and Redis Rate Limiter dependencies to pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway-mvc</artifactId>
</dependency>
- Configure Redis rate limiter in application.yml:
spring:
redis:
host: localhost
port: 6379
cloud:
gateway:
routes:
- id: book-service
uri: lb://book-service
predicates:
- Path=/api/books/**
filters:
- name: CircuitBreaker
args:
name: bookService
fallbackUri: forward:/fallback/books
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
Step 5: Create Custom Filter for Request Logging
Create a global filter for logging:
package com.example.apigateway.filter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.UUID;
@Component
public class LoggingFilter implements GlobalFilter, Ordered {
private static final Logger logger = LoggerFactory.getLogger(LoggingFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String requestId = UUID.randomUUID().toString();
logger.info("Request ID: {}, Method: {}, Path: {}, Source IP: {}",
requestId,
request.getMethod(),
request.getPath(),
request.getRemoteAddress());
long startTime = System.currentTimeMillis();
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
long endTime = System.currentTimeMillis();
logger.info("Request ID: {}, Response Status: {}, Duration: {} ms",
requestId,
exchange.getResponse().getStatusCode(),
(endTime - startTime));
}));
}
@Override
public int getOrder() {
return -1; // High priority to ensure this runs first
}
}
Step 6: Implement CORS Configuration
Add CORS support to allow frontend applications to access our API:
package com.example.apigateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsWebFilter() {
CorsConfiguration corsConfig = new CorsConfiguration();
corsConfig.setAllowedOrigins(Arrays.asList("*"));
corsConfig.setMaxAge(3600L);
corsConfig.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
corsConfig.setAllowedHeaders(Arrays.asList("*"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfig);
return new CorsWebFilter(source);
}
}
Step 7: Implement Custom Route for API Documentation
Create a custom route to aggregate Swagger documentation from all services:
- Add the OpenAPI dependency:
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-webflux-ui</artifactId>
<version>1.6.15</version>
</dependency>
- Configure in application.yml:
spring:
cloud:
gateway:
routes:
# Add these routes for API documentation
- id: book-service-docs
uri: lb://book-service
predicates:
- Path=/book-service/v3/api-docs/**
filters:
- RewritePath=/book-service/(?<segment>.*), /$\{segment}
- id: user-service-docs
uri: lb://user-service
predicates:
- Path=/user-service/v3/api-docs/**
filters:
- RewritePath=/user-service/(?<segment>.*), /$\{segment}
- id: analytics-service-docs
uri: lb://analytics-service
predicates:
- Path=/analytics-service/v3/api-docs/**
filters:
- RewritePath=/analytics-service/(?<segment>.*), /$\{segment}
- Create a controller for the OpenAPI documentation:
package com.example.apigateway.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
public class ApiDocsController {
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("/api-docs")
public Mono<Map<String, Object>> getApiDocs() {
List<String> services = discoveryClient.getServices();
List<Map<String, String>> serviceUrls = new ArrayList<>();
for (String service : services) {
if (!service.equals("api-gateway")) {
Map<String, String> serviceUrl = new HashMap<>();
serviceUrl.put("name", service);
serviceUrl.put("url", "/" + service + "/v3/api-docs");
serviceUrls.add(serviceUrl);
}
}
Map<String, Object> response = new HashMap<>();
response.put("services", serviceUrls);
return Mono.just(response);
}
}
Step 8: Testing the API Gateway
- Start all the microservices from Module 7:
- Eureka Server
- Book Service
- User Service
- Analytics Service
-
Start the API Gateway application
-
Test the Gateway using curl or Postman:
# Create a user through the gateway
curl -X POST http://localhost:8080/api/users -H "Content-Type: application/json" -d '{
"username": "john_doe",
"email": "[email protected]",
"password": "password123"
}'
# Create a book
curl -X POST http://localhost:8080/api/books -H "Content-Type: application/json" -d '{
"title": "Spring Cloud in Action",
"author": "John Smith",
"isbn": "9781617295423",
"price": 39.99,
"userId": 1
}'
# Get all books
curl http://localhost:8080/api/books
# Get analytics
curl http://localhost:8080/api/analytics/summary
# Check Eureka dashboard
# Open browser at http://localhost:8761
# Check gateway routes
curl http://localhost:8080/actuator/gateway/routes
- Test the Circuit Breaker:
-
Stop the book service
-
Try to access books through the gateway:
curl http://localhost:8080/api/books
-
You should receive the fallback response
Step 9: Enhance API Gateway with Request Transformation
Add a filter to transform requests:
package com.example.apigateway.filter;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
@Component
public class AddRequestHeaderGatewayFilterFactory extends AbstractGatewayFilterFactory<AddRequestHeaderGatewayFilterFactory.Config> {
public AddRequestHeaderGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest().mutate()
.header(config.getHeaderName(), config.getHeaderValue())
.build();
ServerWebExchange mutatedExchange = exchange.mutate().request(request).build();
return chain.filter(mutatedExchange);
};
}
public static class Config {
private String headerName;
private String headerValue;
public String getHeaderName() {
return headerName;
}
public void setHeaderName(String headerName) {
this.headerName = headerName;
}
public String getHeaderValue() {
return headerValue;
}
public void setHeaderValue(String headerValue) {
this.headerValue = headerValue;
}
}
}
Update the routes in application.yml:
spring:
cloud:
gateway:
routes:
- id: book-service
uri: lb://book-service
predicates:
- Path=/api/books/**
filters:
- name: CircuitBreaker
args:
name: bookService
fallbackUri: forward:/fallback/books
- AddRequestHeader=X-Source, api-gateway
Step 10: Implement a Route-specific Metrics Filter
Create a custom filter for collecting metrics:
package com.example.apigateway.filter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.time.Instant;
@Component
public class MetricsFilter implements GlobalFilter, Ordered {
private final MeterRegistry meterRegistry;
public MetricsFilter(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
Instant start = Instant.now();
String path = exchange.getRequest().getPath().value();
String method = exchange.getRequest().getMethod().name();
return chain.filter(exchange)
.doFinally(signalType -> {
Duration duration = Duration.between(start, Instant.now());
String outcome = exchange.getResponse().getStatusCode() != null ?
exchange.getResponse().getStatusCode().toString() : "Unknown";
meterRegistry.timer("gateway.requests",
"path", path,
"method", method,
"outcome", outcome)
.record(duration);
meterRegistry.counter("gateway.requests.count",
"path", path,
"method", method,
"outcome", outcome)
.increment();
});
}
@Override
public int getOrder() {
return -2; // Run before LoggingFilter but after security filters
}
}
Step 11: Integration with the Microservices
- Update the user-service to add a response header indicating the instance it’s running on:
@RestController
@RequestMapping("/api/users")
public class UserController {
@Value("${eureka.instance.instance-id:${random.uuid}}")
private String instanceId;
// Other methods and dependencies...
@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
List<User> users = userService.getAllUsers();
return ResponseEntity.ok()
.header("X-Instance-Id", instanceId)
.body(users);
}
}
- Update the book-service similarly to add instance identification.
Step 12: Deploy and Test the Complete System
- Start all services:
- Eureka Server
- Config Server (if you have one)
- Book Service
- User Service
- Analytics Service
- API Gateway
- Use the API Gateway to access all microservices:
# Access user service through gateway
curl http://localhost:8080/api/users
# Access book service through gateway
curl http://localhost:8080/api/books
# Access analytics service through gateway
curl http://localhost:8080/api/analytics/summary
# Access the API documentation
curl http://localhost:8080/api-docs
- Test scaling by starting multiple instances of a service:
Start another instance of the book service on a different port:
java -jar book-service/target/book-service-0.0.1-SNAPSHOT.jar --server.port=8091
Watch the load balancing in action by making multiple requests:
for i in {1..10}; do curl http://localhost:8080/api/books; done
Observe the X-Instance-Id header to confirm requests are being distributed.
Summary
In this module, we successfully:
- Created a Spring Cloud Gateway as an entry point to our microservices system
- Implemented circuit breakers to handle service failures gracefully
- Added rate limiting to protect our services from overload
- Created custom filters for logging, metrics, and request transformation
- Configured CORS to allow frontend applications to access our API
- Set up API documentation aggregation to provide a unified view of all service APIs
- Tested load balancing by scaling individual services
Key benefits of our Gateway implementation:
- Simplified client access: Clients only need to know about one endpoint
- Enhanced resilience: Circuit breakers prevent cascading failures
- Improved security: Centralized authentication and rate limiting
- Better monitoring: Centralized request logging and metrics
- Simplified API exploration: Aggregated API documentation
In the next module, we’ll focus on containerizing our microservices system using Docker, making it more portable and easier to deploy in various environments.
By Wahid Hamdi