什么是微服务网关
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: DEBUGspring: 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仓库:
@Componentpublic 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:
@Componentpublic 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;@PostConstructprivate 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后缀。
总结
本文率领读者一步步实现了一个微服务网关的搭建,并且将许多可能暗藏的坑进行了解决。最初的成品我的项目在笔者公司曾经上线运行,并且减少了签名验证,日志记录等业务,每天承当百万级别的申请,是通过实战验证过的我的项目。
最初再发一次我的项目源码仓库:
@蛮三刀把刀
原创文章次要内容
- 后端开发实战
- 后端技术面试
- 算法题解/数据结构/设计模式
- 轶闻趣事
集体公众号:后端技术漫谈
如果文章对你有帮忙,请各位老板点赞在看转发反对一下,你的反对对我十分重要~