关于java:API网关才是大势所趋SpringCloud-Gateway保姆级入门教程

64次阅读

共计 8541 个字符,预计需要花费 22 分钟才能阅读完成。

什么是微服务网关

SpringCloud Gateway 是 Spring 全家桶中一个比拟新的我的项目,Spring 社区是这么介绍它的:

该我的项目借助 Spring WebFlux 的能力,打造了一个 API 网关。旨在提供一种简略而无效的办法来作为 API 服务的路由,并为它们提供各种加强性能,例如:安全性,监控和可伸缩性。

而在实在的业务畛域,咱们常常用 SpringCloud Gateway 来做微服务网关,如果你不了解微服务网关和传统网关的区别,能够浏览此篇文章 https://github.com/qqxx6661/s…

手把手造一个网关

引入 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 版本的网关:

此外,请检查一下你的依赖中是否含有 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 时,更加深刻的了解了这句话。在默认状况下,你不须要任何的配置,就可能运行起来最根本的网关。针对你之后特定的需要,再去追加配置。

而 SpringCloud Gateway 更弱小的一点就是内置了十分多的默认性能实现,你须要的大部分性能,比方在申请中增加一个 header,增加一个参数,都只须要在 yml 中引入相应的内置过滤器即可。

能够说,yml 是整个 SpringCloud Gateway 的灵魂。

一个网关最根本的性能,就是配置路由,在这方面,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,来注明咱们这个申请通过了此网关。这样做的益处是后续服务端也可能看到。
  • routes:路由是网关的重点,置信读者们看代码也能了解,我配置了两个路由,一个是 server_v1 的老服务,一个是 server_v2 的新服务。请留神,一个申请满足多个路由的谓词条件时,申请只会被首个胜利匹配的路由转发。因为咱们老服务的路由是 /xx,所以须要将老服务放在前面,优先匹配词缀 /v2 的新服务,不满足的再匹配到 /xx。

来看一下 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”,运行网关,失去后果:

是不是十分清晰明了!

你认为这就完结了吗?这里有两个十分大的坑。

1. body 为空时解决

下面贴出的 CustomReadBodyRoutePredicateFactory 类其实曾经是我修复过的代码,外面有一行 .thenReturn(true) 是须要加上的。这能力保障当 body 为空时,不会报出异样。至于为啥一开始写的有问题,显然因为我偷懒了,间接 copy 网上的代码了,哈哈哈哈哈。

2. body 大小超过了 buffer 的最大限度

这个状况是在公司我的项目上线后才发现的,咱们的申请里 body 有时候会比拟大,然而网关会有默认大小限度。所以上线后发现了频繁的报错:

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

谷歌后,找到了解决方案,须要在配置中减少了如下配置

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

把 buffer 大小改到了 5M。

你认为这就又双叕完结了,太天真了,你会发现可能没有失效。

问题的本源在这里:咱们在 spring 配置了下面的参数,然而咱们自定义的拦截器是会初始化 ServerRequest,这个 DefaultServerRequest 中的 HttpMessageReader 会应用默认的 262144

所以咱们在此处须要从 Spring 中取出 CodecConfigurer,并将外面的 Reader 传给 serverRequest。

具体的 debug 过程能够看这篇参考文献:

http://theclouds.io/tag/sprin…

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

这样每次就会应用咱们的 5MB 来作为最大缓存限度了。

仍然揭示一下,残缺的代码能够请看可运行的 Github 仓库

讲到这里,入门实战就差不多了,你的网关曾经能够上线应用了,你要做的就是加上你须要的业务性能,比方日志,延签,统计等。

踩坑实战

获取客户端实在 IP

很多时候,咱们的后端服务会去通过 host 拿到用户的实在 IP,然而通过外层反向代理 nginx 的转发,很可能就须要从 header 里拿 X -Forward-XXX 相似这样的参数,能力拿到实在 IP。

在咱们退出了微服务网关后,这个简单的链路中又减少了一环。

这不,如果你不做任何设置,因为你的网关和后端服务在同一个容器中,你的后端服务很有可能就会拿到 localhost:8080(你的网关端口)这样的 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 后缀。

总结

本文率领读者一步步实现了一个微服务网关的搭建,并且将许多可能暗藏的坑进行了解决。最初的成品我的项目在笔者公司曾经上线运行,并且减少了签名验证,日志记录等业务,每天承当百万级别的申请,是通过实战验证过的我的项目。

最初再发一次我的项目源码仓库:

@蛮三刀把刀

原创文章次要内容

  • 后端开发实战
  • 后端技术面试
  • 算法题解 / 数据结构 / 设计模式
  • 轶闻趣事

集体公众号:后端技术漫谈

如果文章对你有帮忙,请各位老板点赞在看转发反对一下,你的反对对我十分重要~

正文完
 0