本文由 发布,转载请注明出处,如有问题请联系我们! 发布时间: 2021-05-21API网关才是大势所趋?SpringCloud Gateway保姆级入门教程

加载中

API网关ip才算是必然趋势?SpringCloud Gateway家庭保姆级基础教程

什么叫微服务网关

SpringCloud Gateway是Spring套餐中一个较为新的新项目,Spring小区是那么详细介绍它的:

该新项目依靠Spring WebFlux的工作能力,打造出了一个API网关ip。致力于给予一种简易而合理的方式 来做为API服务项目的路由器,并为他们给予各种各样提高作用,比如:安全系数,监管和可扩展性。

而在真正的业务流程行业,大家常常用SpringCloud Gateway来做微服务网关,假如你不理解微服务网关和传统式网关ip的差别,能够阅读文章此一篇文章 Service Mesh和API Gateway关联深层讨论 来掌握二者的精准定位差别。

以我浅显的了解,传统式的API网关ip,通常是单独于每个后端开发服务项目,要求先用到单独的网关ip层,再打进服务项目群集。而微服务网关,将总流量从南北方迈向改成物品迈向(见下面的图),微服务网关和后端开发服务项目是在同一个器皿中的,因此也有一个别称,称为Gateway Sidecar。

为什么叫Sidecar,这个词应当怎么理解呢,吃鸡游戏里的三蹦子见过没:

摩托就是你的后端开发服务项目,而边上挂着的附加坐椅便是微服务网关,他是依赖于后端开发服务项目的(一般就是指2个过程在同一个器皿中),是否生动形象了一些。

因为自己孤陋寡闻,针对微服务架构有关定义了解上在所难免有误差。就没有此详尽叙述基本原理性的文本了。

文中只讨论SpringCloud Gateway的新手入门构建和实战演练踩坑。 假如朋友们对基本原理有兴趣,能够等事后基本原理剖析文章内容。

注:文中网关ip新项目在小编企业早已发布运作,每日担负上百万等级的要求,是历经实战演练认证的新项目。

文章内容文件目录

  • 从零造一个网关ip
    • 引进pom依靠
    • 撰写yml文件
    • 插口转义难题
    • 获得要求体(Request Body)
  • 踩坑实战演练
    • 获得手机客户端真正IP
    • 尾缀配对
  • 汇总

源码

详细新项目源码早已百度收录到我的GitHub

https://github.com/qqxx6661/springcloud_gateway_demo

从零造一个网关ip

引进pom依靠

我应用了spring-boot 2.2.5.RELEASE做为parent依靠:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.5.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

在dependencyManagement中,大家必须特定sringcloud的版本号,便于确保大家可以引进大家要想的SpringCloud Gateway版本号,因此必须采用dependencyManagement:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Hoxton.SR8</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

最终,是在dependency中引进spring-cloud-starter-gateway:

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

如此一来,大家便引进了2.2.5.RELEASE版本号的网关ip:

除此之外,请检查一下你的依靠中是不是带有spring-boot-starter-web,如果有,请灭掉它。由于大家的SpringCloud Gateway是一个netty webflux完成的web服务器,和Springboot Web本身便是矛盾的。

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>

保证这儿,事实上你的新项目就早已能够运行了,运作SpringcloudGatewayApplication,获得結果如图所示:

撰写yml文件

SpringBoot的关键定义是承诺优先选择于配备,在之前入门Spring时,一直不理解他们的含意,在应用SpringCloud Gateway时,更为深层次的了解了他们。在默认设置状况下,你不用一切的配备,就可以运作起來最基本上的网关ip。对于你以后特殊的要求,再去增加配备。

而SpringCloud Gateway更强劲的一点便是内嵌了十分多的默认设置作用完成,你需要的绝大多数作用,例如在要求中加上一个header,加上一个主要参数,都只必须在yml中引进相对应的内嵌过滤装置就可以。

可以说,yml是全部SpringCloud Gateway的生命。

