spring-cloud-gateway 服務網關

  Spring Cloud Gateway是Spring Cloud官方推出的第二代網關框架,取代Zuul網關。網關作爲流量的,在微服務系統中有着非常作用,網關常見的功能有路由轉發、權限校驗、限流控制等作用。

  Spring Cloud Gateway是Spring官方最新推出的一款基於Spring Framework 5,Project Reactor和Spring Boot 2之上開發的網關。與zuul1.0不同的是,gateway是異步非阻塞的(netty+webflux實現),zuul1.0是同步阻塞請求的。gateway的數據是封裝在ServerWebExchange中,zuul是存放在RequestContext裏的(這裏是重點,圈起來!)Gateway相對於Zuul來說,在路由的配置上更加多樣化,配置更加簡便。

  官方文檔 : https://docs.spring.io/spring-cloud-gateway/docs/2.2.5.RELEASE/reference/html/#gateway-starter

Gateway 核心概念:

  1. Route 路由,它是網關的基礎元素,包含ID、目標URI、斷言、過濾器組成,當前請求到達網關時,會通過Gateway Handler Mapping,基於斷言進行路由匹配,當斷言爲true時,匹配到路由進行轉發

  2. Predicate,斷言,學過java8的同學應該知道這個函數,它可以允許開發人員去匹配HTTP請求中的元素,一旦匹配爲true,則表示匹配到合適的路由進行轉發

  3. Filter,過濾器,可以在請求發出的前後進行一些業務上的處理,比如授權、埋點、限流等。

Gateway 工作模型:

  其中,predicate就是我們的匹配條件;而filter,就可以理解爲一個無所不能的攔截器。有了這兩個元素,再加上目標uri,就可以實現一個具體的路由了。客戶端向 Spring Cloud Gateway 發出請求,如果請求與網關程序定義的路由匹配,則該請求就會被髮送到網關 Web 處理程序,此時處理程序運行特定的請求過濾器鏈。過濾器之間用虛線分開的原因是過濾器可能會在發送代理請求的前後執行邏輯。所有 pre 過濾器邏輯先執行,然後執行代理請求;代理請求完成後,執行 post 過濾器邏輯。

Predicate 路由斷言:

  Spring Cloud Gateway將路由匹配爲Spring WebFlux HandlerMapping基礎設施的一部分。Spring Cloud Gateway包括許多內置的路由謂詞工廠。所有這些謂詞都匹配HTTP請求的不同屬性。您可以使用邏輯和語句組合多個路由謂詞工廠。

  Gateway 的路由斷言機制以 AbstractRoutePredicateFactory 爲基礎,實現瞭如下多種方式。再官方文檔內提供了配置說明

Gateway 網關的實現:

  要在項目中引入Spring Cloud Gateway,引入相關依賴,然後只需要一些簡單的配置即可構建好一個 Gateway 網關服務。

1.引入依賴(基於 spring-cloud-dependencies  Hoxton.SR4 版本)

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

2. 以 PathRoutePredicateFactory 路由斷言爲例子演示,做以下配置:

server:
  port: 9544

