SpringCloud Gateway是Spring全家桶中一个比较新的项目,Spring社区是这么介绍它的:html
该项目借助Spring WebFlux的能力,打造了一个API网关。旨在提供一种简单而有效的方法来做为API服务的路由,并为它们提供各类加强功能,例如:安全性,监控和可伸缩性。
而在真实的业务领域,咱们常常用SpringCloud Gateway来作微服务网关,若是你不理解微服务网关和传统网关的区别,能够阅读此篇文章 Service Mesh和API Gateway关系深度探讨 来了解二者的定位区别。java
以我粗浅的理解,传统的API网关,每每是独立于各个后端服务,请求先打到独立的网关层,再打到服务集群。而微服务网关,将流量从南北走向改成东西走向(见下图),微服务网关和后端服务是在同一个容器中的,因此也有个别名,叫作Gateway Sidecar。react
为啥叫Sidecar,这个词应该怎么理解呢,吃鸡里的三蹦子见过没:nginx
摩托车是你的后端服务,而旁边挂着的额外座椅就是微服务网关,他是依附于后端服务的(通常是指两个进程在同一个容器中),是否是生动形象了一些。git
因为本人才疏学浅,对于微服务相关概念理解上不免会有误差。就不在此详细讲述原理性的文字了。github
本文只探讨SpringCloud Gateway的入门搭建和实战踩坑。 若是小伙伴们对原理感兴趣,能够等后续原理分析文章。web
注:本文网关项目在笔者公司已经上线运行,天天承担百万级别的请求,是通过实战验证的项目。面试
手把手造一个网关算法
踩坑实战spring
完整项目源代码已经收录到个人Github:
https://github.com/qqxx6661/s...
我使用了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,获得结果如图:
SpringBoot的核心概念是约定优先于配置,在之前初学Spring时,一直不理解这句话的意思,在使用SpringCloud Gateway时,更加深刻的理解了这句话。在默认状况下,你不须要任何的配置,就可以运行起来最基本的网关。针对你以后特定的需求,再去追加配置。
而SpringCloud Gateway更强大的一点就是内置了很是多的默认功能实现,你须要的大部分功能,好比在请求中添加一个header,添加一个参数,都只须要在yml中引入相应的内置过滤器便可。
能够说,yml是整个SpringCloud Gateway的灵魂。
一个网关最基本的功能,就是配置路由,在这方面,SpringCloud Gateway支持很是多方式。好比:
这些在官网教程中,都有详细的介绍,就算你百度下,也会有不少民间翻译的入门教程,我就再也不赘述了,我只用一个请求路径作一个简单的例子。
在公司的项目中,因为有新老两套后台服务,咱们使用不一样的uri路径进行区分。
那么能够直接在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/**
上面的代码解释以下:
来看一下http://localhost:8080/api/xxx...
来看一下http://localhost:8080/api/v2/...
能够看到两个请求被正确的路由了。因为咱们真正并无开启后端服务,因此最后一句error请忽略。
在公司实际的项目中,我在搭建好网关后,遇到了一个接口转义问题,相信不少读者可能也会碰到,因此在这里咱们最好是防患于未然,优先处理下。
问题是这样的,不少老项目在url上并无进行转义,致使会出现以下接口请求,http://xxxxxxxxx/api/b3d56a6f...
这样请求过来,网关会报错:
java.lang.IllegalArgumentException: Invalid character '=' for QUERY_PARAM in "http://pic1.ajkimg.com/display/anjuke/b3d56a6fa19975ba520189f3f55de7f6/140x140.jpg?t=1"
在不修改服务代码逻辑的前提下,网关其实已经能够解决这件事情,解决办法就是升级到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,请升级。
在网关的使用中,有时候会须要拿到请求body里面的数据,好比验证签名,body可能须要参与签名校验。
可是SpringCloud Gateway因为底层采用了webflux,其请求是流式响应的,即 Reactor 编程,要读取 Request Body 中的请求参数就没那么容易了。
网上谷歌了好久,不少解决方案要么是完全过期,要么是版本不兼容,好在最后参考了这篇文章,终于有了思路:
https://www.jianshu.com/p/db3...
首先咱们须要将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”,运行网关,获得结果:
是否是很是清晰明了!
你觉得这就结束了吗?这里有两个很是大的坑。
上面贴出的CustomReadBodyRoutePredicateFactory类其实已是我修复过的代码,里面有一行.thenReturn(true)
是须要加上的。这才能保证当body为空时,不会报出异常。至于为啥一开始写的有问题,显然由于我偷懒了,直接copy网上的代码了,哈哈哈哈哈。
这个状况是在公司项目上线后才发现的,咱们的请求里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仓库
讲到这里,入门实战就差很少了,你的网关已经能够上线使用了,你要作的就是加上你须要的业务功能,好比日志,延签,统计等。
不少时候,咱们的后端服务会去经过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后缀。
本文带领读者一步步完成了一个微服务网关的搭建,而且将许多可能隐藏的坑进行了解决。最后的成品项目在笔者公司已经上线运行,而且增长了签名验证,日志记录等业务,天天承担百万级别的请求,是通过实战验证过的项目。
最后再发一次项目源码仓库:
https://github.com/qqxx6661/s...
感谢你们的支持,若是文章对你起到了一丁点帮助,请点赞转发支持一下!
大家的反馈是我持续更新的动力,谢谢~
https://cloud.tencent.com/dev...
https://juejin.cn/post/684490...
https://segmentfault.com/a/11...
https://cloud.spring.io/sprin...
https://www.cnblogs.com/savor...
https://www.servicemesher.com...
https://www.cnblogs.com/hyf-h...
https://www.codercto.com/a/52...
https://github.com/spring-clo...
https://blog.csdn.net/zhangzh...
我是一名奋斗在一线的互联网后端开发工程师。
平时主要关注后端开发,数据安全,边缘计算等方向,欢迎交流。
各大平台都能找到我
原创文章主要内容
我的公众号:后端技术漫谈
若是文章对你有帮助,请各位老板点赞在看转发支持一下,你的支持对我很是重要~