Search code examples
javaspring-bootspring-cloud-gateway

How do I set a predicate for a Route.Builder in Spring Cloud Gateway?


I won't go into much detail (it's not hugely relevant, and it's a lot of code), but here's an interface I wrote:

@FunctionalInterface
public interface RouteProcessor {
    // DocumentedEndpoint is my custom class that encapsulates an exposed endpoint of a service
    Route.Builder process(Route.Builder routeInConstruction, DocumentedEndpoint endpoint);
}

It's used like this (it's a basic idea, not final code):

    private void buildAndAddRoutesFrom(List<? extends DocumentedEndpoint> endpoints) {
        for (DocumentedEndpoint endpoint : endpoints) {
            Route.Builder routeBuilder = Route.builder();
            for (RouteProcessor routeProcessor : routeProcessors) {
                routeBuilder = routeProcessor.process(routeBuilder, endpoint);
            }
            routeFlux = routeFlux.concatWith(Mono.just(routeBuilder.build())); // questionable, but it's beyond the point for now
        }
    }

Again, it's not really important. What's important is that Spring Cloud Gateway's sources have, to put it mildly, room for improvement (in my view). In particular, I don't know how I do such a simple thing as adding a predicate to a Route in construction. Like, suppose I construct it. Where do I pass it now?

    @Bean
    public RouteProcessor authenticationRouteProcessor() {
        return (routeInConstruction, endpoint) -> {
                AsyncPredicate<ServerWebExchange> newPredicate = AsyncPredicate.from(serverWebExchange -> serverWebExchange
                        .getRequest()
                        .getPath()
                        .toString()
                        .startsWith("/api/v1"));

                AsyncPredicate<ServerWebExchange> compoundPredicate = routeInConstruction.getPredicate().and(predicate);

                // what's next?

How do I set a predicate for a Route.Builder in Spring Cloud Gateway?

Like, if you're saying that it's the only way I can pass it, by invoking this constructor (which is private, btw):

// from Route.AbstractBuilder

        public Route build() {
            Assert.notNull(this.id, "id can not be null");
            Assert.notNull(this.uri, "uri can not be null");
            AsyncPredicate<ServerWebExchange> predicate = getPredicate();
            Assert.notNull(predicate, "predicate can not be null");

            // Route has predicate, but Route.Builder doesn't!
            return new Route(this.id, this.uri, this.order, predicate, this.gatewayFilters, this.metadata);
        }

Gateway's design surprises me

I mean we have this nice and() method that we can use as a setter

        public Builder and(Predicate<ServerWebExchange> predicate) {
            Assert.notNull(this.predicate, "can not call and() on null predicate");
            this.predicate = this.predicate.and(predicate);
            return this;
        }

but that setting is basically the method's side effect so it feels more like a workaround rather than something that I should actually do as the library's user

But more importantly, how exactly do I add a predicate to an existing one? It should already be initialized due to the null assert. So this won't work:

        @Bean
    public RouteProcessor basicPredicateProcessor() {
        return (routeInConstruction, endpoint) -> {
            routeInConstruction.and(serverWebExchange -> serverWebExchange
                    .getRequest()
                    .getPath()
                    .toString()
                    .startsWith("/api/v1"));
            return routeInConstruction;
        };
    }

Please let me know that it's not that far gone and I missed some crucial snippets of the source code


Solution

  • Like, before they start to write good code, here's your best bet:

    @FunctionalInterface
    public interface RouteProcessor {
        Route.AsyncBuilder process(Route.AsyncBuilder routeInConstruction, DocumentedEndpoint endpoint);
    }
    
        private void buildAndAddRoutesFrom(List<? extends DocumentedEndpoint> endpoints) {
            for (DocumentedEndpoint endpoint : endpoints) {
                Route.AsyncBuilder routeBuilder = Route.async();
                for (RouteProcessor routeProcessor : routeProcessors) {
                    routeBuilder = routeProcessor.process(routeBuilder, endpoint);
                }
                routeFlux = routeFlux.concatWith(Mono.just(routeBuilder.build()));
            }
        }
    
        @Bean
        // probably you'd want to set it first thing
        // @Order(value = Ordered.HIGHEST_PRECEDENCE)
        public RouteProcessor basicPredicateProcessor() {
            return (routeInConstruction, endpoint) -> {
                routeInConstruction.asyncPredicate(AsyncPredicate.from(serverWebExchange -> serverWebExchange
                                .getRequest()
                                .getPath()
                                .toString()
                                .startsWith("/api/v1")
                        ));
                return routeInConstruction;
            };
        }
    

    It's because Route.AsyncBuilder has a method that doesn't include that null check so you can safely initialize predicate:

            public AsyncBuilder asyncPredicate(AsyncPredicate<ServerWebExchange> predicate) {
                this.predicate = predicate;
                return this;
            }