title: “Spring Cloud学习笔记8-spring cloud gateway(第二代网关)”
url: “https://wsk1103.github.io/”
tags:
官网:http://spring.io/projects/spring-cloud
总纲:https://cloud.spring.io/spring-cloud-static/Finchley.SR2/single/spring-cloud.html
JAVA: 1.8 +
MAVEN: 3.5.0 +
Spring Boot:2.0.7.RELEASE
Spring Cloud:Finchley
说明:
This project provides an API Gateway built on top of the Spring Ecosystem, including: Spring 5, Spring Boot 2 and Project Reactor. Spring Cloud Gateway aims to provide a simple, yet effective way to route to APIs and provide cross cutting concerns to them such as: security, monitoring/metrics, and resiliency.
Spring Cloud Gateway是Spring官方基于Spring 5.0,Spring Boot 2.0和Project Reactor等技术开发的网关,Spring云网关旨在提供一种简单而有效的路由API的方法。Spring Cloud Gateway作为Spring Cloud生态系中的网关,目标是替代Netflix zuul,其不仅提供统一的路由方式,并且基于Filter链的方式提供了网关基本的功能,例如:安全,监控/埋点,和限流等。
Glossary 名词解析
how it work
Clients make requests to Spring Cloud Gateway. If the Gateway Handler Mapping determines that a request matches a Route, it is sent to the Gateway Web Handler. This handler runs sends the request through a filter chain that is specific to the request. The reason the filters are divided by the dotted line, is that filters may execute logic before the proxy request is sent or after. All “pre” filter logic is executed, then the proxy request is made. After the proxy request is made, the “post” filter logic is executed.
客户端向Spring Cloud Gateway发出请求。如果网关处理程序映射确定请求与路由匹配,则将其发送到网关Web处理程序。此处理程序运行通过特定于请求的过滤器链发送请求。过滤器被虚线划分的原因是过滤器可以在发送代理请求之前或之后执行逻辑。执行所有“pre”过滤器逻辑,然后进行代理请求。在发出代理请求之后,执行“post”过滤器逻辑。
VS Netflix Zuul
Zuul 基于 Servlet 2.5(使用 3.x),使用阻塞 API,它不支持任何长连接,如 WebSockets。而 Spring Cloud Gateway 建立在 Spring Framework 5,Project Reactor 和 Spring Boot 2 之上,使用非阻塞 API,支持 WebSockets,并且由于它与 Spring 紧密集成,所以将会是一个更好的开发体验。
要说缺点,其实 Spring Cloud Gateway 还是有的。目前它的文档还不是很完善,官方文档有许多还处于 TODO 状态,网络上关于它的文章也还比较少。如果你决定要使用它,那么你必须得有耐心通过自己阅读源码来解决可能遇到的问题。(2018.5.7)
引用:Spring Cloud(十三):Spring Cloud Gateway(路由)
本项目地址:https://github.com/wsk1103/my-spring-cloud
搭建方法同学习笔记1中新建module
主要增加依赖为
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.boot
spring-boot-starter-actuator
org.springframework.cloud
spring-cloud-starter-gateway
具体pom
4.0.0
com.wsk
gateway
0.0.1-SNAPSHOT
jar
service-gateway
Demo project for Spring Boot
my-spring-cloud
MySpringCloud
1.0-SNAPSHOT
1.8
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-test
test
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.boot
spring-boot-starter-actuator
org.springframework.cloud
spring-cloud-starter-gateway
org.springframework.boot
spring-boot-maven-plugin
server:
port: 8766
spring:
application:
name: service-gateway
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true #在eureka中,服务是以大写的形式注册的,可以转化成小写
routes:
- id: service-client #服务唯一ID标识
uri: lb://service-client # 注册中心的服务id
predicates:
- Path=/client/** #请求转发
filters:
- StripPrefix=1 #切割请求,去除/client/
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
新增注解 @EnableEurekaClient
package com.wsk.gateway;
import com.wsk.gateway.resolver.RemoteAddrKeyResolver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
@EnableEurekaClient
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
访问:http://localhost:8766/client/hi?name=sky
此时可以看到页面跳转到服务 service-client 的相应接口。
package com.wsk.gateway.filter;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
/**
* @author WuShukai
* @version V1.0
* @description 全局过滤器,每次的请求都需要带上token
* @date 2018/12/12 16:55
*/
@Slf4j
@Data
public class TokenFilter implements GlobalFilter, Ordered {
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String uri = exchange.getRequest().getPath().pathWithinApplication().value();
log.info("访问的url为:{}", uri);
String token = exchange.getRequest().getQueryParams().getFirst("token");
if (token == null || token.isEmpty()) {
exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -100;
}
}
将全局过滤器初始化
package com.wsk.gateway.config;
import com.wsk.gateway.filter.TokenFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
/**
* @author WuShukai
* @version V1.0
* @description
* @date 2018/12/12 16:56
*/
@Configuration
public class MyConfig {
@Bean
public TokenFilter tokenFilter() {
return new TokenFilter();
}
}
访问:http://localhost:8766/client/hi?name=sky ,此时,由于没有带上token,所以后台会直接报错 BAD_REQUEST (该值可以在token过滤器里面配置)。
修改地址:http://localhost:8766/client/hi?name=sky&token=1103,重新访问 ->
用于排除不需要带token的url
# 过滤不需要token的uri,多个以,分开。
gateway.filter.uri=/login,/logout
package com.wsk.gateway.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import java.util.Arrays;
import java.util.List;
/**
* @author WuShukai
* @version V1.0
* @description 读取自定义的properties
* @date 2018/12/14 11:17
*/
@Configuration
//配置前缀
@ConfigurationProperties(prefix = "gateway.filter")
//配置文件名
@PropertySource("classpath:/gateway-filter-uri.properties")
@Data
public class PropertiesConfig {
//前缀名(gateway.filter) + 后缀名(uri)
private String uri;
//将定义的uri转化成list
public List handleUri() {
return Arrays.asList(uri.split(","));
}
}
主要为新增代码片
for (String url : all) {
//过滤不需要拥有token的连接
if (uri.startsWith(url)) {
log.info("不需要拥有token的uri:{}", uri);
return chain.filter(exchange);
}
}
新的类
package com.wsk.gateway.filter;
import com.wsk.gateway.config.PropertiesConfig;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
/**
* @author WuShukai
* @version V1.0
* @description 全局过滤器,每次的请求都需要带上token
* @date 2018/12/12 16:55
*/
@Slf4j
@Data
@EnableConfigurationProperties(PropertiesConfig.class)
public class TokenFilter implements GlobalFilter, Ordered {
@Autowired
private PropertiesConfig propertiesConfig;
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
List all = propertiesConfig.handleUri();
String uri = exchange.getRequest().getPath().pathWithinApplication().value();
log.info("访问的url为:{}", uri);
for (String url : all) {
//过滤不需要拥有token的连接
if (uri.startsWith(url)) {
log.info("不需要拥有token的uri:{}", uri);
return chain.filter(exchange);
}
}
String token = exchange.getRequest().getQueryParams().getFirst("token");
if (token == null || token.isEmpty()) {
exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -100;
}
}
其中SkyGatewayFilterFactory.Config为静态内部类,具体可以参考 HystrixGatewayFilterFactory (自带的全局熔断器)
package com.wsk.gateway.filter;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import reactor.core.publisher.Mono;
import java.util.Collections;
import java.util.List;
/**
* @author WuShukai
* @version V1.0
* @description
* @date 2018/12/12 17:01
*/
@Slf4j
public class SkyGatewayFilterFactory extends AbstractGatewayFilterFactory {
private static final String TIME_BEGIN = "TimeBegin";
private static final String KEY = "withParams";
@Override
public List shortcutFieldOrder() {
return Collections.singletonList(KEY);
}
public SkyGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
exchange.getAttributes().put(TIME_BEGIN, System.currentTimeMillis());
return chain.filter(exchange).then(
Mono.fromRunnable(() -> {
Long startTime = exchange.getAttribute(TIME_BEGIN);
if (startTime != null) {
StringBuilder sb = new StringBuilder(exchange.getRequest().getURI().getRawPath())
.append(": ")
.append(System.currentTimeMillis() - startTime)
.append("ms");
if (config.isWithParams()) {
sb.append(" params:").append(exchange.getRequest().getQueryParams());
}
log.info(sb.toString());
}
})
);
};
}
@Data
static class Config {
private boolean withParams;
}
}
主要在filters 里面增加 - Sky=true ,其中Sky 为过滤器的前缀,后缀为GatewayFilterFactory ,gateway是根据这个前缀去加载相应的 GatewayFilterFactory,
server:
port: 8766
spring:
application:
name: service-gateway
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true #在eureka中,服务是以大写的形式注册的,可以转化成小写
routes:
- id: service-client #服务唯一ID标识
uri: lb://service-client # 注册中心的服务id
predicates:
- Path=/client/** #请求转发
filters:
- Sky=true
- StripPrefix=1 #切割请求,去除/client/
初始化局部过滤器。
@Bean
public SkyGatewayFilterFactory skyGatewayFilterFactory() {
return new SkyGatewayFilterFactory();
}
访问:http://localhost:8766/client/hi?name=sky&token=1103
可以在控制台看到
AbstractNameValueGatewayFilterFactory 是spring cloud gateway中定义的一个抽象类,其中的静态类config有name,value这2个属性,本次使用这2个属性来判断是非开启该过滤器。
package com.wsk.gateway.filter;
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Bucket4j;
import io.github.bucket4j.Refill;
import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractNameValueGatewayFilterFactory;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author WuShukai
* @version V1.0
* @description 基于令牌桶的限流过滤器,必须继承AbstractGatewayFilterFactory或者实现GatewayFilterFactory
* @date 2018/12/12 17:12
*/
@Builder
@Data
@ToString
@EqualsAndHashCode(callSuper = false)
@AllArgsConstructor
@NoArgsConstructor
@Slf4j
public class SkyRateLimitByIpGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory implements GatewayFilter, Ordered {
/**
* 桶的最大容量,即能装载 Token 的最大数量
*/
private int capacity;
/**
* 每次 Token 补充量
*/
private int refillTokens;
/**
* 补充 Token 的时间间隔
*/
private Duration refillDuration;
private static final Map CACHE = new ConcurrentHashMap<>();
private Bucket createNewBucket() {
Refill refill = Refill.greedy(refillTokens, refillDuration);
Bandwidth limit = Bandwidth.classic(capacity, refill);
return Bucket4j.builder().addLimit(limit).build();
}
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String ip = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
Bucket bucket = CACHE.computeIfAbsent(ip, k -> createNewBucket());
log.info("IP: " + ip + ", TokenBucket Available Tokens: " + bucket.getAvailableTokens());
if (bucket.tryConsume(1)) {
return chain.filter(exchange);
} else {
//请求太多,服务器之间返回 TOO_MANY_REQUESTS 429
exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
return exchange.getResponse().setComplete();
}
}
@Override
public int getOrder() {
return -1000;
}
@Override
public GatewayFilter apply(NameValueConfig config) {
//开启限流
if ("open".equals(config.getName())) {
return this;
}
return (exchange, chain) -> chain.filter(exchange);
}
}
新增bean SkyRateLimitByIpGatewayFilterFactory
@Bean
public SkyRateLimitByIpGatewayFilterFactory skyRateLimitByIpGatewayFilterFactory() {
return new SkyRateLimitByIpGatewayFilterFactory(10, 1, Duration.ofSeconds(1));
}
参数分析:
/**
* 桶的最大容量,即能装载 Token 的最大数量-----》10 个
*/
private int capacity;
/**
* 每次 Token 补充量 -------》 1个
*/
private int refillTokens;
/**
* 补充 Token 的时间间隔 -------》 1秒
*/
private Duration refillDuration;
主要为新增了过滤器SkyRateLimitByIp配置 和增加Redis配置(因为该限流需要使用到Redis)。
spring:
application:
name: service-gateway
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true #在eureka中,服务是以大写的形式注册的,可以转化成小写
routes:
- id: service-client #服务唯一ID标识
uri: lb://service-client # 注册中心的服务id
predicates:
- Path=/client/** #请求转发
filters:
- StripPrefix=1 #切割请求,去除/client/
- name: SkyRateLimitByIp
args:
name: open
value: close
- name: Retry #重试机制
args:
retries: 3
statuses: BAD_GATEWAY
# gateway限流工具
redis:
host: localhost
port: 6379
增加Redis依赖和bucket4j依赖
com.github.vladimir-bukhtoyarov
bucket4j-core
4.2.0
org.springframework.boot
spring-boot-starter-data-redis-reactive
访问:http://localhost:8766/client/go?name=ss&token=11
可以看到控制台输出:
短时间重复刷新页面:
可以看到服务器响应 TOO_MANY_REQUESTS 429
查看控制台,可以看到令牌在减少,并且每1秒有回复1个的迹象,当令牌减少到O的时候,继续访问就会出现 TOO_MANY_REQUESTS
上面的几种过滤器都是基于yml,这次用Java来声明一个过滤器。
SkyRateLimitByIpGatewayFilterFactory 必须实现接口GatewayFilter , Ordered
自定义的Java链的过滤器比较容易,只需要在启动类GatewayApplication中添加bean,如何new相应的构造链。
@Bean
public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/client/**")
.filters(f -> f.stripPrefix(1)
.filter(new SkyRateLimitByIpGatewayFilterFactory(10, 1, Duration.ofSeconds(1)))
//多个过滤器的时候,可以继续构造下去
)
.uri("lb://service-client")
.order(0)
.id("service-client")
)
.build();
}
这样的声明可以和yml配置混合着用,过滤器的优先级主要看该过滤器实现 Ordered 中getOrder() 方法返回的数字,数字越小,表示优先级越高。
新增hystrix 依赖
org.springframework.cloud
spring-cloud-starter-netflix-hystrix
配置hystrix 和 熔断时间 timeoutInMilliseconds
spring:
application:
name: service-gateway
cloud:
gateway:
default-filters:
- Sky=true
- name: Hystrix #全局异常熔断处理,必须为Hystrix,会自动匹配HystrixGatewayFilterFactory
args:
name: fallbackcmd #必须为fallbackcmd,用于HystrixGatewayFilterFactory中bean的声明
fallbackUri: forward:/fallback #当熔断的时候,跳转该链接
#Hystrix的fallbackcmd的时间,默认为1s
hystrix:
command:
fallbackcmd:
execution:
isolation:
thread:
timeoutInMilliseconds: 2000 #毫秒
声明fallback 这个熔断响应
package com.wsk.gateway.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author WuShukai
* @version V1.0
* @description
* @date 2018/12/13 11:33
*/
@RestController
@Slf4j
public class HystrixController {
@GetMapping("/fallback")
public String fallback() {
return "hystrix fall";
}
}
Hystrix的fallbackcmd 的时间为2秒,所以我们设置一个接口地址,里面sleep了4秒,用来模拟超时
在包 controller 的类 OneController 新增代码片段
@GetMapping("/sky/histrix")
public String myHistrix(@RequestParam(value = "name", defaultValue = "true") boolean name) throws InterruptedException {
if (name) {
TimeUnit.SECONDS.sleep(4);
}
return "success";
}
访问:http://localhost:8766/client/sky/histrix?token=11
2秒后响应
证明全局熔断成功。