Spring Cloud Gateway学习

简介

本文主要是用来学习Spring Cloud gateway网关使用的;API网关作为后端服务的统一入口,可提供请求路由、协议转换、安全认证、服务鉴权、流量控制、日志监控等服务。

概览

gateway网关处理流程图如下:


网关
  1. 请求发送到网关,DispatcherHandler是Http请求的中央分发器,将请求匹配到相应的HandlerMapping;
  2. 请求和处理器之间有一个映射关系,网关将会对请求进行路由,handler会匹配RoutePredicateHandlerMapping,以匹配到对应的Route;
  3. 经过RoutePredicateHandlerMapping处理后,请求会发送到Web处理器,该WebHandler代理了一系列网关过滤器和全局过滤,此时会对请求或者响应头进行处理;
  4. 最后转发到具体的代理服务;

DispatcherHandler--->RoutePredicateHandlerMapping
RoutePredicateHandlerMapping---->FilteringWebHandler
FilteringWebHandler----->DefaultGatewayFilterChain

Route路由

什么是路由?一个路由就代表一个真实的下游请求路径,路由在gateway里面有两个重要的辅助概念-路由定义和定位器;

  • 路由定义RouteDefinition:路由定义是Gateway Properties中的属性,可以定义获取路由的方式;
  • 定位器Locator:定位器分为路由定位器RouteLocatorRouteDefinitionLocator,
    RouteLocator用来获取routes,而RouteDefinitionLocator则用来获RouteDefinitions;

RouteDefinition路由定义

路由定义有五个基本属性:

  • id: 路由id,默认为uuid;
  • predicates: 路由断言定义列表;
  • fliters:该路由需要经过的过滤器;
  • URI:该路由请求的路径;
  • order:优先级 ;

定位器

RouteLocator

用来获取路由的定位器,其源码如下:

public interface RouteLocator {

    Flux<Route> getRoutes();
}

其内置了3个路由定位器实现类:

  • CachingRouteLocator:具备缓存功能的路由定位器,其内具有监听RefreshRoutesEvent事件,并能及时刷新缓存;
    @EventListener(RefreshRoutesEvent.class)
    /* for testing */ void handleRefresh() {
        refresh();
    }
  • CompositeRouteLocator:组合路由定位器,其内有代理了一系列的定位器,来实现路由查询;
  • RouteDefinitionRouteLocator:基于路由定义的路由定位器,根据路由定义定位器获取路由定义并将其转换成对应的路由;代码如下:
public Flux<Route> getRoutes() {
        return this.routeDefinitionLocator.getRouteDefinitions()
                .map(this::convertToRoute)
                //TODO: error handling
                .map(route -> {
                    if (logger.isDebugEnabled()) {
                        logger.debug("RouteDefinition matched: " + route.getId());
                    }
                    return route;
                });
    }

也可以自定义一个路由定位器,如下代码:



@SpringBootApplication
public class DemogatewayApplication {
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
            .route("path_route", r -> r.path("/get")
                .uri("http://httpbin.org"))//将所有/get开头的请求路径转发到httpbin.org
            .route("host_route", r -> r.host("*.myhost.org")
                .uri("http://httpbin.org"))//将域名以myhost.org结尾的请求转发
            .route("rewrite_route", r -> r.host("*.rewrite.org")
                .filters(f -> f.rewritePath("/foo/(?<segment>.*)", "/${segment}"))
                .uri("http://httpbin.org"))//将域名以myhost.org结尾的请求转发并进行路径重写,保留/foo/后面路径;
            .route("hystrix_route", r -> r.host("*.hystrix.org")
                .filters(f -> f.hystrix(c -> c.setName("slowcmd")))
                .uri("http://httpbin.org"))
            .route("hystrix_fallback_route", r -> r.host("*.hystrixfallback.org")
                .filters(f -> f.hystrix(c -> c.setName("slowcmd").setFallbackUri("forward:/hystrixfallback")))
                .uri("http://httpbin.org"))
            .route("limit_route", r -> r
                .host("*.limited.org").and().path("/anything/**")
                .filters(f -> f.requestRateLimiter(c -> c.setRateLimiter(redisRateLimiter())))
                .uri("http://httpbin.org"))
            .build();
    }
}


路由定义定位器

