GateWay 是 Spring 生态系统之上构建的API网关服务, 基于 Spring5,SpringBoot 2 和Project Reactor 等技术。
Gateway 旨在提供一种简单而有效的方式来对 API 进行路由, 以及提供一些强大的过滤功能, 例如 熔断, 限流, 重试等。
SpringCloud Gateway 是SpringCloud 的一个全新项目,基于Spring 5 和 Spring Boot 2.0 Project Reactor 等技术开发的网关, 他是为了微服务提供一种简单有效的API路由管理方式
SpriingCloud Gateway 作为 Spring Cloud 生态系统中的网关, 目标是替代 Zuul,在Spring Cloud 2.0以上版本, 没有新版本的 Zuul2.0 以上最新版本的进行集成,仍然还是使用Zuul 1.x 非Reactor 模式的老版本, 而为了提升网关的性能, Spring Cloud Gateway 是基于 WebFlux 框架实现的, 而WebFlux 框架底层则使用了高性能的Reactor 模式通信的框架 Netty
SpringCloud Gateway 的目标是提供统一的路由方式且基于 Filter 链的方式提供了网关的基本功能, 例如: 安全, 监控、 限流,
传统的Web 框架,比如说 struts2, SpringMVC 等都是基于 Servlet API和servlet 容器基础之上,但是,在Servlet 3.1 之后有了异步非阻塞的支持,而WebFlux 是一个典型的非阻塞异步的框架,他的核心是基于 Reactor 的相关API实现的,相对于传统的 web 框架来说, 他可以在Netty, Undertow 及 支持Servlet 3.1 的容器上, 非阻塞式 + 函数式编程
Spring WebFlux 是 Spring 5.0 引入的新的响应式框架,区别于SpringMVC, 他不需要依赖 Servlet API, 他是完全异步非阻塞的。 并且基于 Reactor来实现响应式规范。
官方示意图
客户端向Gateway 发出请求, 然后在Gateway Handler Mapping 中找到请求相匹配的路由, 将其发送到Gateway Web Handler
Handler 在通过制定的过滤器链来讲请求发送到我们实际的服务执行业务逻辑,然后返回,过滤器链之间用虚线分开是因为过滤器可能会在发送代理之前 (“pre”) 或之后 (“post”)
sgg-gateway-api9527
引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
<dependency>
<groupId>cn.flldaygroupId>
<artifactId>sgg-api-commonartifactId>
<version>${project.version}version>
dependency>
dependencies>
server:
port: 9527
spring:
application:
name: cloud-gateway-service
cloud:
gateway:
discovery:
locator:
enabled: true # 开启动态路由
routes:
- id: payment-service # 路由唯一id
uri: lb://SGG-PAYMENT-SERVICE # lb 代表 负载均衡loadBalances , 后面跟着的是微服务名称 在 eureka 中注册的名字
predicates: # 断言
- Path=/** # 判断是否匹配
eureka:
instance:
hostname: cloud-gateway-service
instance-id: cloud-gateway-service-${server.port}
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka
@SpringBootApplication
@EnableEurekaClient
public class Gateway9527App {
public static void main(String[] args) {
SpringApplication.run(Gateway9527App.class, args);
}
}
顺序启动 7001, 7002,8001, 8002, 9527 。
通过我们的网关访问我们的payment 提供的接口
http://localhost:9527/SGG-PAYMENT-SERVICE/payment/111
。 SGG-PAYMENT-SERVICE
就是我们配置的 lb 负载均衡 的服务名 后面就匹配我们的 -Path
断言
可以看到我们通过网关访问, 网关顺利转发到真正的服务接口上。 同时还具有负载均衡的效果。
我们可以看一下其他的断言类。发现都是继承自 AbstractRoutePredicateFactory
我们也来尝试自己定义一个 断言工厂
注意项:
创建 PortPredicateFactory.java
@Component
@Slf4j
public class PortPredicateFactory extends AbstractRoutePredicateFactory<PortPredicateFactory.Config> {
public PortRoutePredicateFactory() {
super(Config.class);
}
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return serverWebExchange -> {
ServerHttpRequest request = exchange.getRequest();
ApplicationContext applicationContext = exchange.getApplicationContext();
if (config.getPort().equals(new Integer("8001"))) {
return true;
}
return false;
};
}
@Data
public class Config {
private Integer port;
}
}
上面的意思就是如果 端口号为 8001 就可以执行, 不等于 8001 就不执行。 可以看到 通过 exchange
参数我们可以获取到很多东西。 这个时候就可以通过这些东西做很多事情。 具体的我就不演示了。 现在配置yml
文件试试吧。
修改 yml 文件
routes:
- id: payment-service
uri: lb://SGG-PAYMENT-SERVICE
predicates:
- Path=/**
- Port=8001
重启服务访问 接口。
使用 8001 的时候,访问没有问题。 我们修改配置类将 -Port=8002
试试
一下都是转自 芋道源码
Gateway 内置了许多种 Route Filter 实现。 对请求进行拦截 实现自定义的功能, 比如说,限流。熔断等功能,并且, 多个 Route Filter 可以组合实现, 满足我们大多数的处理逻辑
创建AuthGatewayFilterFactory
认证filter工厂
@Component
public class AuthGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthGatewayFilterFactory.Config> {
public AuthGatewayFilterFactory() {
super(AuthGatewayFilterFactory.Config.class);
}
@Override
public GatewayFilter apply(Config config) {
Map<String, Integer> tokenMap = new HashMap<>();
tokenMap.put("gss", 1);
// 创建 gatewayfilter 对象
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
String token = headers.getFirst(config.getTokenHeaderName());
// 如果没有 token 不进行认证
if (!StringUtils.hasText(token)) {
return chain.filter(exchange);
}
// 认证开始
ServerHttpResponse response = exchange.getResponse();
Integer userId = tokenMap.get(token);
// 通过 token 获取不到 userId 说明认证不通过
if (userId == null) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
DataBuffer wrap = exchange.getResponse().bufferFactory().wrap("认证未通过".getBytes());
return response.writeWith(Flux.just(wrap));
}
request = request.mutate().header(config.getUserIdHeaderName(), String.valueOf(userId)).build();
return chain.filter(exchange.mutate().request(request).build());
}
};
}
public static class Config {
private static final String DEFAULT_TOKEN_HEADER_NAME = "token";
private static final String DEFAULT_HEADER_NAME = "user-id";
private String tokenHeaderName = DEFAULT_TOKEN_HEADER_NAME;
private String userIdHeaderName = DEFAULT_HEADER_NAME;
public static String getDefaultTokenHeaderName() {
return DEFAULT_TOKEN_HEADER_NAME;
}
public static String getDefaultHeaderName() {
return DEFAULT_HEADER_NAME;
}
public String getTokenHeaderName() {
return tokenHeaderName;
}
public void setTokenHeaderName(String tokenHeaderName) {
this.tokenHeaderName = tokenHeaderName;
}
public String getUserIdHeaderName() {
return userIdHeaderName;
}
public void setUserIdHeaderName(String userIdHeaderName) {
this.userIdHeaderName = userIdHeaderName;
}
}
}
通过@Compoent
注解 保证 Gateway 在加载所有的 GatewayFilterFactory Bean 的时候,能够加载到我们的自定义AuthGatewayFilterFactory
, 有没有发现和自定义断言的时候 一毛一样 哈哈哈
继承 AbstractGatewayFilterFactory
抽象类。 并将泛型参数
设置为我们自己定义的 AuthGatewayFilterFactory.Config
配置类, 这样,Gateway 在解析的时候,会转换成 Config 对象
注意:在AuthGatewayFilterFactory 构造方法中,需要传递 Config 类给父级构造方法,保证能够正确创建出Config 对象。在Config 类中我们定义了两个属性。
- tokenHeaderName
: 认证token 的 header 的名字, 默认值为 token
- userIdHeaderName
: 认证后的UserId 的 header 名字, 默认为 user-id
在方法 apply(Config config)
中, 我们通过内部类定义了需要创建的GatewayFilter。
spring:
cloud:
gateway:
default-filters:
- name: Auth
args:
token-header-name: access-token
spring.cloud.gateway.default-filters
配置项, Gateway 默认过滤器,对所有的路由都生效。对应 FilterDefinition 数组, 在这里我们配置之了一个自定义的 Filter 配置
- name
: 过滤器名称,这里我们设置为Auth
, 因为 Gateway 默认使用的 XXXGatewayFilterFactory
的前缀 XXX
名字。 因为 AuthGatewayFilterFactory
就是 Auth
- args
: 过滤器的参数配置, 对应 Config 类,这里我们设置 token-header-name
配置项 access-token
, 表示从请求头 access-token
中获取认证 token
使用 PostMan 请求接口
使用 access-token 为 gss 时候
Gateway 内置 RequestRateLimiterGatewayFilterFactory
提供请求限流功能。 该 Filter
是基于 Token Bucket Algorithm
令牌桶算法。 同时搭配 redis 实现分布式限流。
限流需要使用到 redis 我们先启动一下 redis, 然后引入redis 的依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
配置 yml 文件
spring:
redis:
host: 127.0.0.1
port: 6379
cloud:
gateway:
default-filters:
- name: Auth
args:
token-header-name: access-token
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1 # 令牌桶的每秒放的数量
redis-rate-limiter.burstCapacity: 2 # 令牌桶的最大令牌数
key-resolver: "#{@ipKeyResolver}" # 获取限流key 的bean 的名字
说一下。 yml 文件配置是增量的。 在上面的基础上添加。 不是删除掉。在添加这个
再次添加一个 default-filters
配置项 。 添加了限流过滤器 RequestRateLimiter
配置参数:
- redis-rate-limiter.replenishRate
: 1 # 令牌桶的每秒放的数量
- redis-rate-limiter.burstCapacity
: 2 # 令牌桶的最大令牌数
- key-resolver
: 获取限流key 的Bean 的名字
burstCapacity 参数: 可以理解为每秒最大的请求数,因此每请求一次,都会从桶里获取一块令牌
ReplenishRate 参数: 可以近似理解为每秒平均的请求数,如果令牌桶为空的情况下,一秒最多放这么多令牌书, 所以最大请求数当然也是这么多
实际上,在令牌桶满的情况下, 每秒最大请求书是 burstCapacity + ReplenishRate
在 GatewayConfig 配置类下创建获取限流Key 的Bean
@Bean
public KeyResolver ipKeyResolver(){
return new KeyResolver() {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
};
}
创建的 ipKeyResolver Bean 是通过解析请求的来源 IP 作为限流 KEY,这样我们就能实现基于 IP 的请求限流。我们想要实现基于用户的请求限流,那么我们可以创建从请求中解析用户身份的 KeyResolver Bean。也就是说,通过自定义的 KeyResolver 来实现不同粒度的请求限流
使用浏览器,疯狂访问 http://localhost:9527/SGG-PAYMENT-SERVICE/payment/1
就会被限流。 出现 429 页面。
引入依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
创建 FallbackController
,提供 /fallback
接口。 用于Hystrix fallback 的重定向。
@RestController
@Slf4j
public class FallbackController {
@GetMapping(value = "fallback")
public String fallback(ServerWebExchange exchange) {
Object requestURL = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
Object error = exchange.getAttribute(ServerWebExchangeUtils.HYSTRIX_EXECUTION_EXCEPTION_ATTR);
log.error("fallback 发生异常: [ {} ], [ {} ]", error, requestURL);
return "服务降级。。。" + error;
}
}
通过 exchange.getAttribute(ServerWebExchangeUtils.HYSTRIX_EXECUTION_EXCEPTION_ATTR)
可以获取具体的fallback 异常。
配置 yml 文件
spring:
redis:
host: 127.0.0.1
port: 6379
cloud:
gateway:
default-filters:
- name: Hystrix
args:
name: fallbackcmd # 对应 Hystrix Command 名字
fallbackUri: forwad:/fallback # 处理 Hystrix fallback 的情况, 重定向到指定地址
在 filters 中配置项,添加了 Hystrix 过滤器。 其配置参数如下。:
name
: 对应的 Hystrix Command 名字,后续可以通过 hystrix.command.{name} 配置项, 来设置 name 对应的 Hystrix Command 的配置, 比如说超时时间,隔离策略等。fallbackUri
: 处理 Hystrix fallback 的情况。 重定向到指定地址,主要 要么为空 要么必须以forward:
开头
访问 timeout 超时接口。 可以看到出现异常了就会转发到我们的 fallback
在Gateway 中, 有两类过滤器
- Route Filter 路由过滤器 对应GatewayFilter
接口
- Global Filter 全局过滤器 对应 GlobalFilter
接口
两者基本是等价的,不同的是 Route Filter不是全局,可以配置在 指定路由上。 绝大多数情况, RouteFilter 能满足我们的拓展需求的情况下,优先使用它, 并且如果想要作用到所有的路由上。可以使用 spring.cloud.gateway.default-filters
配置上。另外 Global Filter 可能在未来的版本有一定的变化。
Gateway的过滤器的执行分成了前置 pre 和 后置 post 两个阶段,其中低排序值的过滤器在pre 阶段先执行, 高排序值得过滤器在post阶段限制性。 示例代码 。 创建了三个 filter
@Component
@Order(1)
@Slf4j
public class FGlobalFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("[F, [pre]]");
return chain.filter(exchange).then(Mono.<Void>fromRunnable(()->log.info("[F, [post]]")));
}
}
@Component
@Order(2)
@Slf4j
public class SGlobalFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("[S, [pre]]");
return chain.filter(exchange).then(Mono.<Void>fromRunnable(()->log.info("[S, [post]]")));
}
}
@Component
@Order(3)
@Slf4j
public class TGlobalFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("[T, [pre]]");
return chain.filter(exchange).then(Mono.<Void>fromRunnable(()->log.info("[T, [post]]")));
}
}
重启接口,发现打印和我们之前说的一样
2020-09-13 01:24:05.068 INFO 16968 --- [ioEventLoop-4-1] cn.fllday.filter.FGlobalFilter : [F, [pre]]
2020-09-13 01:24:05.070 INFO 16968 --- [ioEventLoop-4-1] cn.fllday.filter.SGlobalFilter : [S, [pre]]
2020-09-13 01:24:05.070 INFO 16968 --- [ioEventLoop-4-1] cn.fllday.filter.TGlobalFilter : [T, [pre]]
2020-09-13 01:24:05.198 INFO 16968 --- [ctor-http-nio-4] cn.fllday.filter.TGlobalFilter : [T, [post]]
2020-09-13 01:24:05.198 INFO 16968 --- [ctor-http-nio-4] cn.fllday.filter.SGlobalFilter : [S, [post]]
2020-09-13 01:24:05.198 INFO 16968 --- [ctor-http-nio-4] cn.fllday.filter.FGlobalFilter : [F, [post]]
Gateway的 actuate
模块, 基于 SpringBoot Actuator , 提供了 自动以监控断点 gateway
, 提供了 Gateway 的各种监控管理的功能
路径 | 用途 |
---|---|
GET /globalfilters | 获取所有的 GlobalFilter |
GET /routefilters | 获取所有GatewayFilterFactory |
GET /routepredicates | 后去所有的RoutePredicateFactory |
GET /routes | 获取所有的路由 |
GET /routes/{id} | 获取指定路由 |
GET /routes/{id}/combinedfilters | 获得指定路由的过滤器 |
POST /routes/{id} | 新增或修改,参数为 RouteDefinition |
DELETE /routes/{id} | 删除指定路由 |
POST /refresh | 刷新路由缓存 |
注意: 所有的基础路径为 /actuator/gateway
,所以 /globalfilters
对应的完整路径是 /actuator/gateway/globalfilters
引入 pom 依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
修改 yml 文件 , 配置 Spring Boot Actuator
management:
endpoints:
web:
exposure:
include: '*' # 需要开放的端点,默认只开放 health 和 info 两个端点,通过 * 开放所有的端点。
endpoint:
health:
enabled: true # 是否开启, 默认true 开启
show-details: always # 何时显示完整的健康信息。 默认 never 都不展示。 可选择 WHEN_AUTHORIZED 当经过授权的用户, 可选 always 总是显示
获取 filter 和 predicate
GlobalFilter: http://localhost:9527/actuator/gateway/globalfilters
GatewayFilterFactory: http://localhost:9527/actuator/gateway/routefilters
Predicates: http://localhost:9527/actuator/gateway/routepredicates
路由管理
使用 PostMan 请求GET /actuator/gateway/routes/{id}
获取一个路由详情
然后根据这个 我们新建一个路由
POST /actuator/gateway/routes/{id}
添加完成之后,我们通过 使用 PostMan 请求GET /actuator/gateway/routes/baidutieba
获取一个路由详情
如果不行的话,记得使用refresh 刷新一下 POST /actuator/gateway/refresh
然后不要重启服务。 直接访问 http://localhost:9527/
就会直接跳转到百度贴吧咯
修改 yml 文件。配置日记级别如下
logging:
level:
reactor.netty: DEBUG
org.springframework.cloud.gateway: TRACE
这样,SpringCloudGateway 和 Reactor Netty 可以打印更多的日志
重启网关。
Gateway日志。 其他 Reactor Netty 日志太多。 就不截图了。
Reactor Netty 提供了 Wiretap 窃听功能。 让 Reactor Netty 打印包含请求和响应信息的日志, 比如说请求和响应的Header Body 等等, 开启 Reactor Netty 的Wiretap 功能一共有三个配置项
reactor.netty
的配置项为DEBUG 和 TRACEspring.cloud.gateway.httpserver.wiretap
配置项为 true
开启 HttpServer Wiretap 功能spring.cloud.gateway.httpclient.wiretap
配置项为 true
开启 Http Client wiretap 功能 httpserver: # 配置 Reactor Netty 相关配置
wiretap: true
httpclient:
wiretap: true
感谢B站尚硅谷老师哈哈 ~~~ 本文参考了
芋道源码 感谢!@!!