java B2B2C Springboot多租戶電子商城系統- gateway之filter篇

it小飛俠的微博發表於2019-04-09

Filter:過濾器,它需要實現javax.servlet.Filter介面。Filter是過濾器,與攔截器不同。過濾器是先於與之相關的Servlet和JSP執行在伺服器上。

Filter的生命週期

程式啟動呼叫Filter的init()方法(永遠只呼叫一次),程式停止呼叫Filter的destroy()方法(永遠只呼叫一次),doFilter()方法每次的訪問請求如果符合攔截條件都會呼叫(程式第一次執行,會在servlet呼叫init()方法以後呼叫,不管第幾次,都在呼叫doGet(),doPost()方法之前)。需要JAVA Spring Cloud大型企業分散式微服務雲構建的B2B2C電子商務平臺原始碼 一零三八七七四六二六

現在挑幾個常見的過濾器工廠來講解,每一個過濾器工廠在官方文件都給出了詳細的使用案例,如果不清楚的還可以在org.springframework.cloud.gateway.filter.factory看每一個過濾器工廠的原始碼。

AddRequestHeader GatewayFilter Factory

建立工程,引入相關的依賴,包括spring boot 版本2.0.5,spring Cloud版本Finchley,gateway依賴如下:

<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

複製程式碼

在工程的配置檔案中,加入以下的配置:

server:
  port: 8081
spring:
  profiles:
    active: add_request_header_route

---
spring:
  cloud:
    gateway:
      routes:
      - id: add_request_header_route
        uri: http://httpbin.org:80/get
        filters:
        - AddRequestHeader=X-Request-Foo, Bar
        predicates:
        - After=2017-01-20T17:42:47.789-07:00[America/Denver]
  profiles: add_request_header_route

複製程式碼

在上述的配置中,工程的啟動埠為8081,配置檔案為add_request_header_route,在add_request_header_route配置中,配置了roter的id為add_request_header_route,路由地址為http://httpbin.org:80/get,該router有AfterPredictFactory,有一個filter為AddRequestHeaderGatewayFilterFactory(約定寫成AddRequestHeader),AddRequestHeader過濾器工廠會在請求頭加上一對請求頭,名稱為X-Request-Foo,值為Bar。為了驗證AddRequestHeaderGatewayFilterFactory是怎麼樣工作的,檢視它的原始碼,AddRequestHeaderGatewayFilterFactory的原始碼如下:


public class AddRequestHeaderGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {

	@Override
	public GatewayFilter apply(NameValueConfig config) {
		return (exchange, chain) -> {
			ServerHttpRequest request = exchange.getRequest().mutate()
					.header(config.getName(), config.getValue())
					.build();

			return chain.filter(exchange.mutate().request(request).build());
		};
    }

}

複製程式碼

由上面的程式碼可知,根據舊的ServerHttpRequest建立新的 ServerHttpRequest ,在新的ServerHttpRequest加了一個請求頭,然後建立新的 ServerWebExchange ,提交過濾器鏈繼續過濾。

啟動工程,通過curl命令來模擬請求:


curl localhost:8081


複製程式碼

最終顯示了從 httpbin.org:80/get得到了請求,響應…

{
  "args": {},
  "headers": {
    "Accept": "*/*",
    "Connection": "close",
    "Forwarded": "proto=http;host=\"localhost:8081\";for=\"0:0:0:0:0:0:0:1:56248\"",
    "Host": "httpbin.org",
    "User-Agent": "curl/7.58.0",
    "X-Forwarded-Host": "localhost:8081",
    "X-Request-Foo": "Bar"
  },
  "origin": "0:0:0:0:0:0:0:1, 210.22.21.66",
  "url": "http://localhost:8081/get"
}

複製程式碼

可以上面的響應可知,確實在請求頭中加入了X-Request-Foo這樣的一個請求頭,在配置檔案中配置的AddRequestHeader過濾器工廠生效。

跟AddRequestHeader過濾器工廠類似的還有AddResponseHeader過濾器工廠,在此就不再重複。

RewritePath GatewayFilter Factory

在Nginx服務啟中有一個非常強大的功能就是重寫路徑,Spring Cloud Gateway預設也提供了這樣的功能,這個功能是Zuul沒有的。在配置檔案中加上以下的配置:


spring:
 profiles:
   active: rewritepath_route