路由定义定位器用来获取路由定义的,路由定义可以在配置文件中进行设置,gateway内置了三个路由定义定位器:

  • CompositeRouteDefinitionLocator:其内代理了一系列RouteDefinitionLocators,通过其来获取路由定义集合;
  • RouteDefinitionRepository & InMemoryRouteDefinitionRepository:该定位器实现了路由定义的增删改查等操作,gateway内置端点接口会用到这些操作,同时也可以通过该路由定义定位器可以实现路由仓库,来动态更新路由定义信息;
  • CachingRouteDefinitionLocator:带有缓存功能路由定义定位器;
  • DiscoveryClientRouteDefinitionLocator:服务发现路由定义定位器,通过服务发现机制以及Spel表达式来动态更新路由定义信息;获取路由源码如下:
@Override
    public Flux<RouteDefinition> getRouteDefinitions() {
                 //获取Spel表达式
        SpelExpressionParser parser = new SpelExpressionParser();
        Expression includeExpr = parser.parseExpression(properties.getIncludeExpression());
        Expression urlExpr = parser.parseExpression(properties.getUrlExpression());

        Predicate<ServiceInstance> includePredicate;
        if (properties.getIncludeExpression() == null || "true".equalsIgnoreCase(properties.getIncludeExpression())) {
            includePredicate = instance -> true;
        } else {
            includePredicate = instance -> {
                Boolean include = includeExpr.getValue(evalCtxt, instance, Boolean.class);
                if (include == null) {
                    return false;
                }
                return include;
            };
        }
              //获取service clients以及其instances,通过serviceId替换的方式获取路由定义
        return Flux.fromIterable(discoveryClient.getServices())
                .map(discoveryClient::getInstances)
                .filter(instances -> !instances.isEmpty())
                .map(instances -> instances.get(0))
                .filter(includePredicate)
                .map(instance -> {
                    String serviceId = instance.getServiceId();

                    RouteDefinition routeDefinition = new RouteDefinition();
                    routeDefinition.setId(this.routeIdPrefix + serviceId);
                    String uri = urlExpr.getValue(evalCtxt, instance, String.class);
                    routeDefinition.setUri(URI.create(uri));

                    final ServiceInstance instanceForEval = new DelegatingServiceInstance(instance, properties);

                    for (PredicateDefinition original : this.properties.getPredicates()) {
                        PredicateDefinition predicate = new PredicateDefinition();
                        predicate.setName(original.getName());
                        for (Map.Entry<String, String> entry : original.getArgs().entrySet()) {
                            String value = getValueFromExpr(evalCtxt, parser, instanceForEval, entry);
                            predicate.addArg(entry.getKey(), value);
                        }
                        routeDefinition.getPredicates().add(predicate);
                    }

                    for (FilterDefinition original : this.properties.getFilters()) {
                        FilterDefinition filter = new FilterDefinition();
                        filter.setName(original.getName());
                        for (Map.Entry<String, String> entry : original.getArgs().entrySet()) {
                            String value = getValueFromExpr(evalCtxt, parser, instanceForEval, entry);
                            filter.addArg(entry.getKey(), value);
                        }
                        routeDefinition.getFilters().add(filter);
                    }

                    return routeDefinition;
                });
    }
  • PropertiesRouteDefinitionLocator: 通过配置文件来获取路由定义集合,其源码如下:
@Override
    public Flux<RouteDefinition> getRouteDefinitions() {
        return Flux.fromIterable(this.properties.getRoutes());
    }

Predicate断言

Predicate通过Predicate函数式接口来判断当前请求是否满足选择条件,Predicate分为以下条件划分为不同的路由断言工厂类

Datetime类型

根据日期判断是否满足路由选择条件,其有三个工厂类:

  • AfteRoutePredicateFactory: 接收一个日期参数,判断请求日期是否晚于指定日志;
  • BeforeRoutePredicateFactory:接收一个日期参数,判断请求日期是否早于指定日期;
  • BetweenRoutePredicateFactory:接收两个日期参数,判断请求日期是否位于之间;
    其配置如下:
spring:
    cloud: 
        gateway:
            routes: 
            - id: after_route_id
            uri: http://www.baidu.com
            predicates: 
            - After= 2018-12-30T23:59:59.789+08:00[Asia/Shanghai]

Cookie类型

根据请求的Cookie正则匹配是否通过选择条件;
唯一工厂类:CookieRoutePredicateFactory;
其配置实例如下:

spring:
  cloud:
    gateway:
      routes:
      - id: cookie_route
        uri: https://example.org
        predicates:
        - Cookie=chocolate, ch.p

Header头断言类型

请求头属性正则匹配是否通过选择,唯一工厂类:HeaderRoutePredicateFactory,其配置如下:

