Spring Cloud Starter Netflix Zuul was deprecated a long time ago and its last version was published in November 2021 and it was officially compatible with Spring 2.3.12.RELEASE but for a miracle, it was still working with the version 2.7.x which was the latest version of Spring Boot 2.x.

But postponing and hoping it continues working with newer versions of Spring is not a good strategy and also we would be stuck with a library with no security fixes and new functionalities, so we had to look for a replacement

The best contender for replacing Zuul is Spring Cloud Gateway. The first Spring Cloud Gateway project (and the main one) is reactive and while it may be the best option for an api-gateway, it is a very big change to do both the framework change and also from MVC to reactive and I personally don’t have experience with reactive apps and thought it to be a bit risky to try for an important service.

Luckily for us in November 2023, Spring Cloud Gateway got a new Server MVC version released on the Spring Cloud 2023.0.0 release train. This new MVC was a great candidate for us, in case we want to continue it being an MVC so we decided to migrate our Spring Cloud Starter Netflix Zuul to Spring Cloud Gateway Server MVC

Spring Cloud Gateway Server MVC

Spring Cloud Gateway Server MVC is different from Zuul and also from the majority of other Spring Projects as it uses WebMvc.fn (functional endpoints) and it supports almost everything that we had with Zuul but also provides more filters compared to only Filter Path in Zuul.

The 3 main building blocks are Routes, Predicates, and Filters (source).

  • Route: In simple terms, to which downstream service the request will be sent.
  • Predicate: What you use to filter requests, You can match anything from the HTTP request, such as headers or parameters.
  • Filter: Used to modify requests and responses before or after sending the downstream request. Also, used to add logic to handle and block requests. Here is where we say what can and cannot go through.

If you are familiar with Zuul, the concepts are kind of similar, but here we have a lot more flexibility in the new SCG Server MVC.

In our case, when we receive a request, the first step is that it goes to Authentication and Authorization. After it determines the routes based on the Predicates (in our case, the Path of the request) and uses the Filters to modify the request as needed and forward it to the downstream service.

From Spring Cloud Starter Netflix Zuul to Spring Cloud Gateway Server MVC

It is easier to understand with an example, so let’s talk about how we set up before when we were using Netflix Zuul and how we are setting up now with SCG Server MVC.

Filters

In Zuul, the basic structure of a filter is shown in the example filter ExampleFilter below.

@Slf4j  
@Component  
public class ExampleFilter extends ZuulFilter {  
  
    @Override  
    public int filterOrder() {  
        return -1000;  
    }  
  
    @Override  
    public Object run() {  
     // logic here
  
        return null;  
    }  
  
    @Override  
    public boolean shouldFilter() {  
        return true;  
    }  
  
    @Override  
    public String filterType() {  
        return PRE_TYPE;  
    }  
}

The filterOrder determines the order in which this filter will run relative to all other filters. In SCG the order is defined by the order we put the filters in the route configuration. I will show a bit more about the order when we talk about Routes.

The shouldFilter determines if this filter should run, in the example above it is true so it will always run, but for some other filters, we check ENV variables, request info, etc. This is not implemented in the SCG but we can still use the same function and just return the request. I will show a better example soon.

The most common filterType are PRE_TYPE and POST_TYPE. Basically, it defines if the filter should run BEFORE sending the request downstream of AFTER (before returning the response). In SCG, we have different filter types as well.

By default, SCG has many usefu ready to usel filters, but we can also write our own custom ones which is what you are probably going to need.

We have the Before Filter which is the same as PRE_TYPE where we manipulate a request before sending it downstream. For example, the ExampleFilter is a PRE_TYPE, and this is how it became after the change to SCG.

public static Function<ServerRequest, ServerRequest> exampleFilter() {
    return request -> {
       // your logic here
        return request;
    };
}

One example of an After Filter (old POST_TYPE) is shown below in the SCG

public BiFunction<ServerRequest, ServerResponse, ServerResponse> afterFilter() {
    return (request, response) -> {
        // your logic here

        return response;
    };
}

In this case, as it is an after-filter, we have access to the request and also the response so we can change the response if we want.

SCG also has a filter that can do both before and after so you can manipulate the request and the response in the same place. We have a new RedictFilter that applies this concept as shown below.

public HandlerFilterFunction<ServerResponse, ServerResponse> redirectIfNoAuthorization() {
    return (request, next) -> {
        // logic before

       ServerResponse response = next.handle(request);

        //logic after
        return response;
    };
}

