1. 爲何是Spring Cloud Gatewayjava
一句話,Spring Cloud已經放棄Netflix Zuul了。如今Spring Cloud中引用的仍是Zuul 1.x版本,而這個版本是基於過濾器的,是阻塞IO,不支持長鏈接。Zuul 2.x版本跟1.x的架構大同樣,性能也有所提高。既然Spring Cloud已經再也不集成Zuul 2.x了,那麼是時候瞭解一下Spring Cloud Gateway了。react
能夠看到,最新的Spring Cloud中的Zuul仍是1.3.1版本git
並且,官網中也明確說了再也不維護Zuul了github
(PS:順便補充幾個名詞: 服務發現(Eureka),斷路器(Hystrix),智能路由(Zuul),客戶端負載均衡(Ribbon))web
2. API網關正則表達式
API網關是一個服務器,是系統的惟一入口。從面向對象設計的角度看,它與外觀模式相似。API網關封裝了系統內部架構,爲每一個客戶端提供一個定製的API。它可能還具備其它職責,如身份驗證、監控、負載均衡、緩存、請求分片與管理、靜態響應處理。API網關方式的核心要點是,全部的客戶端和消費端都經過統一的網關接入微服務,在網關層處理全部的非業務功能。一般,網關也是提供REST/HTTP的訪問API。redis
網關應當具有如下功能:算法
目前,比較流行的網關有:Nginx 、 Kong 、Orange等等,還有微服務網關Zuul 、Spring Cloud Gateway等等spring
對於 API Gateway,常見的選型有基於 Openresty 的 Kong、基於 Go 的 Tyk 和基於 Java 的 Zuul。這三個選型自己沒有什麼明顯的區別,主要仍是看技術棧是否能知足快速應用和二次開發。apache
以上說的這些功能,這些開源的網關組件都有,或者藉助Lua也能實現,好比:Nginx + Lua
那要Spring Cloud Gateway還有什麼用呢?
其實,我我的理解是這樣的:
因此,你看到的網關多是這樣的:
2.1. Netflix Zuul 1.x VS Netflix Zuul 2.x
3. Spring Cloud Gateway
3.1. 特性
3.2. 術語
Route : 路由是網關的基本組件。它由ID、目標URI、謂詞集合和過濾器集合定義。若是聚合謂詞爲true,則匹配路由
Predicate : This is a Java 8 Function Predicate
Filter : 是GatewayFilter的一個實例,在這裏,能夠在發送下游請求以前或以後修改請求和響應
3.3. 原理
(PS:看到這張圖是否是很熟悉,沒錯,很像SpringMVC的請求處理過程)
客戶端向Spring Cloud Gateway發出請求。若是Gateway Handler Mapping肯定請求與路由匹配,則將其發送給Gateway Web Handler。這個Handler運行經過特定於請求的過濾器鏈發送請求。過濾器能夠在發送代理請求以前或以後執行邏輯。執行全部的「pre」過濾邏輯,而後發出代理請求,最後執行「post」過濾邏輯。
3.4. Route Predicate Factories
3.4.1. After Route Predicate Factory
spring: cloud: gateway: routes: - id: after_route uri: https://example.org predicates: - After=2017-01-20T17:42:47.789-07:00[America/Denver]
這個路由匹配「美國丹佛時間2017-01-20 17:42」以後的任意請求
3.4.2. Header Route Predicate Factory
spring: cloud: gateway: routes: - id: header_route uri: https://example.org predicates: - Header=X-Request-Id, \d+
這個路由匹配「請求頭包含X-Request-Id而且其值匹配正則表達式\d+」的任意請求
3.4.3. Method Route Predicate Factory
spring: cloud: gateway: routes: - id: method_route uri: https://example.org predicates: - Method=GET
這個路由匹配任意GET請求
3.4.4. Path Route Predicate Factory
spring: cloud: gateway: routes: - id: host_route uri: https://example.org predicates: - Path=/foo/{segment},/bar/{segment}
這個路由匹配這樣路徑的請求,好比:/foo/1 或 /foo/bar 或 /bar/baz
3.4.5. Query Route Predicate Factory
這個Predicate有兩個參數:一個必須的參數名和一個可選的正則表達式
spring: cloud: gateway: routes: - id: query_route uri: https://example.org predicates: - Query=baz
這個路由匹配「查詢參數中包含baz」的請求
spring: cloud: gateway: routes: - id: query_route uri: https://example.org predicates: - Query=foo, ba.
這個路由匹配「查詢參數中包含foo,而且其參數值知足正則表達式ba.」的請求,好比:bar,baz
3.4.6. RemoteAddr Route Predicate Factory
這個路由接受一個IP(IPv4或IPv6)地址字符串。例如:192.168.0.1/16,其中192.168.0.1,16是子網掩碼
spring: cloud: gateway: routes: - id: remoteaddr_route uri: https://example.org predicates: - RemoteAddr=192.168.1.1/24
這裏路由匹配遠程地址是這樣的請求,例如:192.168.1.10
3.5. GatewayFilter Factories(網關過濾器)
路由過濾器容許以某種方式修改傳入的HTTP請求或傳出HTTP響應。路由過濾器的做用域是特定的路由。Spring Cloud Gateway包含許多內置的網關過濾器工廠。
3.5.1. AddRequestHeader GatewayFilter Factory
spring: cloud: gateway: routes: - id: add_request_header_route uri: https://example.org filters: - AddRequestHeader=X-Request-Foo, Bar
對於全部匹配的請求,將會給傳給下游的請求添加一個請求頭 X-Request-Foo:Bar
3.5.2. AddRequestParameter GatewayFilter Factory
spring: cloud: gateway: routes: - id: add_request_parameter_route uri: https://example.org filters: - AddRequestParameter=foo, bar
對於全部匹配的請求,將給傳給下游的請求添加一個查詢參數 foo=bar
3.5.3. AddResponseHeader GatewayFilter Factory
spring: cloud: gateway: routes: - id: add_response_header_route uri: https://example.org filters: - AddResponseHeader=X-Response-Foo, Bar
對於全部匹配的請求,添加一個響應頭 X-Response-Foo:Bar
3.5.4. Hystrix GatewayFilter Factory
Hystrix網關過濾器容許你將斷路器引入網關路由,保護你的服務免受級聯失敗的影響,並在下游發生故障時提供預備響應。
爲了啓用Hystrix網關過濾器,你須要引入 spring-cloud-starter-netflix-hystrix
Hystrix網關過濾器須要一個name參數,這個name是HystrixCommand的名字
spring: cloud: gateway: routes: - id: hystrix_route uri: https://example.org filters: - Hystrix=myCommandName
給這個過濾器包裝一個名字叫myCommandName的HystrixCommand
Hystrix網關過濾器也接受一個可選的參數fallbackUri,可是目前只支持forward:前綴的URL。也就是說,若是這個fallback被調用,請求將被重定向到匹配的這個URL。
spring: cloud: gateway: routes: - id: hystrix_route uri: lb://backing-service:8088 predicates: - Path=/consumingserviceendpoint filters: - name: Hystrix args: name: fallbackcmd fallbackUri: forward:/incaseoffailureusethis - RewritePath=/consumingserviceendpoint, /backingserviceendpoint
當fallback被調用的時候,請求將被重定向到/incaseoffailureusethis
spring: cloud: gateway: routes: - id: ingredients uri: lb://ingredients predicates: - Path=//ingredients/** filters: - name: Hystrix args: name: fetchIngredients fallbackUri: forward:/fallback - id: ingredients-fallback uri: http://localhost:9994 predicates: - Path=/fallback
在這個例子中,專門定義了一個端點來處理/fallback請求,它在localhost:9994上。也就是說,當fallback被調用的時候將重定向到http://localhost:9994/fallback
3.5.5. PrefixPath GatewayFilter Factory
spring: cloud: gateway: routes: - id: prefixpath_route uri: https://example.org filters: - PrefixPath=/mypath
全部匹配的請求都將加上前綴/mypath。例如,若是請求是/hello,那麼通過這個過濾器後,發出去的請求變成/mypath/hello
3.5.6. RequestRateLimiter GatewayFilter Factory
RequestRateLimiter網關過濾器使用一個RateLimiter實現來決定是否當前請求能夠繼續往下走。若是不能,默認將返回HTTP 429 - Too Many Requests
這個過濾器接受一個可選的參數keyResolver,這個參數是一個特定的rate limiter
keyResolver是實現了KeyResolver接口的一個Bean。
在配置的時候,使用SpEL按名稱引用Bean。#{@myKeyResolver}是一個SpEL表達式,表示引用名字叫myKeyResolver的Bean。
KeyResolver.java
public interface KeyResolver { Flux<RouteDefinition> getRouteDefinitions(); }
KeyResolver默認的實現是PrincipalNameKeyResolver,它從ServerWebExchange中檢索Principal,並調用Principal.getName()方法。
默認狀況下,若是KeyResolver沒有找到一個key,那麼請求將會被denied(譯:否定,拒絕)。這種行爲能夠經過spring.cloud.gateway.filter.request-rate-limiter.deny-empty-key (true or false) 和 spring.cloud.gateway.filter.request-rate-limiter.empty-key-status-code 屬性來進行調整.
Redis RateLimiter
須要引用 spring-boot-starter-data-redis-reactive
這個邏輯使用令牌桶算法
一個穩定的速率是經過將replenishRate 和 burstCapacity設爲相同的值來實現的。也能夠將burstCapacity設得比replenishRate大,以應對臨時爆發的流量。在這種狀況下,須要容許速率限制器在突發事件之間間隔一段時間,由於連續兩次突發事件將致使丟棄請求(HTTP 429 - Too Many Requests)
application.yml
spring: cloud: gateway: routes: - id: requestratelimiter_route uri: https://example.org filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 10 redis-rate-limiter.burstCapacity: 20
Config.java
1 @Bean 2 KeyResolver userKeyResolver() { 3 return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user")); 4 }
這裏定義了每一個用戶的請求速率限制爲10。容許使用20個請求,可是在接下來的一秒中,只有10個請求可用。
這個例子中只是簡單地從請求參數中獲取"user",在實際生產環境中不建議這麼作。
咱們也能夠經過實現RateLimiter接口來自定義,這個時候,在配置中咱們就須要引用這個Bean,例如:#{@myRateLimiter}
spring: cloud: gateway: routes: - id: requestratelimiter_route uri: https://example.org filters: - name: RequestRateLimiter args: rate-limiter: "#{@myRateLimiter}" key-resolver: "#{@userKeyResolver}"
3.5.7. Default Filters
若是你想要添加一個過濾器而且把它應用於全部路由的話,你能夠用spring.cloud.gateway.default-filters。這個屬性接受一個過濾器列表。
spring: cloud: gateway: default-filters: - AddResponseHeader=X-Response-Default-Foo, Default-Bar - PrefixPath=/httpbin
3.6. Global Filters(全局過濾器)
GlobalFilter接口的方法簽名和GatewayFilter相同。這些是有條件地應用於全部路由的特殊過濾器。
3.6.1. GlobalFilter和GatewayFilter的順序
當一個請求過來的時候,將會添加全部的GatewayFilter實例和全部特定的GatewayFilter實例到過濾器鏈上。過濾器鏈按照org.springframework.core.Ordered接口對該鏈路上的過濾器進行排序。你能夠經過實現接口中的getOrder()方法或者使用@Order註解。
Spring Cloud Gateway將過濾器執行邏輯分爲「pre」和「post」階段。優先級最高的過濾器將會是「pre」階段中的第一個過濾器,同時它也將是「post」階段中的最後一個過濾器。
ExampleConfiguration.java
1 @Bean 2 @Order(-1) 3 public GlobalFilter a() { 4 return (exchange, chain) -> { 5 log.info("first pre filter"); 6 return chain.filter(exchange).then(Mono.fromRunnable(() -> { 7 log.info("third post filter"); 8 })); 9 }; 10 } 11 12 @Bean 13 @Order(0) 14 public GlobalFilter b() { 15 return (exchange, chain) -> { 16 log.info("second pre filter"); 17 return chain.filter(exchange).then(Mono.fromRunnable(() -> { 18 log.info("second post filter"); 19 })); 20 }; 21 } 22 23 @Bean 24 @Order(1) 25 public GlobalFilter c() { 26 return (exchange, chain) -> { 27 log.info("third pre filter"); 28 return chain.filter(exchange).then(Mono.fromRunnable(() -> { 29 log.info("first post filter"); 30 })); 31 }; 32 }
3.6.2. LoadBalancerClient Filter
LoadBalancerClientFilter查找exchange屬性中查找ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR一個URI。若是url符合lb schema(例如:lb://myservice),那麼它將使用Spring Cloud LoadBalancerClient 來解析這個名字到一個實際的主機和端口,並替換URI中相同的屬性。原始url中未被修改的部分被附加到ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR屬性列表中。
application.yml
spring: cloud: gateway: routes: - id: myRoute uri: lb://service predicates: - Path=/service/**
默認狀況下,當一個服務實例在LoadBalancer中沒有找到時,將返回503。你能夠經過配置spring.cloud.gateway.loadbalancer.use404=true來讓它返回404。
3.7. 配置
RouteDefinitionLocator.java
public interface RouteDefinitionLocator { Flux<RouteDefinition> getRouteDefinitions(); }
默認狀況下,PropertiesRouteDefinitionLocator經過@ConfigurationProperties機制加載屬性
下面兩段配置是等價的
spring: cloud: gateway: routes: - id: setstatus_route uri: https://example.org filters: - name: SetStatus args: status: 401 - id: setstatusshortcut_route uri: https://example.org filters: - SetStatus=401
下面用Java配置
GatewaySampleApplication.java
1 // static imports from GatewayFilters and RoutePredicates 2 @Bean 3 public RouteLocator customRouteLocator(RouteLocatorBuilder builder, ThrottleGatewayFilterFactory throttle) { 4 return builder.routes() 5 .route(r -> r.host("**.abc.org").and().path("/image/png") 6 .filters(f -> 7 f.addResponseHeader("X-TestHeader", "foobar")) 8 .uri("http://httpbin.org:80") 9 ) 10 .route(r -> r.path("/image/webp") 11 .filters(f -> 12 f.addResponseHeader("X-AnotherHeader", "baz")) 13 .uri("http://httpbin.org:80") 14 ) 15 .route(r -> r.order(-1) 16 .host("**.throttle.org").and().path("/get") 17 .filters(f -> f.filter(throttle.apply(1, 18 1, 19 10, 20 TimeUnit.SECONDS))) 21 .uri("http://httpbin.org:80") 22 ) 23 .build(); 24 }
這種風格容許自定義更多的謂詞斷言,默認是邏輯與(and)。你也能夠用and() , or() , negate()
再來一個例子
1 @SpringBootApplication 2 public class DemogatewayApplication { 3 @Bean 4 public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { 5 return builder.routes() 6 .route("path_route", r -> r.path("/get") 7 .uri("http://httpbin.org")) 8 .route("host_route", r -> r.host("*.myhost.org") 9 .uri("http://httpbin.org")) 10 .route("hystrix_route", r -> r.host("*.hystrix.org") 11 .filters(f -> f.hystrix(c -> c.setName("slowcmd"))) 12 .uri("http://httpbin.org")) 13 .route("hystrix_fallback_route", r -> r.host("*.hystrixfallback.org") 14 .filters(f -> f.hystrix(c -> c.setName("slowcmd").setFallbackUri("forward:/hystrixfallback"))) 15 .uri("http://httpbin.org")) 16 .route("limit_route", r -> r 17 .host("*.limited.org").and().path("/anything/**") 18 .filters(f -> f.requestRateLimiter(c -> c.setRateLimiter(redisRateLimiter()))) 19 .uri("http://httpbin.org")) 20 .build(); 21 } 22 }
3.8. CORS配置
spring: cloud: gateway: globalcors: corsConfigurations: '[/**]': allowedOrigins: "https://docs.spring.io" allowedMethods: - GET
上面的例子中,全部原始爲docs.spring.io的GET請求均被容許跨域請求。
4. 示例
本例中又4個項目,以下圖:
4.1. cjs-eureka-server
pom.xml
1 <?xml version="1.0" encoding="UTF-8"?> 2 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 3 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4 <modelVersion>4.0.0</modelVersion> 5 <parent> 6 <groupId>org.springframework.boot</groupId> 7 <artifactId>spring-boot-starter-parent</artifactId> 8 <version>2.1.6.RELEASE</version> 9 <relativePath/> <!-- lookup parent from repository --> 10 </parent> 11 <groupId>com.cjs.example</groupId> 12 <artifactId>cjs-eureka-server</artifactId> 13 <version>0.0.1-SNAPSHOT</version> 14 <name>cjs-eureka-server</name> 15 16 <properties> 17 <java.version>1.8</java.version> 18 <spring-cloud.version>Greenwich.SR1</spring-cloud.version> 19 </properties> 20 21 <dependencies> 22 <dependency> 23 <groupId>org.springframework.cloud</groupId> 24 <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> 25 </dependency> 26 <dependency> 27 <groupId>ch.qos.logback</groupId> 28 <artifactId>logback-classic</artifactId> 29 <version>1.2.3</version> 30 </dependency> 31 </dependencies> 32 33 <dependencyManagement> 34 <dependencies> 35 <dependency> 36 <groupId>org.springframework.cloud</groupId> 37 <artifactId>spring-cloud-dependencies</artifactId> 38 <version>${spring-cloud.version}</version> 39 <type>pom</type> 40 <scope>import</scope> 41 </dependency> 42 </dependencies> 43 </dependencyManagement> 44 45 <build> 46 <plugins> 47 <plugin> 48 <groupId>org.springframework.boot</groupId> 49 <artifactId>spring-boot-maven-plugin</artifactId> 50 </plugin> 51 </plugins> 52 </build> 53 54 </project>
application.yml
1 server: 2 port: 8761 3 4 spring: 5 application: 6 name: cjs-eureka-server 7 8 eureka: 9 client: 10 service-url: 11 defaultZone: http://10.0.29.92:8761/eureka/,http://10.0.29.232:8761/eureka/ 12 13 logging: 14 file: ${spring.application.name}.log
Application.java
1 package com.cjs.example; 2 3 import org.springframework.boot.SpringApplication; 4 import org.springframework.boot.autoconfigure.SpringBootApplication; 5 import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; 6 7 /** 8 * @author chengjiansheng 9 * @date 2019-06-26 10 */ 11 @EnableEurekaServer 12 @SpringBootApplication 13 public class CjsEurekaServerApplication { 14 15 public static void main(String[] args) { 16 SpringApplication.run(CjsEurekaServerApplication.class, args); 17 } 18 19 }
4.2. cjs-gateway-server
pom.xml
1 <?xml version="1.0" encoding="UTF-8"?> 2 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 3 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4 <modelVersion>4.0.0</modelVersion> 5 <parent> 6 <groupId>org.springframework.boot</groupId> 7 <artifactId>spring-boot-starter-parent</artifactId> 8 <version>2.1.6.RELEASE</version> 9 <relativePath/> <!-- lookup parent from repository --> 10 </parent> 11 <groupId>com.cjs.example</groupId> 12 <artifactId>cjs-gateway-server</artifactId> 13 <version>0.0.1-SNAPSHOT</version> 14 <name>cjs-gateway-server</name> 15 16 <properties> 17 <java.version>1.8</java.version> 18 <spring-cloud.version>Greenwich.SR1</spring-cloud.version> 19 </properties> 20 21 <dependencies> 22 <dependency> 23 <groupId>org.springframework.cloud</groupId> 24 <artifactId>spring-cloud-starter-gateway</artifactId> 25 </dependency> 26 27 <dependency> 28 <groupId>org.springframework.boot</groupId> 29 <artifactId>spring-boot-starter-data-redis-reactive</artifactId> 30 </dependency> 31 <dependency> 32 <groupId>ch.qos.logback</groupId> 33 <artifactId>logback-classic</artifactId> 34 <version>1.2.3</version> 35 </dependency> 36 </dependencies> 37 38 <dependencyManagement> 39 <dependencies> 40 <dependency> 41 <groupId>org.springframework.cloud</groupId> 42 <artifactId>spring-cloud-dependencies</artifactId> 43 <version>${spring-cloud.version}</version> 44 <type>pom</type> 45 <scope>import</scope> 46 </dependency> 47 </dependencies> 48 </dependencyManagement> 49 50 <build> 51 <plugins> 52 <plugin> 53 <groupId>org.springframework.boot</groupId> 54 <artifactId>spring-boot-maven-plugin</artifactId> 55 </plugin> 56 </plugins> 57 </build> 58 59 </project>
application.yml
1 server: 2 port: 8080 3 servlet: 4 context-path: / 5 spring: 6 application: 7 name: cjs-gateway-server 8 redis: 9 host: 10.0.29.187 10 password: 123456 11 port: 6379 12 cloud: 13 gateway: 14 routes: 15 - id: header_route 16 uri: http://10.0.29.187:8080/ 17 predicates: 18 - Header=X-Request-Id, \d+ 19 # - id: path_route 20 # uri: http://10.0.29.187:8080/ 21 # predicates: 22 # - Path=/foo/{segment},/bar/{segment} 23 - id: query_route 24 uri: http://10.0.29.187:8080/ 25 predicates: 26 - Query=baz 27 # default-filters: 28 # - AddResponseHeader=X-Response-Foo, Bar 29 # - AddRequestParameter=hello, world 30 31 logging: 32 file: ${spring.application.name}.log
Application.java
1 package com.cjs.example.gateway; 2 3 import org.springframework.boot.SpringApplication; 4 import org.springframework.boot.autoconfigure.SpringBootApplication; 5 import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver; 6 import org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter; 7 import org.springframework.cloud.gateway.route.RouteLocator; 8 import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; 9 import org.springframework.context.annotation.Bean; 10 import org.springframework.web.bind.annotation.RestController; 11 import reactor.core.publisher.Mono; 12 13 /** 14 * @author chengjiansheng 15 */ 16 @RestController 17 @SpringBootApplication 18 public class CjsGatewayServerApplication { 19 20 public static void main(String[] args) { 21 SpringApplication.run(CjsGatewayServerApplication.class, args); 22 } 23 24 @Bean 25 public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { 26 return builder.routes() 27 .route("path_route", r -> r.path("/price/**") 28 .filters(f -> f.addRequestHeader("hello", "world") 29 .addRequestParameter("name", "zhangsan") 30 .requestRateLimiter(c -> c.setRateLimiter(redisRateLimiter()))) 31 .uri("http://10.0.29.232:8082/price")) 32 .route("path_route", r -> r.path("/commodity/**").uri("http://10.0.29.92:8081/commodity")) 33 .build(); 34 } 35 36 @Bean 37 public RedisRateLimiter redisRateLimiter() { 38 return new RedisRateLimiter(2, 4); 39 } 40 41 @Bean 42 KeyResolver userKeyResolver() { 43 // return exchange -> Mono.just(exchange.getRequest().getHeaders().getFirst("userId")); 44 return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("userId")); 45 } 46 47 }
其他代碼就不一一貼出來了,都在git上
https://github.com/chengjiansheng/cjs-springcloud-example
截兩張圖吧
下面看效果
效果一:正常路由
效果二:限流
1 ab -n 20 -c 10 http://10.0.29.187:8080/price/index/test01?userId=123
觀察控制檯會看到
1 2019-07-03 18:21:23.946 DEBUG 34433 --- [ioEventLoop-4-1] o.s.c.g.f.ratelimit.RedisRateLimiter : response: Response{allowed=false, headers={X-RateLimit-Remaining=0, X-RateLimit-Burst-Capacity=4, X-RateLimit-Replenish-Rate=2}, tokensRemaining=-1} 2 2019-07-03 18:21:23.946 DEBUG 34433 --- [ioEventLoop-4-1] o.s.w.s.adapter.HttpWebHandlerAdapter : [53089629] Completed 429 TOO_MANY_REQUESTS