一个网关ip最基本上的作用,便是配备路由器,在这些方面,SpringCloud Gateway适用十分多方法。例如:

  • 根据時间配对
  • 根据 Cookie 配对
  • 根据 Header 特性配对
  • 根据 Host 配对
  • 根据要求方法配对
  • 根据要求途径配对
  • 根据要求主要参数配对
  • 根据要求 ip 详细地址开展配对

这种在官方网站实例教程中,都是有详尽的详细介绍,即使你百度下,也会出现许多民俗汉语翻译的基础教程,我也不会再过多阐释了,我仅用一个要求途径做一个简易的事例。

在企业的新项目中,因为有新老用户两个后台管理服务项目,大家应用不一样的uri途径开展区别。

  • 老服务项目途径为:url/api/xxxxxx,服务项目端口为8001
  • 新服务项目途径为:url/api/v2/xxxxx,服务项目端口为8002

那麼能够立即在yml里边配备:

logging:
  level:
    org.springframework.cloud.gateway: DEBUG
    reactor.netty.http.client: DEBUG

spring:
  cloud:
    gateway:
      default-filters:
        - AddRequestHeader=gateway-env, springcloud-gateway
      routes:
        - id: "server_v2"
          uri: "http://127.0.0.1:8002"
          predicates:
            - Path=/api/v2/**
        - id: "server_v1"
          uri: "http://127.0.0.1:8001"
          predicates:
            - Path=/api/**

上边的编码表述以下:

  • logging:因为文章内容必须,大家开启gateway和netty的Debug方式,能够看清要求进去后实行的步骤,便捷事后表明。
  • default-filters:我们可以便捷的应用default-filters,在要求中添加一个自定的header,大家添加一个KV为gateway-env:springcloud-gateway,来标明大家这一要求历经了此网关ip。那样做的益处是事后服务器端也可以见到。
  • routes:路由器是网关ip的关键,坚信阅读者们看编码也可以了解,我配备了2个路由器,一个是server_v1的老服务项目,一个是server_v2的新服务项目。一定要注意,一个要求达到好几个路由器的谓词标准时,要求总是被第一个取得成功配对的路由器分享。因为大家老服务项目的路由器是/xx,因此必须将老服务项目放到后边,优先选择配对橙装/v2的新服务项目,不符合的再配对到/xx。

看来一下http://localhost:8080/api/xxxxx的結果:

看来一下http://localhost:8080/api/v2/xxxxx的結果:

能够见到2个要求被恰当的路由器了。因为大家真真正正并沒有打开后端开发服务项目,因此最后一句error请忽视。

插口转义难题

在企业具体的新项目中,我还在构建好网关ip后,碰到了一个插口转义难题,坚信许多阅读者很有可能也会遇到,因此在这儿大家最好防范于未然,优先选择解决下。

难题是那样的,许多老新项目在url上并沒有开展转义,造成会发生以下插口要求,http://xxxxxxxxx/api/b3d56a6fa19975ba520189f3f55de7f6/140x140.jpg?t=1"

那样要求回来,网关ip会出错:

java.lang.IllegalArgumentException: Invalid character '=' for QUERY_PARAM in "http://pic1.ajkimg.com/display/anjuke/b3d56a6fa19975ba520189f3f55de7f6/140x140.jpg?t=1"

在没有改动服务项目编码逻辑性的前提条件下,网关ip实际上早已能够处理这一件事儿,解决方案便是升級到2.1.1.RELEASE之上的版本号。

The issue was fixed in version spring-cloud-gateway 2.1.1.RELEASE.

因此大家一开始便是用了高版本号2.2.5.RELEASE,防止了这个问题,假如小伙伴们发觉以前应用的版本号小于 2.1.1.RELEASE,请升級。

获得要求体(Request Body)

在网关ip的应用中,有时会必须取得要求body里边的数据信息,例如认证签字,body很有可能必须参加签字校检。

可是SpringCloud Gateway因为最底层选用了webflux,其要求是流式的回应的,即 Reactor 程序编写,要载入 Request Body 中的要求主要参数就不容易了。

网上谷歌了好长时间,许多解决方法要不是完全落伍,要不是版本号兼容问题,好在最终参照了本文,总算拥有构思:

https://www.jianshu.com/p/db3b15aec646

最先大家必须将body从要求中拿出来,因为是流式的解决,Request的Body是只有载入一次的,假如立即根据在Filter中载入,会造成后边的服务项目没法获取数据。

SpringCloud Gateway 內部给予了一个肯定加工厂类ReadBodyPredicateFactory,这一类完成了载入Request的Body內容并放进缓存文件,我们可以根据从缓存文件中获得body內容来完成大家的目地。

最先新创建一个CustomReadBodyRoutePredicateFactory类,这儿只贴出来重要编码,详细编码可以看可运作的Github库房:

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

    protected static final Log log = LogFactory.getLog(CustomReadBodyRoutePredicateFactory.class);
    private List<HttpMessageReader<?>> messageReaders;

    @Value("${spring.codec.max-in-memory-size}")
    private DataSize maxInMemory;

    public CustomReadBodyRoutePredicateFactory() {
        super(Config.class);
        this.messageReaders = HandlerStrategies.withDefaults().messageReaders();
    }

    public CustomReadBodyRoutePredicateFactory(List<HttpMessageReader<?>> messageReaders) {
        super(Config.class);
        this.messageReaders = messageReaders;
    }

    @PostConstruct
    private void overrideMsgReaders() {
        this.messageReaders = HandlerStrategies.builder().codecs((c) -> c.defaultCodecs().maxInMemorySize((int) maxInMemory.toBytes())).build().messageReaders();
    }

    @Override
    public AsyncPredicate<ServerWebExchange> applyAsync(Config config) {
        return new AsyncPredicate<ServerWebExchange>() {
            @Override
            public Publisher<Boolean> apply(ServerWebExchange exchange) {
                Class inClass = config.getInClass();
                Object cachedBody = exchange.getAttribute("cachedRequestBodyObject");
                if (cachedBody != null) {
                    try {
                        boolean test = config.predicate.test(cachedBody);
                        exchange.getAttributes().put("read_body_predicate_test_attribute", test);
                        return Mono.just(test);
                    } catch (ClassCastException var6) {
                        if (CustomReadBodyRoutePredicateFactory.log.isDebugEnabled()) {
                            CustomReadBodyRoutePredicateFactory.log.debug("Predicate test failed because class in predicate does not match the cached body object", var6);
                        }
                        return Mono.just(false);
                    }
                } else {
                    return ServerWebExchangeUtils.cacheRequestBodyAndRequest(exchange, (serverHttpRequest) -> {
                        return ServerRequest.create(exchange.mutate().request(serverHttpRequest).build(), CustomReadBodyRoutePredicateFactory.this.messageReaders).bodyToMono(inClass).doOnNext((objectValue) -> {
                            exchange.getAttributes().put("cachedRequestBodyObject", objectValue);
                        }).map((objectValue) -> {
                            return config.getPredicate().test(objectValue);
                        }).thenReturn(true);
                    });
                }
            }

            @Override
            public String toString() {
                return String.format("ReadBody: %s", config.getInClass());
            }
        };
    }

    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        throw new UnsupportedOperationException("ReadBodyPredicateFactory is only async.");
    }
}

编码关键功效:在有body的要求来临时,将body载入出去放进运行内存缓存文件中。若沒有body,则未作一切实际操作。

那样大家便能够在拦截器里应用exchange.getAttribute("cachedRequestBodyObject")获得body体。

正确了,大家都还没演试一个filter是要怎么写的,在这儿就先写一个详细的demofilter。

使我们新创建类DemoGatewayFilterFactory:

@Component
public class DemoGatewayFilterFactory extends AbstractGatewayFilterFactory<DemoGatewayFilterFactory.Config> {

    private static final String CACHE_REQUEST_BODY_OBJECT_KEY = "cachedRequestBodyObject";

    public DemoGatewayFilterFactory() {
        super(Config.class);
        log.info("Loaded GatewayFilterFactory [DemoFilter]");
    }

    @Override
    public List<String> shortcutFieldOrder() {
        return Collections.singletonList("enabled");
    }

    @Override
    public GatewayFilter apply(DemoGatewayFilterFactory.Config config) {
        return (exchange, chain) -> {
            if (!config.isEnabled()) {
                return chain.filter(exchange);
            }
            log.info("-----DemoGatewayFilterFactory start-----");
            ServerHttpRequest request = exchange.getRequest();
            log.info("RemoteAddress: [{}]", request.getRemoteAddress());
            log.info("Path: [{}]", request.getURI().getPath());
            log.info("Method: [{}]", request.getMethod());
            log.info("Body: [{}]", (String) exchange.getAttribute(CACHE_REQUEST_BODY_OBJECT_KEY));
            log.info("-----DemoGatewayFilterFactory end-----");
            return chain.filter(exchange);
        };
    }

    public static class Config {

        private boolean enabled;

        public Config() {}

        public boolean isEnabled() {
            return enabled;
        }

        public void setEnabled(boolean enabled) {
            this.enabled = enabled;
        }
    }
}

这一filter里,大家取得了新鮮的要求,而且打印出出了他的path,method,body等。

大家推送一个post要求,body就写一个“我是body”,运作网关ip,获得結果:

是否十分清楚一目了然!

你觉得这就结束了吗?这里有2个十分大的坑。

1. body为空时解决

上边贴出来的CustomReadBodyRoutePredicateFactory类实际上早已就是我修补过的编码,里边有一行.thenReturn(true)是必须再加上的。这才可以确保当body为空时,不容易给出出现异常。对于为什么一开始写的有什么问题,显而易见由于我懒惰了,立即copy在网上的编码了,啊哈哈哈。

2. body尺寸超出了buffer的较大 限定

这一状况是在企业新项目发布后才发觉的,大家的要求里body有时会较为大,可是网关ip会出现默认设置尺寸限定。因此发布后发觉了经常的出错:

org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer : 262144

Google后,找到解决方法,必须在配备中提升了以下配备

spring: 
  codec:
    max-in-memory-size: 5CB

把buffer尺寸改到5C。

你觉得这就又双叕告一段落,想的太多了,你能发觉很有可能沒有起效。

难题的根本原因在这儿:我们在spring配备了上边的主要参数,可是大家自定的拦截器是会复位ServerRequest,这一DefaultServerRequest中的HttpMessageReader会应用默认设置的262144

因此我们在这里必须从Spring中取下CodecConfigurer, 并将里边的Reader发送给serverRequest。

详尽的debug全过程能看这篇论文参考文献:

http://theclouds.io/tag/spring-gateway/

OK,寻找难题后,就可以改动大家的编码,在CustomReadBodyRoutePredicateFactory里,提升:

@Value("${spring.codec.max-in-memory-size}")
private DataSize maxInMemory;

@PostConstruct
private void overrideMsgReaders() {
  this.messageReaders = HandlerStrategies.builder().codecs((c) -> c.defaultCodecs().maxInMemorySize((int) maxInMemory.toBytes())).build().messageReaders();
}

那样每一次便会应用大家的5CB来做为较大 缓存文件限定了。

仍然提示一下,详细的编码能够可以看可运作的Github库房

讲到这儿,新手入门实战演练就差不多了,你的网关ip早已能够发布应用了,你需要做的便是再加上你需要的业务流程作用,例如日志,延签,统计分析等。

踩坑实战演练

获得手机客户端真正IP

许多情况下,大家的后端开发服务项目会去根据host取得客户的真正IP,可是根据表层反向代理Nginx的分享,很可能就必须从header里拿X-Forward-XXX相近那样的主要参数,才可以取得真正IP。

在大家添加了微服务网关后,这一繁杂的链接中又提升了一环。

这并不,假如你没做一切设定,因为你的网关ip和后端开发服务项目在同一个器皿中,你的后端开发服务项目很有可能便会取得localhost:8080(你的网关ip端口号)那样的IP。

此刻,你需要在yml里配备PreserveHostHeader,它是SpringCloud Gateway内置的完成:

filters:
  - PreserveHostHeader # 避免host被改动为localhost

字面意思,便是将Host的Header保存起來,透发送给后端开发服务项目。

filter里边的源代码贴上去给大伙儿:

public GatewayFilter apply(Object config) {
    return new GatewayFilter() {
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            exchange.getAttributes().put(ServerWebExchangeUtils.PRESERVE_HOST_HEADER_ATTRIBUTE, true);
            return chain.filter(exchange);
        }

        public String toString() {
            return GatewayToStringStyler.filterToStringCreator(PreserveHostHeaderGatewayFilterFactory.this).toString();
        }
    };
}

尾缀配对

企业的新项目中,老的后端开发库房api都以.json末尾(/api/xxxxxx.json),这就激发了一个要求,在我们对老插口开展了重新构建后,期待其打进大家的新服务项目,大家就需要将.json这一尾缀摘除。能够在filters里设定:

filters:
  - RewritePath=(?<segment>/?.*).json, $\{segment} # 重新构建插口抹除.json尾缀

那样就可以完成打进后端插口去除开.json后缀名。

汇总

文中领着阅读者一步步完成了一个微服务网关的构建,而且将很多很有可能掩藏的坑开展了处理。最终的制成品新项目在小编企业早已发布运作,而且提升了签字认证,日志纪录等业务流程,每日担负上百万等级的要求,是历经实战演练认证过的新项目。

最终再发一次新项目源代码库房:

https://github.com/qqxx6661/springcloud_gateway_demo

感谢大家的适用,假如文章内容对你具有了一丁点协助,来看我吧分享适用一下!

大家的意见反馈就是我不断升级的驱动力,感谢~

参照

https://cloud.tencent.com/developer/article/1449300

https://juejin.cn/post/6844903795973947400#heading-3

https://segmentfault.com/a/1190000016227780

https://cloud.spring.io/spring-cloud-static/Greenwich.SR1/multi/multi__reactor_netty_access_logs.html

https://www.cnblogs.com/savorboard/p/api-gateway.html

https://www.servicemesher.com/blog/service-mesh-and-api-gateway/

https://www.cnblogs.com/hyf-huangyongfei/p/12849406.html

https://www.codercto.com/a/52970.html

https://github.com/spring-cloud/spring-cloud-gateway/issues/1658

https://blog.csdn.net/zhangzhen02/article/details/109082792

关注我

我是一名奋斗在一线的互联网技术后端工程师技术工程师。

平常关键关心后端工程师,网络信息安全,边缘计算等方位,热烈欢迎沟通交流。

各网络平台都能找到我

  • 微信公众平台:后端开发技术性漫谈
  • Github:@qqxx6661
  • CSDN:@蛮三刀把刀
  • 知乎问答:@后端开发技术性漫谈
  • 开拓者:@蛮三刀把刀
  • 腾讯云服务 小区:@后端开发技术性漫谈
  • 博客园:@后端开发技术性漫谈
  • BiliBili:@蛮三刀把刀

原创文章内容具体内容

  • 后端工程师实战演练
  • 后端开发技术性招聘面试
  • 算法题解/算法设计/策略模式
  • 轶闻趣事

微信公众号:后端开发技术性漫谈

个人公众号:后端技术漫谈

假如文章内容对您有协助,请诸位老总关注点赞在看分享适用一下,你的适用一件事十分关键~

评论(0条)

刀客源码 游客评论