And that is basically the most important point from the filter perspective, but how do we make the filter be evaluated? That is where we need to define Routes.

Routes

Routes define where you send the request downstream. To which service, to which path, etc.

Routes in Netflix Zuul

In Zuul, one of the ways to define the route configurations was on the application properties file. Like the example below for my-service.

zuul:
  routes:
    my-service:
      path: /my-service/**
      serviceId: my-service

This basically uses a “path” filter and filters all the calls that start with /my-service and route to my-service. But then you ask (at least I asked myself), how does it know which port it forwards to? For that, Zull has a ribbon configuration similar to the one below.

my-service:
  ribbon:
    listOfServers: http://my-service:9600

How do we make this work in SCG Server MVC? That is a great question, I am glad you asked.

Setting Routes in Spring Cloud Gateway Server MVC

While in SCG we can also set routes in the application properties file, I decided to go with the Java configuration as it is more compile-friendly and it can break if there are some configuration issues. It also allows us to pass non-static context to the filters (maybe it is possible in properties, I just didn’t try).

Let’s see how we can configure my-service to continue working as before. For that, we created a ServiceRouter configuration component with beans for each route. The following bean is for my service.

 @Bean
    public RouterFunction<ServerResponse> myServiceRouter() {
        return route()
                .route(path("/my-service/**"), http())
                .filter(lb("my-service"))
                // other filters
                .build();
    }

Here we are creating a route to filter for the path("/my-service/**"). Everything that starts with my-service will be using the load balancer with id my-service which is configured in line .filter(lb(“my-service”)).

But here again, you ask, how does it know the IP? In theory, it has some fancy ways to do discovery, but as we were not doing it before, I didn’t try here so I went with the simplest approach, I set up a list like we had before in application properties. The following is the configuration of the load balancer for the service.

spring:
  cloud:
    discovery:
      client:
        simple:
          instances:
            my-service:
              - uri: http://my-service:9600

Notice that it does not use MVC, but Spring Cloud Discovery. But it works, I swear (I tested it on my machine 😬 )

Adding Filters to Routes

I told you guys that I was going to talk more about the order of the filters. Now it is the time. In the myServiceRouter above we omitted the filters on purpose, now it is the time to see more of them.

You can see that after .filter(lb(“my-service”)) we added a bunch of before(), filter(), and after().

@Bean
public RouterFunction<ServerResponse> myServiceRouter() {
    return route()
            .route(path("/my-service/**"), http())
            .filter(lb("my-service"))
            .before(before1())
            .before(before2("my-service"))
            .before(before3())
            .filter(filter1())
            .before(before4())
            .before(before5())
            .after(after1())
            .after(after2())
            .build();
}

All of these are our lovely custom filters. The keyword before, filter, and after defines when they execute, based on their type, and the order in which they are defined is the order that they will execute. Easy peasy, piece of cake.

We already notice that almost everything is really different. I feel that I rewrote the whole api gateway service. But after finishing, it is was not that complicated. We just had to put the things in new places.

What we are missing

It is not just flowers, Spring Cloud Gateway Server MVC also has its flaws. The only one that I was not able to find a workaround for is the specific service read-timeout configuration.

In Zuul with Ribbon, we were able to define a general read timeout

ribbon:
  ReadTimeout: 15000

And this we still can do with SCG using the config below

spring:
  cloud:
    gateway:
      mvc:
        http-client:
          read-timeout: 15s

But one nice thing that Ribbon had and that LoadBalancer does not have (as implemented yet on Server MVC) is the per-route timeout.

In Ribbon, we had this in some services

another-service:
  ribbon:
    listOfServers: http://another-service:9100
    ReadTimeout: 30000

I even opened an issue in the repo https://github.com/spring-cloud/spring-cloud-gateway/issues/3378 and the god of SCG (the guy who coded it) said we don’t have it yet but hopefully we will get it in the future.

Performance

I didn’t fully done a performance test to compare both versions, but what I can say is that the metrics of memory and CPU usage were basically the same in production as the previous versions. We still didn’t enable the virtual threads yet so I guess once we do it the performance will be even better.


This was a bit on how to migrate from Spring Cloud Starter Netflix Zuul to Spring Cloud Gateway Server MVC. I hope this article helps your migration to be less painfull and with less incidents.