spring:
  cloud:
    gateway:
      routes:
      - id: header_route
        uri: https://example.org
        predicates:
        - Header=X-Request-Id, \d+

Host域名断言类型

Host域名正则匹配,是否通过选择,唯一工厂类:HostRoutePredicateFactory,
其配置如下:

spring:
  cloud:
    gateway:
      routes:
      - id: host_route
        uri: https://example.org
        predicates:
        - Host=**.somehost.org,**.anotherhost.org

Method断言类型

基于方法类型来进行相应匹配选择,唯一工厂类MethodRoutePredicateFactory;
其配置如下:

spring:
  cloud:
    gateway:
      routes:
      - id: method_route
        uri: https://example.org
        predicates:
        - Method=GET

Path断言类型

Path断言类型会通过接受的两个参数来判断是否匹配选择,唯一工厂类:PathRoutePredicateFactory,其配置如下:

spring:
  cloud:
    gateway:
      routes:
      - id: host_route
        uri: https://example.org
        predicates:
        - Path=/foo/{segment},/bar/{segment}

Query查询类型

Query类型通过接受两个参数:一个要求的参数和一个可选的regexp表达式,来匹配是否选择,其工厂类为QueryRoutePredicateFactory,配置示例如下:

spring:
  cloud:
    gateway:
      routes:
      - id: query_route
        uri: https://example.org
        predicates:
        - Query=baz,ba.

路由必须满足有一个baz的查询参数,且该参数值必须满足ba.表达式,方可通过请求;

RemoteAddr类型

该类型通过设置一系列的IPv4或者IPv6地址以及子网掩码形式的字符串,来进行匹配,其唯一工厂类为:RemoteAddrRoutePredicateFactory;
其配置如下:

spring:
  cloud:
    gateway:
      routes:
      - id: remoteaddr_route
        uri: https://example.org
        predicates:
        - RemoteAddr=192.168.1.1/24

路由请求必须满足192.168.1.1/24指定的ip域内方可通过选择;

Weight类型

Weight类型通过设置权重,来进行路由选择,其配置如下:

spring:
  cloud:
    gateway:
      routes:
      - id: weight_high
        uri: https://weighthigh.org
        predicates:
        - Weight=group1, 8
      - id: weight_low
        uri: https://weightlow.org
        predicates:
        - Weight=group1, 2

上例说明路由百分之80将发向https://weighthigh.org,百分20发向https://weightlow.org

Filter过滤器

配置文件

fliter需要在配置文件bootstrap.yml中进行相应设置,或者手动定义一个实现了Filter;
配置文件设置filter属性如下:

    gateway:
        discovery:
           locator:
              enabled: true
              predicates:
                  -  Path='/api/**/'+serviceId+'/**'                 
              filters:
              - name: RewritePath
                args: 
                     regexp: "'/api/.*/' + serviceId + '/(?<remaining>.*)'"                 
                     replacement: "'/${remaining}'"

通过设置name便可调用相应的过滤器工厂类创建一个过滤器,上例中,前缀RewritePath便会通过RewritePathGatewayFilterFactory创建一个GatewayFilter对象,来进行相应过滤操作;

GlobalFilter

gateway网关Filter分为以下Global和Define,其中Gobal有以下几种(按ordered从小到大):

  • AdaptCachedBodyGlobalFilter(Integer.Min_VALU):通过适配缓存requestbody参数来实现request的重新构建;

  • NettyWriteResponseFilter(-1):将当前请求的响应进行输出到服务调用方;

  • ForwardPathFilter(0):解析路径并将路径转发

  • GatewayMetricsFilter(0):网关性能测量器,可以用来收集网关请求参数,方便创建一个Grafana Dashbord;

  • RouteToRequestUrlFilter(10000):转换路由中的URI

  • LoadBalancerClientFilter(10100):通过负载均衡客户端根据路由的URL解析转换成真实的请求URL

  • WebsocketRoutingFilter(Integer.MAX_VALUE-1):负责处理Websocket类型请求响应信息;

  • ForwardRoutingFilter(Integer.MAX_VALUE):其根据 forward:// 前缀( Scheme )过滤处理,将请求转发到当前网关实例本地接口。

  • NettyRoutingFilter(Integer.MAX_VALUE)):通过HttpClient客户端转发真实的URL请求到服务提供方,并将响应写入到当前的请求响应中;

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,295评论 6 512
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,928评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,682评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,209评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,237评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,965评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,586评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,487评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,016评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,136评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,271评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,948评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,619评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,139评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,252评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,598评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,267评论 2 358

推荐阅读更多精彩内容