Spring Cloud Gateway 自定義Filter
Spring Cloud Gateway 的Filter分為GatewayFilter和GlobalFilter兩種,二者區別如下
- GatewayFilter : 需要通過spring.cloud.routes.filters 配置在具體路由下,只作用在當前路由上或通過spring.cloud.default-filters配置在全局,作用在所有路由上
- GlobalFilter : 全局過濾器,不需要在配置文件中配置,作用在所有的路由上,最終通過GatewayFilterAdapter包裝成GatewayFilterChain可識別的過濾器,它為請求業務以及路由的URI轉換為真實業務服務的請求地址的核心過濾器,不需要配置,系統初始化時加載,並作用在每個路由上。
Gateway Filter
Gateway filter 是從 Web Filter中復制來的,相當於一個Filter過濾器,可以對訪問的URL進行過濾,進行橫切面處理,應用場景包括超時、安全等。
Global Filter
Spring Cloud Gateway 定義了Global filter的接口,讓我們可以自定義實現自己的Glabl Filter,Glabl Filter是一個全局的Filter,作用於所有的路由。
自定義GatewayFilter
創建一個自定的filter,功能:打印請求的耗時
/**
* 自定義GatewayFilter
* 打印一條請求下的耗時
*
* @author chengluchao
*/
public class RequestTimeFilter implements GatewayFilter, Ordered {
private static final Log log = LogFactory.getLog(GatewayFilter.class);
private static final String REQUEST_TIME_BEGIN = "requestTimeBegin";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
exchange.getAttributes().put(REQUEST_TIME_BEGIN, System.currentTimeMillis());
return chain.filter(exchange).then(
Mono.fromRunnable(() -> {
Long startTime = exchange.getAttribute(REQUEST_TIME_BEGIN);
if (startTime != null) {
log.info(exchange.getRequest().getURI().getRawPath() + ": " + (System.currentTimeMillis() - startTime) + "ms");
}
})
);
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
代碼解析:
- Ordered中的int getOrder()方法是來給過濾器設定優先級別的,值越大則優先級越低。
- 還有有一個filterI(exchange,chain)方法,在該方法中,先記錄了請求的開始時間,並保存在ServerWebExchange中,此處是一個“pre”類型的過濾器,然后再chain.filter的內部類中的run()方法中相當於"post"過濾器,在此處打印了請求所消耗的時間。
然后將該過濾器注冊到router中,代碼如下:
@Bean
public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/demo/**")
.filters(f -> f.filter(new RequestTimeFilter()))
.uri("http://httpbin.org:80/get")
.order(0)
.id("customer_filter_router")
)
.build();
}
執行程序,會打印出方法的耗時
自定義過濾器工廠
過濾器工廠的頂級接口是GatewayFilterFactory,我們可以直接繼承它的兩個抽象類來簡化開發AbstractGatewayFilterFactory和AbstractNameValueGatewayFilterFactory,這兩個抽象類的區別就是前者接收一個參數(像StripPrefix和我們創建的這種),后者接收兩個參數(像AddResponseHeader)。
做一個可配置是否開啟的打印請求耗時的過濾器
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 RequestTimeGatewayFilterFactory extends AbstractGatewayFilterFactory<RequestTimeGatewayFilterFactory.Config> {
private static final Log log = LogFactory.getLog(GatewayFilter.class);
private static final String REQUEST_TIME_BEGIN = "requestTimeBegin";
private static final String KEY = "withParams";
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList(KEY);
}
public RequestTimeGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
exchange.getAttributes().put(REQUEST_TIME_BEGIN, System.currentTimeMillis());
return chain.filter(exchange).then(
Mono.fromRunnable(() -> {
Long startTime = exchange.getAttribute(REQUEST_TIME_BEGIN);
if (startTime != null) {
StringBuilder sb = new StringBuilder(exchange.getRequest().getURI().getRawPath())
.append(": ")
.append(System.currentTimeMillis() - startTime)
.append("ms");
if (config.isWithParams()) {
sb.append(" params:").append(exchange.getRequest().getQueryParams());
}
log.info(sb.toString());
}
})
);
};
}
public static class Config {
private boolean withParams;
public boolean isWithParams() {
return withParams;
}
public void setWithParams(boolean withParams) {
this.withParams = withParams;
}
}
}
在上面的代碼中 apply(Config config)方法內創建了一個GatewayFilter的匿名類,具體的實現邏輯跟之前一樣,只不過加了是否打印請求參數的邏輯,而這個邏輯的開關是config.isWithParams()。靜態內部類類Config就是為了接收那個boolean類型的參數服務的,里邊的變量名可以隨意寫,但是要重寫List shortcutFieldOrder()這個方法。
需要注意的是,在類的構造器中一定要調用下父類的構造器把Config類型傳過去,否則會報ClassCastException
最后,需要在工程的啟動文件Application類中,向Srping Ioc容器注冊RequestTimeGatewayFilterFactory類的Bean。
@Bean
public RequestTimeGatewayFilterFactory elapsedGatewayFilterFactory() {
return new RequestTimeGatewayFilterFactory();
}
配置項:
spring:
cloud:
gateway:
routes:
- id: elapse_route
uri: http://httpbin.org:80/get
filters:
- RequestTime=false
predicates:
- After=2017-01-20T17:42:47.789-07:00[America/Denver]
profiles: elapse_route
現在可以通過配置來決定是否開啟打印時間的日志
自定義Global Filter
該GlobalFilter會校驗請求中是否包含了“token”,如何不包含請求參數“token”則不轉發路由,否則執行正常的邏輯。代碼如下:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 TokenFilter implements GlobalFilter, Ordered {
Logger logger= LoggerFactory.getLogger( TokenFilter.class );
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getHeaders().getFirst("token");
if (token == null || token.isEmpty()) {
logger.info( "token is empty..." );
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -100;
}
}
在上面的TokenFilter需要實現GlobalFilter和Ordered接口,這和實現GatewayFilter很類似。然后根據ServerWebExchange獲取ServerHttpRequest,然后根據ServerHttpRequest中是否含有參數token,如果沒有則完成請求,終止轉發,否則執行正常的邏輯。
然后需要將TokenFilter在工程的啟動類中注入到Spring Ioc容器中,代碼如下:
@Bean
public TokenFilter tokenFilter(){
return new TokenFilter();
}