Hello, everybody! In this short article, I want to describe how you can create a filter with the Spring Cloud Gateway framework.
Let’s start. We have a problem: in our company we use microservice architecture. So, we have service discovery and a cloud gateway that are based on Spring technologies and more than one hundred microservices. Each developer has his own area of responsibility — each using a few microservices.
When I want to test my new version of microservice in a developer environment with connections to other services via a gateway, I need to forward all requests from my machine to my service. Let me explain: there are two microservices, named “UI”, one on my computer and the other in our network. Also, I have another microservice on my machine called “AUTH,” which uses Gateway to send requests to “UI.” But I want to communicate only with my instance, I don’t need a round-robin calling of “UI” services. So this is our task: create a custom filter that can solve this problem.
First of all, let’s assume we have one simple route:
We only rewrite the URI path, but our purpose is to change URI from “lb://UI/” to out machine.
To create a custom filter, we need to make a bean and implement two inter-faces — GatewayFilter or GlobalFilter and Ordered. The difference between GatewayFilter and GlobalFilter is as such: GlobalFilter will apply for each route in the gateway, without modifying each route. GatewayFilter must be included to filter expression.
So, our class will look like this:
getOrder() function response the order filter that will apply. So, we need to return high value because we want to proceed with requests in the end. We will use number 20000.
public static final int ROUTE_TO_LOCAL_FILTER_ORDER = 20_000;
Now, we have to add logic to our filter method. First of all, we need to create a user IP address and service name. All information stored in ServerWeb-Exchange variable:
String userIp = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
When we use routes with load balancing, our URL looks like “lb://UI/”, where the host part corresponds to the service name. We can extract it:
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
String serviceName = route.getUri().getHost().toLowerCase()
Now, we have our IP address and service name. So, we need to find a service registry with such a name and address. Let’s autowire from spring context ServiceDiscovery bean and search our application there.
Now, if the discovery client contents microservice with such params, it will be stored in the myService variable. So, if it presents, we insert it to the exchange:
URI destination = new URI(..our IP + relative path..);
We embed the modified context to the response chain:
Eureka app context and Gateway context doesn’t refresh very often, but we want to request our service immediately after starting during the day. So, for developer convenience, we need some cache to store success endpoints. It will be a simple map:
private final Map<String, URI> cache = new ConcurrentHashMap<>();
The best idea for a map key is a combination of IP address and service name.
String cacheKey = serviceName + “:” + userIp;
Each instance from discovery service will be stored in a cache map:
If discovery is empty, take an instance address from the map:
URI localUri = cache.get(cacheKey);
But, it can be unreachable, so we need to check it before substituting that URI to exchange. The solution is a simple method isAlive(), that connects to actuator URL:
Even if the service discovery has not reloaded the context, we can still communicate with our microservice. In the GlobalFilter case, it will be useful to add the @ConditionalOnProperty annotation to enable/disable the filter from application properties. If you use the GatewayFilter implementation, just add a new filter to the route:
.filters(f -> f.rewritePath(“/api/”, “/service-instances/”).filter(routeToLocalServicesFilter))
Thank you for attention. Full source code you can find here: https://github.com/r331/firebackfilter