Custom Gateway Filter Implementation
When you need to implement custom logic within the gateway's request processing pipeline, the GatewayFilter interface provides the necessary hooks. Below is a timing filter that meausres request duration:
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.core.Ordered;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
public class RequestTimerFilter implements GatewayFilter, Ordered {
private static final String TIMER_START_ATTR = "requestStartTime";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
exchange.getAttributes().put(TIMER_START_ATTR, System.currentTimeMillis());
return chain.filter(exchange).then(
Mono.fromRunnable(() -> {
Long startMillis = exchange.getAttribute(TIMER_START_ATTR);
if (startMillis != null) {
long duration = System.currentTimeMillis() - startMillis;
System.out.println(
exchange.getRequest().getURI().getRawPath() +
": " +
duration + "ms"
);
}
})
);
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
The filter captures the current timestamp before chain.filter(exchange) executes and calculates elapsed time in the then() block. Code executed before chain.filter(exchange) represents the "pre" phase, while code within then() represents the "post" phase.
To register this filter with a route:
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/api/users/**")
.filters(f -> f.stripPrefix(1)
.filter(new RequestTimerFilter())
.addResponseHeader("X-Response-Default-Foo", "Default-Bar"))
.uri("lb://USER-SERVICE")
.order(0)
.id("user_service_route")
)
.build();
}
Accessing http://localhost:20000/api/users/hello/cloud produces output like:
2020-08-23 17:17:43.584 INFO 9856 — [ctor-http-nio-1] o.s.cloud.gateway.filter.GatewayFilter : /hello/cloud: 1ms
Global Filters
Route-specific filters require configuration for each route definition. For cross-cutting concerns like authentication that should apply to all routes, global filters offer a cleaner solution.
Implementing a global filter is nearly identical to a custom filter—just implement GlobalFilter instead of GatewayFilter:
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
public class AuthenticationFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String authToken = exchange.getRequest().getHeaders().getFirst("Authorization");
if (authToken == null || authToken.isEmpty()) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -100;
}
}
Register the filter as a Spring bean:
@Bean
public AuthenticationFilter authenticationFilter() {
return new AuthenticationFilter();
}
The filter logs when processing requests:
2020-08-23 17:54:10.574 DEBUG 20452 — [ctor-http-nio-4] o.s.c.g.h.RoutePredicateHandlerMapping : Route matched: service_customer
2020-08-23 17:54:10.579 DEBUG 20452 — [ctor-http-nio-4] o.s.c.g.h.RoutePredicateHandlerMapping : Mapping [Exchange: GET http://localhost:20000/customer/hello/cloud] to Route{id='service_customer', uri=lb://EUREKA-CONSUMER, order=0, predicate=Paths: [/customer/**], match trailing slash: true, gatewayFilters=[[[StripPrefix parts = 1], order = 1], [[AddResponseHeader X-Response-Default-Foo = 'Default-Bar'], order = 2]], metadata={}}
2020-08-23 17:54:10.579 DEBUG 20452 — [ctor-http-nio-4] o.s.c.g.h.RoutePredicateHandlerMapping : [b6c683f3-7] Mapped to org.springframework.cloud.gateway.handler.FilteringWebHandler@28fcb031
2020-08-23 17:54:10.579 DEBUG 20452 — [ctor-http-nio-4] o.s.c.g.handler.FilteringWebHandler : Sorted gatewayFilterFactories: [[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RemoveCachedBodyFilter@15ccd9e2}, order = -2147483648], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@3dc6b2a1}, order = -2147482648], [GatewayFilterAdapter{delegate=com.example.filter.AuthenticationFilter@633d9a32}, order = -100], ...]
Custom Filter Factories
Spring Cloud Gateway provides built-in filter factories like StripPrefix and AddResponseHeader that can be configured declaratively:
filters:
- StripPrefix=1
- AddResponseHeader=X-Response-Default-Foo, Default-Bar
Creating a custom filter factory allows parameterized configuration. The following example extends AbstractGatewayFilterFactory to create a configurable timing filter:
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.List;
public class TimingGatewayFilterFactory
extends AbstractGatewayFilterFactory<TimingGatewayFilterFactory.Config> {
private static final Log log = LogFactory.getLog(GatewayFilter.class);
private static final String REQUEST_START_ATTR = "requestStartTime";
private static final String INCLUDE_QUERY_PARAM = "includeQuery";
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList(INCLUDE_QUERY_PARAM);
}
public TimingGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
exchange.getAttributes().put(REQUEST_START_ATTR, System.currentTimeMillis());
return chain.filter(exchange).then(
Mono.fromRunnable(() -> {
Long startTime = exchange.getAttribute(REQUEST_START_ATTR);
if (startTime != null) {
StringBuilder output = new StringBuilder()
.append(exchange.getRequest().getURI().getRawPath())
.append(": ")
.append(System.currentTimeMillis() - startTime)
.append("ms");
if (config.isIncludeQuery()) {
output.append(" params:").append(exchange.getRequest().getQueryParams());
}
log.info(output.toString());
}
})
);
};
}
public static class Config {
private boolean includeQuery;
public boolean isIncludeQuery() {
return includeQuery;
}
public void setIncludeQuery(boolean includeQuery) {
this.includeQuery = includeQuery;
}
}
}
Two abstract base classes simplify filter factory development:
| Base Class | Parameter Count | Example |
|---|---|---|
AbstractGatewayFilterFactory |
Single parameter | StripPrefix, RequestSize |
AbstractNameValueGatewayFilterFactory |
Two parameters | AddRequestHeader, AddResponseHeader |
The Config inner class receives the filter's configuration parameters. The shortcutFieldOrder() method maps configuration values to class fields. The parent constructor must be called with the Config class to avoid ClassCastException.
Register the factory as a Spring bean:
@Bean
public TimingGatewayFilterFactory timingGatewayFilterFactory() {
return new TimingGatewayFilterFactory();
}
Configuration in YAML:
spring:
cloud:
gateway:
discovery:
locator:
enabled: true
default-filters:
- Timing=true
routes:
- id: customer_service
uri: lb://CONSUMER
order: 0
predicates:
- Path=/customer/**
filters:
- StripPrefix=1
- AddResponseHeader=X-Response-Default-Foo, Default-Bar
Requesting http://localhost:20000/customer/hello/user?token=1000 produces:
2018-05-08 16:53:02.030 INFO 84423 — [ctor-http-nio-1] o.s.cloud.gateway.filter.GatewayFilter : /hello/user: 656ms params:{token=[1000]}