Custom Filters in Spring Cloud Gateway 2.x: GatewayFilter, GlobalFilter, and Filter Factories
- Spring Cloud release train: Greenwich.SR3 (Spring Cloud Gateway 2.1.x)
- Spring Boot 2.1.x
- Sample setup assumes Eureka Server, Eureka Ribbon client, Eureka Feign client, and Spring Cloud Gateway
GatewayFilter: per-route filtering
Implement a route-scoped filter by implementing GatewayFilter and Ordered. Ordered determines precedence (smaller values run earlier).
package com.example.gateway.filters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
public class TimingGatewayFilter implements GatewayFilter, Ordered {
private static final Logger log = LoggerFactory.getLogger(TimingGatewayFilter.class);
private static final String START_NANOS = "timingStartNanos";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
exchange.getAttributes().put(START_NANOS, System.nanoTime());
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
Long started = exchange.getAttribute(START_NANOS);
if (started != null) {
long elapsedMs = (System.nanoTime() - started) / 1_000_000;
log.info("request={} took={}ms", exchange.getRequest().getURI(), elapsedMs);
}
}));
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
Attach the filter to a route definition using RouteLocatorBuilder:
package com.example.gateway.config;
import com.example.gateway.filters.TimingGatewayFilter;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RoutesConfiguration {
@Bean
public RouteLocator customRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route("ribbon-route", r -> r
.order(0)
.path("/ribbon/**")
.filters(f -> f
.stripPrefix(1)
.filter(new TimingGatewayFilter())
.addResponseHeader("X-Demo-Response", "gw-2x"))
.uri("lb://EUREKA-RIBBON"))
.build();
}
}
Pre vs. Post logic in a filter
- Code before invoking chain.filter(exchange) is the pre phase.
- Logic chained via then(...) runs after the downstream handler completes and is the post phase.
GlobalFilter: cross-route filtering
A GlobalFilter applies to every route (subject to its order). Example: reject requests missing a token query parameter.
package com.example.gateway.filters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class TokenGuardFilter implements GlobalFilter, Ordered {
private static final Logger log = LoggerFactory.getLogger(TokenGuardFilter.class);
private static final String TOKEN_PARAM = "token";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getQueryParams().getFirst(TOKEN_PARAM);
if (token == null || token.trim().isEmpty()) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 1; // runs early, before LOWEST_PRECEDENCE filters
}
}
Optional bean registration alternative if component scanning isn’t used:
package com.example.gateway.config;
import com.example.gateway.filters.TokenGuardFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FiltersRegistration {
@Bean
public TokenGuardFilter tokenGuardFilter() {
return new TokenGuardFilter();
}
}
Run and verify
- Start Eureka Server, Ribbon client, Feign client, and Gateway.
- Access http://localhost:8100/ribbon/sayHello without a token → HTTP 401.
- Access http://localhost:8100/ribbon/sayHello?token=abc → proxied response; logs will include timing from TimingGatewayFilter.
GatewayFilterFactory: reusable, configurable filters
Create a custom factory for reusable filters with configuration bound from YAML shortcuts.
package com.example.gateway.filters.factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import reactor.core.publisher.Mono;
import java.util.Collections;
import java.util.List;
public class RequestMetricsGatewayFilterFactory extends AbstractGatewayFilterFactory<RequestMetricsGatewayFilterFactory.Config> {
private static final Logger log = LoggerFactory.getLogger(RequestMetricsGatewayFilterFactory.class);
private static final String ATTR_REQ_START = "reqMetricsStart";
public RequestMetricsGatewayFilterFactory() {
super(Config.class);
}
@Override
public List<String> shortcutFieldOrder() {
return Collections.singletonList("logParams");
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
exchange.getAttributes().put(ATTR_REQ_START, System.currentTimeMillis());
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
Long t0 = exchange.getAttribute(ATTR_REQ_START);
if (t0 == null) {
return;
}
long took = System.currentTimeMillis() - t0;
StringBuilder sb = new StringBuilder()
.append("uri=").append(exchange.getRequest().getURI())
.append(", time=").append(took).append("ms");
if (config.isLogParams()) {
sb.append(", params=").append(exchange.getRequest().getQueryParams());
}
log.info(sb.toString());
}));
};
}
public static class Config {
private boolean logParams;
public boolean isLogParams() { return logParams; }
public void setLogParams(boolean logParams) { this.logParams = logParams; }
}
}
Register the factory as a Spring bean:
package com.example.gateway.config;
import com.example.gateway.filters.factory.RequestMetricsGatewayFilterFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FilterFactoryConfig {
@Bean
public RequestMetricsGatewayFilterFactory requestMetricsGatewayFilterFactory() {
return new RequestMetricsGatewayFilterFactory();
}
}
Enable and use the factory in configuration
application.yml example with discovery locator and default/route filters:
server:
port: 8100
spring:
application:
name: spring-gateway
cloud:
gateway:
discovery:
locator:
enabled: true
default-filters:
- RequestMetrics=true # enable custom factory; logs params when true
routes:
- id: ribbon-route
uri: lb://EUREKA-RIBBON
order: 0
predicates:
- Path=/ribbon/**
filters:
- StripPrefix=1
- AddResponseHeader=X-Demo-Response, gw-2x
- id: feign-route
uri: lb://EUREKA-FEIGN
order: 0
predicates:
- Path=/feign/**
filters:
- StripPrefix=1
- AddResponseHeader=X-Demo-Response, gw-2x
eureka:
instance:
hostname: eureka1.server.com
lease-renewal-interval-in-seconds: 5
lease-expiration-duration-in-seconds: 10
client:
service-url:
defaultZone: http://eureka1.server.com:8701/eureka/,http://eureka2.server.com:8702/eureka/,http://eureka3.server.com:8703/eureka/
Visit http://localhost:8100/ribbon/sayHello?token=xyz and observe a log entry similar to:
- RequestMetricsGatewayFilterFactory: uri=http://localhost:8100/ribbon/sayHello?token=xyz, time=NNms, params:{token=[xyz]}
Notes
- GlobalFilter and GatewayFilter share the same method signature; GlobalFilter instances can affect all routes.
- When extending AbstractGatewayFilterFactory, pass the custom Config class too the super constructor so the frmaework can bind properties from YAML.
- AbstractGatewayFilterFactory binds properties based on shortcutFieldOrder; a single boolean maps clean to the YAML form: RequestMetrics=true.