spring:
  application:
    name: gateway-service
  cloud:
    gateway:
      enabled: true
      discovery:
        locator:
          enabled: false #gateway開啓服務註冊和發現的功能
          lowerCaseServiceId: true #請求路徑上的服務名配置爲小寫
      routes:
        - id: ribbon-server
          uri: lb://RIBBON-SERVER #uri以lb://開頭(lb代表從註冊中心獲取服務),後面接的就是你需要轉發到的服務名稱
          predicates:
            - Path=/demo/**
          filters:
            - StripPrefix=1 # 代表 Path 的值中將第一段捨棄,本例子就是轉發的時候爲 /** 將/demo 去除。

eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka/
  instance:
    instance-id: gateway-service

3.啓動服務訪問相關的路徑即可看到效果。

  自定義Predicate 路由斷言的實現:

  創建一個類,繼承 AbstractRoutePredicateFactory 類,實現對應方法:

@Component
public class AuthRoutePredicateFactory extends AbstractRoutePredicateFactory<AuthRoutePredicateFactory.Config> {

    public AuthRoutePredicateFactory() {
        super(Config.class);
    }

    private static final String NAME_KEY = "name";
    private static final String VALUE_KEY = "value";

    @Override
    public List<String> shortcutFieldOrder() {
        //屬性進行匹配對應
        return Arrays.asList(NAME_KEY, VALUE_KEY);
    }

    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        //Header中攜帶了某個值,進行header的判斷
        return (exchange -> {
            HttpHeaders headers = exchange.getRequest().getHeaders();
            List<String> headerList = headers.get(config.getName());
            return headerList.size() > 0;
        });
    }

    public static class Config {
        private String name;
        private String value;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getValue() {
            return value;
        }

        public void setValue(String value) {
            this.value = value;
        }
    }
}

  yml 配置如下:

spring:
  application:
    name: gateway-service
  cloud:
    gateway:
      enabled: true
      discovery:
        locator:
          enabled: false #gateway開啓服務註冊和發現的功能
          lowerCaseServiceId: true #請求路徑上的服務名配置爲小寫
      routes:
        - id: cookie_route
          predicates:
            - Auth=Authorization,token
          filters:
            - StripPrefix=1
          uri: lb://RIBBON-SERVER

  其中 Authorization 爲 name,token 爲 value,打個斷點看看就行了。

Filter 請求過濾器:

  Filter分爲全局過濾器和路由過濾器,當請求與路由匹配時,過濾Web處理程序會將的所有實例GlobalFilter和所有特定GatewayFilter於路由的實例添加到過濾器鏈中。該組合的過濾器鏈按org.springframework.core.Ordered接口排序,您可以通過實現該getOrder()方法進行設置。

  需要獲取到更詳細的信息可以慘開官網 :https://docs.spring.io/spring-cloud-gateway/docs/2.2.5.RELEASE/reference/html/#gatewayfilter-factories

  這裏演示一下自定義的過濾器(全局、路由)

1.全局過濾器只需要實現接口  org.springframework.cloud.gateway.filter.GlobalFilter,而後無需任何配置,即可生效。

@Component
public class CustomGlobalFilter implements GlobalFilter, Ordered {

    Logger logger= LoggerFactory.getLogger(CustomGlobalFilter.class);

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        logger.info("custom global filter");
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return -1;
    }
}

2.路由過濾器 需要繼承  org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory

每個過濾器工廠都對應一個實現類,並且這些類的名稱必須以 GatewayFilterFactory 結尾,這是Spring Cloud Gateway的一個約定,例如 AddRequestHeader 對應的實現類爲 AddRequestHeaderGatewayFilterFactory 

@Component
public class WuzzDefineGatewayFilterFactory extends AbstractGatewayFilterFactory<WuzzDefineGatewayFilterFactory.WuzzConfig>{

    private static final String NAME_KEY="name";

    Logger logger= LoggerFactory.getLogger(WuzzDefineGatewayFilterFactory.class);

    public WuzzDefineGatewayFilterFactory() {
        super(WuzzConfig.class);
    }

    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList(NAME_KEY);
    }

    @Override
    public GatewayFilter apply(WuzzConfig config) {
        //Filter pre  post
        return ((exchange,chain)->{
            logger.info("[pre] Filter Request, name:"+config.getName());
            //TODO
            return chain.filter(exchange).then(Mono.fromRunnable(()->{
                //TODO
                logger.info("[post]: Response Filter");
            }));
        });
    }

    public static class WuzzConfig{
        private String name;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }
}

3. 配置路由規則

spring:
  application:
    name: gateway-service
  cloud:
    gateway:
      enabled: true
      discovery:
        locator:
          enabled: false #gateway\u5F00\u542F\u670D\u52A1\u6CE8\u518C\u548C\u53D1\u73B0\u7684\u529F\u80FD
          lowerCaseServiceId: true #\u8BF7\u6C42\u8DEF\u5F84\u4E0A\u7684\u670D\u52A1\u540D\u914D\u7F6E\u4E3A\u5C0F\u5199
      routes:
        - id: config_route
          predicates:
            - Path=/demo/**
          filters:
            - StripPrefix=1
            - WuzzDefine=Hello Wuzz
          uri: lb://RIBBON-SERVER

4. 啓動測試,發送一個請求,可以在控制檯看到如下信息,說明過濾器均生效:

  使用自帶的限流過濾器 :

spring:
  application:
    name: gateway-service
  cloud:
    gateway:
      enabled: true
      discovery:
        locator:
          enabled: false #gateway\u5F00\u542F\u670D\u52A1\u6CE8\u518C\u548C\u53D1\u73B0\u7684\u529F\u80FD
          lowerCaseServiceId: true #\u8BF7\u6C42\u8DEF\u5F84\u4E0A\u7684\u670D\u52A1\u540D\u914D\u7F6E\u4E3A\u5C0F\u5199
      routes:
        - id: config_route
          predicates:
            - Path=/demo/**
          filters:
            - StripPrefix=1
            - WuzzDefine=Hello Wuzz
          uri: lb://RIBBON-SERVER
        - id: ratelimiter_route
          predicates:
            - Path=/ratelimiter/**
          filters:
            - StripPrefix=1
            - name: RequestRateLimiter
              args:
                deny-empty-key: true
                keyResolver: '#{@ipAddressKeyResolver}'
                redis-rate-limiter.replenishRate: 1
                redis-rate-limiter.burstCapacity: 2
          uri: lb://RIBBON-SERVER

  以上配置了限流過濾器,

  1. replenishRate:令牌桶中令牌的填充速度,代表允許每秒執行的請求數。
  2. burstCapacity:令牌桶的容量,也就是令牌桶最多能夠容納的令牌數。表示每秒用戶最大能夠執行的請求數量。

  其中還需要配置一個   keyResolver:

@Component
public class IpAddressKeyResolver implements KeyResolver{

    @Override
    public Mono<String> resolve(ServerWebExchange exchange) {
        return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
    }
}

  然後啓動測試,迅速刷新頁面訪問接口,會限流:

動態路由:

  Spring Cloud Gateway 提供了 Endpoint 端點,暴露路由信息,有獲取所有路由、刷新路由、查看單個路由、刪除路由等方法,源碼在 org.springframework.cloud.gateway.actuate.GatewayControllerEndpoint 中,想訪問端點中的方法需要添加 spring-boot-starter-actuator 依賴,並在配置文件中暴露所有端點

management:
  endpoints:
    web:
      exposure:
        include: "*"

  列舉幾個常用的操作API 。詳細信息及配置查看官方文檔:https://docs.spring.io/spring-cloud-gateway/docs/2.2.5.RELEASE/reference/html/#actuator-api

  • /actuator/gateway/routes GET 獲取路由列表
  • /actuator/gateway/globalfilters  GET 全局過濾器列表
  • /actuator/gateway/routefilters  GET 路由過濾器列表
  • /actuator/gateway/refresh   POST  刷新新增的路由
  • /gateway/routes/{id_route_to_create}   POST/DELETS  新增或者刪除
  • ......

  新增路由示例 :

  然後調用一下 刷新的接口,即可生效。但是默認情況下,我們新增的路由只是保存在內存中,萬一服務重啓,則配置信息丟失,這個時候就需要將路由信息持久化。

  路由持久化--基於redis

1. 引入 redis 依賴:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
</dependency>

2. 實現 org.springframework.cloud.gateway.route.RouteDefinitionRepository 接口,重寫路由的相關操作方法

@Component
public class RedisRouteDefinitionRepository implements RouteDefinitionRepository {

    private final static String GATEWAY_ROUTE_KEY="gateway_dynamic_route";

    @Autowired
    RedisTemplate<String,String> redisTemplate;

    @Override
    public Flux<RouteDefinition> getRouteDefinitions() {
        List<RouteDefinition> routeDefinitionList=new ArrayList<>();
        redisTemplate.opsForHash().values(GATEWAY_ROUTE_KEY).stream().forEach(route->{
            routeDefinitionList.add(JSON.parseObject(route.toString(),RouteDefinition.class));
        });
        return Flux.fromIterable(routeDefinitionList);
    }

    @Override
    public Mono<Void> save(Mono<RouteDefinition> route) {
        return route.flatMap(routeDefinition -> {
            redisTemplate.opsForHash().put(GATEWAY_ROUTE_KEY,routeDefinition.getId(),JSON.toJSONString(routeDefinition));
            return Mono.empty();
        });
    }

    @Override
    public Mono<Void> delete(Mono<String> routeId) {
        return routeId.flatMap(id->{
            if(redisTemplate.opsForHash().hasKey(GATEWAY_ROUTE_KEY,id)){
                redisTemplate.opsForHash().delete(GATEWAY_ROUTE_KEY,id);
                return Mono.empty();
            }
            return Mono.defer(()->Mono.error(new Exception("routeDefinition not found:"+routeId)));
        });
    }
}

3.配置路由

spring:
  application:
    name: gateway-service
  cloud:
    gateway:
      enabled: true
      discovery:
        locator:
          enabled: false #gateway\u5F00\u542F\u670D\u52A1\u6CE8\u518C\u548C\u53D1\u73B0\u7684\u529F\u80FD
          lowerCaseServiceId: true #\u8BF7\u6C42\u8DEF\u5F84\u4E0A\u7684\u670D\u52A1\u540D\u914D\u7F6E\u4E3A\u5C0F\u5199
      routes:
        - id: config_route
          predicates:
            - Path=/demo/**
          filters:
            - StripPrefix=1
            - WuzzDefine=Hello Wuzz
          uri: lb://RIBBON-SERVER

4. 測試,啓動後按照之前一樣創建一個路由,這個時候發現 redis裏面已經保存了這個路由配置信息

  也可以通過配置中心,比如 config 、Nacos 實現動態的配置。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章