Internal Mechanics of Spring Cloud Gateway: Auto-Loading Predicates and Filters
Auto-Configuration Entry Point
Spring Cloud Gateway acts as a routing proxy that directs incoming requests to downstream services based on defined rules. When working with Gateway, configurations typically involve defining routes with specific predicates and filters.
A standard Maven dependency setup looks like this:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
Alongside the dependency, a basic route configuration is required. For instance, applying a time-based restriction and adding a header:
spring:
cloud:
gateway:
routes:
- id: time-based-route
uri: lb://inventory-service
predicates:
- Before=2040-01-01T00:00:00.000+00:00[UTC]
filters:
- AddRequestHeader=X-Request-Source, gateway
Gateway automatically recognizes directives like Before and AddRequestHeader. To understand how these are resolved and loaded, we must examine the Spring Boot auto-configuration mechanism. The starting point is the spring.factories file, which points to the org.springframework.cloud.gateway.config.GatewayAutoConfiguration class.
Route Locator Bean Definitions
Within GatewayAutoConfiguration, two crucial beans are responsible for assembling the routing infrastructure:
@Bean
public RouteLocator primaryRouteLocator(GatewayProperties gatewayProps,
List<GatewayFilterFactory> filterFactories,
List<RoutePredicateFactory> predicateFactories,
RouteDefinitionLocator definitionLocator) {
return new RouteDefinitionRouteLocator(definitionLocator, predicateFactories, filterFactories, gatewayProps);
}
@Bean
@Primary
public RouteLocator cachingRouteLocator(List<RouteLocator> routeLocators) {
return new CachingRouteLocator(new CompositeRouteLocator(Flux.fromIterable(routeLocators)));
}
Notice that the primaryRouteLocator method accepts List<RoutePredicateFactory> and List<GatewayFilterFactory> as parameters. Spring automatically resolves these lists by gathering all implementing beans from the application context.
Dependency Injection Resolution Mechanism
When Spring instantiates the RouteLocator bean, it relies on its internal autowiring mechanism to populate the factory lists. During bean creation, Spring analyzes the factory method parameters. For each parameter, it invokes resolution logic:
private ArgumentsHolder constructArgumentArray(
String beanIdentifier, RootBeanDefinition beanDef, @Nullable ConstructorArgumentValues resolvedVals,
BeanWrapper wrapper, Class<?>[] paramTypes, @Nullable String[] paramNames, Executable executable,
boolean autowiring, boolean fallback) throws UnsatisfiedDependencyException {
// Iterating through each required parameter type
for (int paramIndex = 0; paramIndex < paramTypes.length; paramIndex++) {
try {
// Resolving the specific autowired argument (e.g., List<RoutePredicateFactory>)
Object autowiredArgument = resolveAutowiredArgument(
methodParam, beanIdentifier, autowiredBeanNames, converter, fallback);
args.rawArguments[paramIndex] = autowiredArgument;
args.arguments[paramIndex] = autowiredArgument;
args.preparedArguments[paramIndex] = new AutowiredArgumentMarker();
args.resolveNecessary = true;
}
catch (BeansException ex) {
throw new UnsatisfiedDependencyException(
beanDef.getResourceDescription(), beanIdentifier, new InjectionPoint(methodParam), ex);
}
}
// ...
return args;
}
To find the actual implementations of the factory interfaces, Spring searches the bean factory. The findAutowireCandidates method is instrumental here:
protected Map<String, Object> locateAutowireCandidates(
@Nullable String beanIdentifier, Class<?> requiredType, DependencyDescriptor desc) {
// Fetches all bean names implementing the required type from the factory
String[] matchingNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
this, requiredType, true, desc.isEager());
Map<String, Object> matchedResults = new LinkedHashMap<>(matchingNames.length);
// Instantiate and collect valid candidates
for (String currentName : matchingNames) {
if (!isSelfReference(beanIdentifier, currentName) && isAutowireCandidate(currentName, desc)) {
addCandidateEntry(matchedResults, currentName, desc, requiredType);
}
}
return matchedResults;
}
This process ensures that all built-in and custom implementations of RoutePredicateFactory and GatewayFilterFactory are collected and injected into the RouteLocator.
Factory Initialization and Naming Convention
Once the lists of factories are injected into the RouteDefinitionRouteLocator constructor, they are organized into maps for efficient lookup:
public RouteDefinitionRouteLocator(RouteDefinitionLocator definitionLocator,
List<RoutePredicateFactory> predicateFactories,
List<GatewayFilterFactory> filterFactories,
GatewayProperties properties) {
this.definitionLocator = definitionLocator;
initFactories(predicateFactories);
filterFactories.forEach(factory -> this.filterFactoryMap.put(factory.name(), factory));
this.properties = properties;
}
The initFactories method iterates over the predicate factories, storing them in a map. The same logic applies to filter factories via the forEach loop. The key used for the map is derived from factory.name(). This default method is defined in the factory interfaces:
default String name() {
return NameUtils.normalizeRouteFactoryName(getClass());
}
The normalizeRouteFactoryName utility strips the RoutePredicateFactory or GatewayFilterFactory sufffix from the class name. For example, a class named BeforeRoutePredicateFactory yields the key Before. This key must exactly match the configuration string (e.g., Before=...).
Consequently, to create a custom predicate or filter, one simply needs to implement the corresponding factory interface, annotate the class with @Component, and ensure the class name ends with the appropriate suffix sothat the remaining string matches the desired YAML configuration key.