---
spring:
 cloud:
   gateway:
     routes:
     - id: rewritepath_route
       uri: https://blog.csdn.net
       predicates:
       - Path=/foo/**
       filters:
       - RewritePath=/foo/(?<segment>.*), /$\{segment}
 profiles: rewritepath_route

複製程式碼

上面的配置中,所有的/foo/**開始的路徑都會命中配置的router,並執行過濾器的邏輯,在本案例中配置了RewritePath過濾器工廠,此工廠將/foo/(?.*)重寫為{segment},然後轉發到https://blog.csdn.net。比如在網頁上請求localhost:8081/foo/forezp,此時會將請求轉發到https://blog.csdn.net/forezp的頁面,比如在網頁上請求localhost:8081/foo/forezp/1,頁面顯示404,就是因為不存在https://blog.csdn.net/forezp/1這個頁面。

自定義過濾器

Spring Cloud Gateway內建了19種強大的過濾器工廠,能夠滿足很多場景的需求,那麼能不能自定義自己的過濾器呢,當然是可以的。在spring Cloud Gateway中,過濾器需要實現GatewayFilter和Ordered2個介面。寫一個RequestTimeFilter,程式碼如下:


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 0;
   }
}

複製程式碼

在上面的程式碼中,Ordered中的int getOrder()方法是來給過濾器設定優先順序別的,值越大則優先順序越低。還有有一個filterI(exchange,chain)方法,在該方法中,先記錄了請求的開始時間,並儲存在ServerWebExchange中,此處是一個“pre”型別的過濾器,然後再chain.filter的內部類中的run()方法中相當於"post"過濾器,在此處列印了請求所消耗的時間。然後將該過濾器註冊到router中,程式碼如下:

    @Bean
    public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) {
        // @formatter:off
        return builder.routes()
                .route(r -> r.path("/customer/**")
                        .filters(f -> f.filter(new RequestTimeFilter())
                                .addResponseHeader("X-Response-Default-Foo", "Default-Bar"))
                        .uri("http://httpbin.org:80/get")
                        .order(0)
                        .id("customer_filter_router")
                )
                .build();
        // @formatter:on
    }


複製程式碼

重啟程式,通過curl命令模擬請求: curl localhost:8081/customer/123

在程式的控制檯輸出一下的請求資訊的日誌:

2018-11-16 15:02:20.177 INFO 20488 --- [ctor-http-nio-3] o.s.cloud.gateway.filter.GatewayFilter : /customer/123: 152ms

自定義過濾器工廠

在上面的自定義過濾器中,有沒有辦法自定義過濾器工廠類呢?這樣就可以在配置檔案中配置過濾器了。現在需要實現一個過濾器工廠,在列印時間的時候,可以設定引數來決定是否列印請引數。

過濾器工廠的頂級介面是GatewayFilterFactory,有2個兩個較接近具體實現的抽象類,分別為AbstractGatewayFilterFactory和AbstractNameValueGatewayFilterFactory,這2個類前者接收一個引數,比如它的實現類RedirectToGatewayFilterFactory;後者接收2個引數,比如它的實現類AddRequestHeaderGatewayFilterFactory類。現在需要將請求的日誌列印出來,需要使用一個引數,這時可以參照RedirectToGatewayFilterFactory的寫法。


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:
  profiles:
    active: elapse_route

---
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

複製程式碼

啟動工程,在瀏覽器上訪問localhost:8081?name=forezp,可以在控制檯上看到,日誌輸出了請求消耗的時間和請求引數。

global filter

Spring Cloud Gateway根據作用範圍劃分為GatewayFilter和GlobalFilter,二者區別如下:

GatewayFilter : 需要通過spring.cloud.routes.filters 配置在具體路由下,只作用在當前路由上或通過spring.cloud.default-filters配置在全域性,作用在所有路由上

GlobalFilter : 全域性過濾器,不需要在配置檔案中配置,作用在所有的路由上,最終通過GatewayFilterAdapter包裝成GatewayFilterChain可識別的過濾器,它為請求業務以及路由的URI轉換為真實業務服務的請求地址的核心過濾器,不需要配置,系統初始化時載入,並作用在每個路由上。

在下面的案例中將講述如何編寫自己GlobalFilter,該GlobalFilter會校驗請求中是否包含了請求引數“token”,如何不包含請求引數“token”則不轉發路由,否則執行正常的邏輯。程式碼如下:


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().getQueryParams().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();
}

複製程式碼

啟動工程,使用curl命令請求:

 curl localhost:8081/customer/123
 
複製程式碼

可以看到請沒有被轉發,請求被終止,並在控制檯列印瞭如下日誌:

2018-11-16 15:30:13.543  INFO 19372 --- [ctor-http-nio-2] gateway.TokenFilter         : token is empty...
複製程式碼

上面的日誌顯示了請求進入了沒有傳“token”的邏輯。

總結

本篇文章講述了Spring Cloud Gateway中的過濾器,包括GatewayFilter和GlobalFilter。從官方文件的內建過濾器講起,然後講解自定義GatewayFilter、GatewayFilterFactory以及自定義的GlobalFilter。有很多內建的過濾器並沒有講述到,比如限流過濾器,這個我覺得是比較重要和大家關注的過濾器,將在之後的文章講述。

 java B2B2C Springboot多租戶電子商城系統 

相關文章