- 简介
- predicate
- filter
- 限流
- 配合注册中心路由转发
- 使用
Spring Cloud Gateway是Spring Cloud官方推出的第二代网关框架,取代Zuul网关。网关作为流量的,在微服务系统中有着非常作用,网关常见的功能有路由转发、权限校验、限流控制等作用。
简介
作用
- 协议转换,路由转发
- 流量聚合,对流量进行监控,日志输出
- 作为整个系统的前端工程,对流量进行控制,有限流的作用
- 作为系统的前端边界,外部流量只能通过网关才能访问系统
- 可以在网关层做权限的判断
- 可以在网关层做缓存
工作流程
客户端向Spring Cloud Gateway发出请求。 如果Gateway Handler Mapping确定请求与路由匹配(这个时候就用到predicate),则将其发送到Gateway web handler处理。
Gateway web handler处理请求时会经过一系列的过滤器链。 过滤器链被虚线划分的原因是过滤器链可以在发送代理请求之前或之后执行过滤逻辑。
先执行所有“pre”过滤器逻辑,然后进行代理请求。 在发出代理请求之后,收到代理服务的响应之后执行“post”过滤器逻辑。在执行所有“pre”过滤器逻辑时,往往进行了鉴权、限流、日志输出等功能,以及请求头的更改、协议的转换;转发之后收到响应之后,会执行所有“post”过滤器的逻辑,在这里可以响应数据进行了修改,比如响应头、协议的转换等。
predicate
predicate简介
predicate(断言)的作用是将请求和路由进行匹配,它决定了一个请求走哪一个路由。
Predicate来自于java8的接口。Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将Predicate组合成其他复杂的逻辑(比如:与,或,非)。可以用于接口请求参数校验、判断新老数据是否有变化需要进行更新操作。add–与、or–或、negate–非。
Spring Cloud Gateway内置了许多Predict,这些Predict的源码在org.springframework.cloud.gateway.handler.predicate包中,列举各种Predicate如下图:
上图中有很多类型的Predicate
- 时间类型的Predicated(AfterRoutePredicateFactory BeforeRoutePredicateFactory BetweenRoutePredicateFactory),当只有满足特定时间要求的请求会进入到此predicate中,并交由router处理
- cookie类型的CookieRoutePredicateFactory,指定的cookie满足正则匹配,才会进入此router
- 以及host、method、path、querparam、remoteaddr类型的predicate,每一种predicate都会对当前的客户端请求进行判断,是否满足当前的要求,如果满足则交给当前请求处理。
- 如果有很多个Predicate,并且一个请求满足多个Predicate,则按照配置的顺序第一个生效。
predicate实战
案例来源于官方文档:http://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.0.0.RELEASE/single/spring-cloud-gateway.html
Method Route Predicate Factory
Method Route Predicate Factory 需要一个参数,即请求的类型。比如GET类型的请求都转发到此路由。在工程的配置文件加上以下的配置:
代码语言:javascript复制spring:
profiles:
active: method_route
---
spring:
cloud:
gateway:
routes:
- id: method_route
uri: http://httpbin.org:80/get
predicates:
- Method=GET
profiles: method_route
在上面的配置中,所有的GET类型的请求都会路由转发到配置的uri。使用 curl命令模拟 get类型的请求,会得到正确的返回结果。
代码语言:javascript复制$ curl localhost:8081
使用 curl命令模拟 post请求,则返回404结果。
代码语言:javascript复制$ curl -XPOST localhost:8081
源码下载
https://github.com/forezp/SpringCloudLearning/tree/master/sc-f-gateway-predicate
filter
Predict决定了请求由哪一个路由处理,在路由处理之前,需要经过“pre”类型的过滤器处理,处理返回响应之后,可以由“post”类型的过滤器处理。
在“pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等,在“post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等。
作用
当我们有很多个服务时,客户端请求各个服务的Api时,每个服务都需要做相同的事情,比如鉴权、限流、日志输出等。
对于这样重复的工作,可以在微服务的上一层加一个全局的权限控制、限流、日志输出的Api Gatewat服务,然后再将请求转发到具体的业务服务层。这个Api Gateway服务就是起到一个服务边界的作用,外接的请求访问系统,必须先通过网关层。
生命周期
客户端的请求先经过“pre”类型的filter,然后将请求转发到具体的业务服务,收到业务服务的响应之后,再经过“post”类型的filter处理,最后返回响应到客户端。
filter除了分为“pre”和“post”两种方式的filter外,filter从作用范围可分为另外两种,一种是针对于单个路由的gateway filter,它在配置文件中的写法同predict类似;另外一种是针对于所有路由的global gateway filer。
gateway filter
过滤器允许以某种方式修改传入的HTTP请求或传出的HTTP响应。过滤器可以限定作用在某些特定请求路径上。 Spring Cloud Gateway包含许多内置的GatewayFilter工厂。
GatewayFilter工厂同上一篇介绍的Predicate工厂类似,都是在配置文件application.yml中配置。在配置文件中配置的GatewayFilter Factory最终都会相应的过滤器工厂类处理。
遵循了约定大于配置的思想,只需要在配置文件配置GatewayFilter Factory的名称,而不需要写全部的类名,比如AddRequestHeaderGatewayFilterFactory只需要在配置文件中写AddRequestHeader,而不是全部类名。
Spring Cloud Gateway 内置的过滤器工厂一览表如下:
AddRequestHeader GatewayFilter Factory
创建工程,引入相关的依赖,包括spring boot 版本2.0.5,spring Cloud版本Finchley,gateway依赖如下:
代码语言:javascript复制<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
在工程的配置文件中,加入以下的配置:
代码语言:javascript复制server:
port: 8081
spring:
profiles:
active: add_request_header_route
---
spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: http://httpbin.org:80/get
filters:
- AddRequestHeader=X-Request-Foo, Bar
predicates:
- After=2017-01-20T17:42:47.789-07:00[America/Denver]
profiles: add_request_header_route
在add_request_header_route配置中,配置了路由地址为http://httpbin.org:80/get,该router有AfterPredictFactory,有一个filter为AddRequestHeaderGatewayFilterFactory(约定写成AddRequestHeader)。
AddRequestHeader过滤器工厂会在请求头加上一对请求头,名称为X-Request-Foo,值为Bar。
AddRequestHeaderGatewayFilterFactory的源码如下:
代码语言:javascript复制public class AddRequestHeaderGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
@Override
public GatewayFilter apply(NameValueConfig config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest().mutate()
.header(config.getName(), config.getValue())
.build();
return chain.filter(exchange.mutate().request(request).build());
};
}
}
由上面的代码可知,根据旧的ServerHttpRequest创建新的 ServerHttpRequest ,在新的ServerHttpRequest加了一个请求头,然后创建新的 ServerWebExchange ,提交过滤器链继续过滤。
启动工程,通过curl命令来模拟请求:
代码语言:javascript复制curl localhost:8081
最终显示了从 http://httpbin.org:80/get得到了请求,响应如下:
代码语言:javascript复制{
"args": {},
"headers": {
"Accept": "*/*",
"Connection": "close",
"Forwarded": "proto=http;host="localhost:8081";for="0:0:0:0:0:0:0:1:56248"",
"Host": "httpbin.org",
"User-Agent": "curl/7.58.0",
"X-Forwarded-Host": "localhost:8081",
"X-Request-Foo": "Bar"
},
"origin": "0:0:0:0:0:0:0:1, 210.22.21.66",
"url": "http://localhost:8081/get"
}
从上面的响应可知,确实在请求头中加入了X-Request-Foo这样的一个请求头,在配置文件中配置的AddRequestHeader过滤器工厂生效。
RewritePath GatewayFilter Factory
在Nginx服务启中有一个非常强大的功能就是重写路径,Spring Cloud Gateway默认也提供了这样的功能,这个功能是Zuul没有的。在配置文件中加上以下的配置:
代码语言:javascript复制spring:
profiles:
active: rewritepath_route
---
spring:
cloud:
gateway:
routes:
- id: rewritepath_route
uri: https://blog.csdn.net
predicates:
- Path=/foo/**
filters:
- RewritePath=/foo/(?<segment>.*), /${segment}
profiles: rewritepath_route
上面的配置中,所有的/foo/**
开始的路径都会命中配置的router,并执行过滤器的逻辑。
在本案例中配置了RewritePath过滤器工厂,此工厂将/foo/(?.*)
重写为{segment},然后转发到https://blog.csdn.net
。比如在网页上请求localhost:8081/foo/xxxxxxx
,此时会将请求转发到https://blog.csdn.net/xxxxxxx
的页面。
自定义过滤器
Spring Cloud Gateway内置了19种强大的过滤器工厂,能够满足很多场景的需求。在spring Cloud Gateway中,过滤器需要实现GatewayFilter和Ordered这2个接口。写一个RequestTimeFilter,代码如下:
代码语言:javascript复制public class RequestTimeFilter implements GatewayFilter, Ordered {
private static final Log log = LogFactory.getLog(GatewayFilter.class);
private static final String REQUEST_TIME_BEGIN = "requestTimeBegin";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
exchange.getAttributes().put(REQUEST_TIME_BEGIN, System.currentTimeMillis());
return chain.filter(exchange).then(
Mono.fromRunnable(() -> {
Long startTime = exchange.getAttribute(REQUEST_TIME_BEGIN);
if (startTime != null) {
log.info(exchange.getRequest().getURI().getRawPath() ": " (System.currentTimeMillis() - startTime) "ms");
}
})
);
}
@Override
public int getOrder() {
return 0;
}
}
在上面的代码中,Ordered中的int getOrder()方法是来给过滤器设定优先级别的,值越大则优先级越低。还有有一个filterI(exchange,chain)方法,在该方法中,先记录了请求的开始时间,并保存在ServerWebExchange中,此处是一个“pre”类型的过滤器,然后再chain.filter的内部类中的run()方法中相当于”post”过滤器,在此处打印了请求所消耗的时间。然后将该过滤器注册到router中,代码如下:
代码语言:javascript复制 @Bean
public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) {
// @formatter:off
return builder.routes()
.route(r -> r.path("/customer/**")
.filters(f -> f.filter(new RequestTimeFilter())
.addResponseHeader("X-Response-Default-Foo", "Default-Bar"))
.uri("http://httpbin.org:80/get")
.order(0)
.id("customer_filter_router")
)
.build();
// @formatter:on
}
重启程序,通过curl命令模拟请求:
代码语言:javascript复制 curl localhost:8081/customer/123
在程序的控制台输出一下的请求信息的日志:
代码语言:javascript复制2018-11-16 15:02:20.177 INFO 20488 --- [ctor-http-nio-3] o.s.cloud.gateway.filter.GatewayFilter : /customer/123: 152ms
自定义过滤器工厂
在上面的自定义过滤器中,有没有办法自定义过滤器工厂类呢?这样就可以在配置文件中配置过滤器了。现在需要实现一个过滤器工厂,在打印时间的时候,可以设置参数来决定是否打印请参数。查看GatewayFilterFactory的源码,可以发现GatewayFilterfactory的层级如下:
过滤器工厂的顶级接口是GatewayFilterFactory,我们可以直接继承它的两个抽象类来简化开发AbstractGatewayFilterFactory和AbstractNameValueGatewayFilterFactory,这两个抽象类的区别就是前者接收一个参数(像StripPrefix和我们创建的这种),后者接收两个参数(像AddResponseHeader)。
过滤器工厂的顶级接口是GatewayFilterFactory,有2个两个较接近具体实现的抽象类,分别为AbstractGatewayFilterFactory和AbstractNameValueGatewayFilterFactory,这2个类前者接收一个参数,比如它的实现类RedirectToGatewayFilterFactory;后者接收2个参数,比如它的实现类AddRequestHeaderGatewayFilterFactory类。现在需要将请求的日志打印出来,需要使用一个参数,这时可以参照RedirectToGatewayFilterFactory的写法。
代码语言:javascript复制public class RequestTimeGatewayFilterFactory extends AbstractGatewayFilterFactory<RequestTimeGatewayFilterFactory.Config> {
private static final Log log = LogFactory.getLog(GatewayFilter.class);
private static final String REQUEST_TIME_BEGIN = "requestTimeBegin";
private static final String KEY = "withParams";
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList(KEY);
}
public RequestTimeGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
exchange.getAttributes().put(REQUEST_TIME_BEGIN, System.currentTimeMillis());
return chain.filter(exchange).then(
Mono.fromRunnable(() -> {
Long startTime = exchange.getAttribute(REQUEST_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());
}
})
);
};
}
public static class Config {
private boolean withParams;
public boolean isWithParams() {
return withParams;
}
public void setWithParams(boolean withParams) {
this.withParams = withParams;
}
}
}
在上面的代码中 apply(Config config)方法内创建了一个GatewayFilter的匿名类,具体的实现逻辑跟之前一样,只不过加了是否打印请求参数的逻辑,而这个逻辑的开关是config.isWithParams()。静态内部类类Config就是为了接收那个boolean类型的参数服务的,里边的变量名可以随意写,但是要重写List shortcutFieldOrder()这个方法。 。
需要注意的是,在类的构造器中一定要调用下父类的构造器把Config类型传过去,否则会报ClassCastException
最后,需要在工程的启动文件Application类中,向Srping Ioc容器注册RequestTimeGatewayFilterFactory类的Bean。
代码语言:javascript复制 @Bean
public RequestTimeGatewayFilterFactory elapsedGatewayFilterFactory() {
return new RequestTimeGatewayFilterFactory();
}
然后可以在配置文件中配置如下:
代码语言:javascript复制spring:
profiles:
active: elapse_route
---
spring:
cloud:
gateway:
routes:
- id: elapse_route
uri: http://httpbin.org:80/get
filters:
- RequestTime=false
predicates:
- After=2017-01-20T17:42:47.789-07:00[America/Denver]
profiles: elapse_route
启动工程,在浏览器上访问localhost:8081?name=forezp,可以在控制台上看到,日志输出了请求消耗的时间和请求参数。
global filter
Spring Cloud Gateway根据作用范围划分为GatewayFilter和GlobalFilter,二者区别如下:
- GatewayFilter : 需要通过spring.cloud.routes.filters 配置在具体路由下,只作用在当前路由上或通过spring.cloud.default-filters配置在全局,作用在所有路由上
- GlobalFilter : 全局过滤器,不需要在配置文件中配置,作用在所有的路由上,最终通过GatewayFilterAdapter包装成GatewayFilterChain可识别的过滤器,它为请求业务以及路由的URI转换为真实业务服务的请求地址的核心过滤器,不需要配置,系统初始化时加载,并作用在每个路由上。
Spring Cloud Gateway框架内置的GlobalFilter如下:
上图中每一个GlobalFilter都作用在每一个router上,能够满足大多数的需求。但是如果遇到业务上的定制,可能需要编写满足自己需求的GlobalFilter。在下面的案例中将讲述如何编写自己GlobalFilter,该GlobalFilter会校验请求中是否包含了请求参数“token”,如何不包含请求参数“token”则不转发路由,否则执行正常的逻辑。代码如下:
代码语言:javascript复制public class TokenFilter implements GlobalFilter, Ordered {
Logger logger=LoggerFactory.getLogger( TokenFilter.class );
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getQueryParams().getFirst("token");
if (token == null || token.isEmpty()) {
logger.info( "token is empty..." );
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -100;
}
}
在上面的TokenFilter需要实现GlobalFilter和Ordered接口,这和实现GatewayFilter很类似。然后根据ServerWebExchange获取ServerHttpRequest,然后根据ServerHttpRequest中是否含有参数token,如果没有则完成请求,终止转发,否则执行正常的逻辑。
然后需要将TokenFilter在工程的启动类中注入到Spring Ioc容器中,代码如下:
代码语言:javascript复制@Bean
public TokenFilter tokenFilter(){
return new TokenFilter();
}
启动工程,使用curl命令请求:
代码语言:javascript复制 curl localhost:8081/customer/123
可以看到请没有被转发,请求被终止,并在控制台打印了如下日志:
代码语言:javascript复制2018-11-16 15:30:13.543 INFO 19372 --- [ctor-http-nio-2] gateway.TokenFilter : token is empty...
上面的日志显示了请求进入了没有传“token”的逻辑。
源码下载
https://github.com/forezp/SpringCloudLearning/tree/master/sc-f-gateway-predicate
限流
常见的限流算法
计数器算法
计数器算法采用计数器实现限流有点简单粗暴,一般我们会限制一秒钟的能够通过的请求数,比如限流qps为100,算法的实现思路就是从第一个请求进来开始计时,在接下去的1s内,每来一个请求,就把计数加1,如果累加的数字达到了100,那么后续的请求就会被全部拒绝。等到1s结束后,把计数恢复成0,重新开始计数。具体的实现可以是这样的:对于每次服务调用,可以通过AtomicLong#incrementAndGet()方法来给计数器加1并返回最新值,通过这个最新值和阈值进行比较。这种实现方式,相信大家都知道有一个弊端:如果我在单位时间1s内的前10ms,已经通过了100个请求,那后面的990ms,只能眼巴巴的把请求拒绝,我们把这种现象称为“突刺现象”
漏桶算法
漏桶算法为了消除”突刺现象”,可以采用漏桶算法实现限流,漏桶算法这个名字就很形象,算法内部有一个容器,类似生活用到的漏斗,当请求进来时,相当于水倒入漏斗,然后从下端小口慢慢匀速的流出。不管上面流量多大,下面流出的速度始终保持不变。不管服务调用方多么不稳定,通过漏桶算法进行限流,每10毫秒处理一次请求。因为处理的速度是固定的,请求进来的速度是未知的,可能突然进来很多请求,没来得及处理的请求就先放在桶里,既然是个桶,肯定是有容量上限,如果桶满了,那么新进来的请求就丢弃。
在算法实现方面,可以准备一个队列,用来保存请求,另外通过一个线程池(ScheduledExecutorService)来定期从队列中获取请求并执行,可以一次性获取多个并发执行。
这种算法,在使用过后也存在弊端:无法应对短时间的突发流量。
令牌桶算法
从某种意义上讲,令牌桶算法是对漏桶算法的一种改进,桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌,所以就存在这种情况,桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,比如设置qps为100,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,这时服务还没完全启动好,等启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。所以,只有桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率执行。
实现思路:可以准备一个队列,用来保存令牌,另外通过一个线程池定期生成令牌放到队列中,每来一个请求,就从队列中获取一个令牌,并继续执行。
Spring Cloud Gateway限流
在Spring Cloud Gateway中,有Filter过滤器,因此可以在“pre”类型的Filter中自行实现上述三种过滤器。但是限流作为网关最基本的功能,Spring Cloud Gateway官方就提供了RequestRateLimiterGatewayFilterFactory这个类,适用Redis和lua脚本实现了令牌桶的方式。
具体实现逻辑在RequestRateLimiterGatewayFilterFactory类中,lua脚本在如下图所示的文件夹中:
首先在工程的pom文件中引入gateway的起步依赖和redis的reactive依赖,代码如下:
代码语言:javascript复制 <dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifatId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
在配置文件中做以下的配置:
代码语言:javascript复制server:
port: 8081
spring:
cloud:
gateway:
routes:
- id: limit_route
uri: http://httpbin.org:80/get
predicates:
- After=2017-01-20T17:42:47.789-07:00[America/Denver]
filters:
- name: RequestRateLimiter
args:
key-resolver: '#{@hostAddrKeyResolver}'
redis-rate-limiter.replenishRate: 1
redis-rate-limiter.burstCapacity: 3
application:
name: gateway-limiter
redis:
host: localhost
port: 6379
database: 0
在上面的配置文件,指定程序的端口为8081,配置了 redis的信息,并配置了RequestRateLimiter的限流过滤器,该过滤器需要配置三个参数:
- burstCapacity,令牌桶总容量。
- replenishRate,令牌桶每秒填充平均速率。
- key-resolver,用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。
KeyResolver需要实现resolve方法,比如根据Hostname进行限流,则需要用hostAddress去判断。实现完KeyResolver之后,需要将这个类的Bean注册到Ioc容器中。
代码语言:javascript复制public class HostAddrKeyResolver implements KeyResolver {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}
}
@Bean
public HostAddrKeyResolver hostAddrKeyResolver() {
return new HostAddrKeyResolver();
}
可以根据uri去限流,这时KeyResolver代码如下:
代码语言:javascript复制public class UriKeyResolver implements KeyResolver {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getURI().getPath());
}
}
@Bean
public UriKeyResolver uriKeyResolver() {
return new UriKeyResolver();
}
也可以以用户的维度去限流:
代码语言:javascript复制 @Bean
KeyResolver userKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
}
用jmeter进行压测,配置10thread去循环请求lcoalhost:8081,循环间隔1s。从压测的结果上看到有部分请求通过,由部分请求失败。通过redis客户端去查看redis中存在的key。如下:
可见,RequestRateLimiter是使用Redis来进行限流的,并在redis中存储了2个key。关注这两个key含义可以看lua源代码。
源码下载
https://github.com/forezp/SpringCloudLearning/tree/master/sc-f-gateway-limiter
配合注册中心进行路由转发
之前采用硬编码的方式进行路由转发,现在Spring Cloud Gateway如何配合服务注册中心进行路由转发。
工程介绍
本案例中使用spring boot的版本为2.0.3.RELEASE,spring cloud版本为Finchley.RELEASE。在中涉及到了三个工程, 分别为注册中心eureka-server、服务提供者service-hi、 服务网关service-gateway,如下:
工程名 | 端口 | 作用 |
---|---|---|
eureka-server | 8761 | 注册中心eureka server |
service-hi | 8762 | 服务提供者 eurka client |
service-gateway | 8081 | 路由网关 eureka client |
这三个工程中,其中service-hi、service-gateway向注册中心eureka-server注册。用户的请求首先经过service-gateway,根据路径由gateway的predict 去断言进到哪一个 router, router经过各种过滤器处理后,最后路由到具体的业务服务,比如 service-hi。如图:
eureka-server、service-hi这两个工程直接复制于我的另外一篇文章https://blog.csdn.net/forezp/article/details/81040925 ,在这就不在重复,可以查看源码,源码地址见文末链接。 其中,service-hi服务对外暴露了一个RESTFUL接口“/hi”接口。现在重点讲解service-gateway。
gateway工程详细介绍
在gateway工程中引入项目所需的依赖,包括eureka-client的起步依赖和gateway的起步依赖,代码如下:
代码语言:javascript复制<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
在工程的配置文件application.yml中 ,指定程序的启动端口为8081,注册地址、gateway的配置等信息,配置信息如下:
代码语言:javascript复制server:
port: 8081
spring:
application:
name: sc-gateway-service
cloud:
gateway:
discovery:
locator:
enabled: true
lowerCaseServiceId: true
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
其中,spring.cloud.gateway.discovery.locator.enabled为true,表明gateway开启服务注册和发现的功能,并且spring cloud gateway自动根据服务发现为每一个服务创建了一个router,这个router将以服务名开头的请求路径转发到对应的服务。spring.cloud.gateway.discovery.locator.lowerCaseServiceId是将请求路径上的服务名配置为小写(因为服务注册的时候,向注册中心注册时将服务名转成大写的了),比如以/service-hi/*的请求路径被路由转发到服务名为service-hi的服务上。
在浏览器上请求输入localhost:8081/service-hi/hi?name=1323,网页获取以下的响应:
代码语言:javascript复制hi 1323 ,i am from port:8762
在上面的例子中,向gateway-service发送的请求时,url必须带上服务名service-hi这个前缀,才能转发到service-hi上,转发之前会将service-hi去掉。 那么我能不能自定义请求路径呢,毕竟根据服务名有时过于太长,或者历史的原因不能根据服务名去路由,需要由自定义路径并转发到具体的服务上。答案是肯定的是可以的,只需要修改工程的配置文件application.yml,具体配置如下:
代码语言:javascript复制spring:
application:
name: sc-gateway-server
cloud:
gateway:
discovery:
locator:
enabled: false
lowerCaseServiceId: true
routes:
- id: service-hi
uri: lb://SERVICE-HI
predicates:
- Path=/demo/**
filters:
- StripPrefix=1
在上面的配置中,配置了一个Path 的predict,将以/demo/**开头的请求都会转发到uri为lb://SERVICE-HI的地址上,lb://SERVICE-HI即service-hi服务的负载均衡地址,并用StripPrefix的filter 在转发之前将/demo去掉。同时将spring.cloud.gateway.discovery.locator.enabled改为false,如果不改的话,之前的localhost:8081/service-hi/hi?name=1323这样的请求地址也能正常访问,因为这时为每个服务创建了2个router。
在浏览器上请求localhost:8081/demo/hi?name=1323,浏览器返回以下的响应:
代码语言:javascript复制hi 1323 ,i am from port:8762
返回的结果跟我们预想的一样。
源码下载
https://github.com/forezp/SpringCloudLearning/tree/master/sc-f-gateway-cloud
使用
工程构建
新建一个gateway的工程,工程目录如下:
gateway需要注册到nacos中去,需要引入以下的依赖:
代码语言:javascript复制 <dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
在配置文件application.pom文件:
代码语言:javascript复制server:
port: 5000
spring:
application:
name: gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
discovery:
locator:
enabled: false
lowerCaseServiceId: true
routes:
- id: provider
uri: lb://provider
predicates:
- Path=/provider/**
filters:
- StripPrefix=1
- id: consumer
uri: lb://consumer
predicates:
- Path=/consumer/**
filters:
- StripPrefix=1
配置的解释请阅读文末的相关教程,在这里不再重复。
在工程的启动文件加上相关注解:
代码语言:javascript复制 @SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
依次启动gatewayconsumerprovider三个工程,在nacos中已经成功注册:
在浏览器上输入http://localhost:5000/consumer/hi-feign,浏览器返回响应:
代码语言:javascript复制hello feign, i'm provider ,my port:8762
创建一个简单的路由
在spring cloud gateway中使用RouteLocator的Bean进行路由转发,将请求进行处理,最后转发到目标的下游服务。在本案例中,会将请求转发到http://httpbin.org:80这个地址上。代码如下:
代码语言:javascript复制@SpringBootApplication
@RestController
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route(p -> p
.path("/get")
.filters(f -> f.addRequestHeader("Hello", "World"))
.uri("http://httpbin.org:80"))
.build();
}
}
在上面的myRoutes方法中,使用了一个RouteLocatorBuilder的bean去创建路由,除了创建路由RouteLocatorBuilder可以让你添加各种predicates和filters,predicates断言的意思,顾名思义就是根据具体的请求的规则,由具体的route去处理,filters是各种过滤器,用来对请求做各种判断和修改。
上面创建的route可以让请求“/get”请求都转发到“http://httpbin.org/get”。在route配置上,我们添加了一个filter,该filter会将请求添加一个header,key为hello,value为world。
启动springboot项目,在浏览器上http://localhost:8080/get,浏览器显示如下:
代码语言:javascript复制{
"args": {},
"headers": {
"Accept": "text/html,application/xhtml xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Cache-Control": "max-age=0",
"Connection": "close",
"Cookie": "_ga=GA1.1.412536205.1526967566; JSESSIONID.667921df=node01oc1cdl4mcjdx1mku2ef1l440q1.node0; screenResolution=1920x1200",
"Forwarded": "proto=http;host="localhost:8080";for="0:0:0:0:0:0:0:1:60036"",
"Hello": "World",
"Host": "httpbin.org",
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"X-Forwarded-Host": "localhost:8080"
},
"origin": "0:0:0:0:0:0:0:1, 210.22.21.66",
"url": "http://localhost:8080/get"
}
可见当我们向gateway工程请求“/get”,gateway会将工程的请求转发到“http://httpbin.org/get”,并且在转发之前,加上一个filter,该filter会将请求添加一个header,key为hello,value为world。
注意HTTPBin展示了请求的header hello和值world。
使用Hystrix
在spring cloud gateway中可以使用Hystrix。Hystrix是 spring cloud中一个服务熔断降级的组件,在微服务系统有着十分重要的作用。 Hystrix是 spring cloud gateway中是以filter的形式使用的,代码如下:
代码语言:javascript复制 @Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
String httpUri = "http://httpbin.org:80";
return builder.routes()
.route(p -> p
.path("/get")
.filters(f -> f.addRequestHeader("Hello", "World"))
.uri(httpUri))
.route(p -> p
.host("*.hystrix.com")
.filters(f -> f
.hystrix(config -> config
.setName("mycmd")
.setFallbackUri("forward:/fallback")))
.uri(httpUri))
.build();
}
在上面的代码中,我们使用了另外一个router,该router使用host去断言请求是否进入该路由,当请求的host有“*.hystrix.com”,都会进入该router,该router中有一个hystrix的filter,该filter可以配置名称、和指向性fallback的逻辑的地址,比如本案例中重定向到了“/fallback”。
现在写的一个“/fallback”的l逻辑:
代码语言:javascript复制 @RequestMapping("/fallback")
public Mono<String> fallback() {
return Mono.just("fallback");
}
Mono是一个Reactive stream,对外输出一个“fallback”字符串。
使用curl执行以下命令:
代码语言:javascript复制 curl --dump-header - --header 'Host: www.hystrix.com' http://localhost:8080/delay/3
返回的响应为:
代码语言:javascript复制fallback
可见,带hostwww.hystrix.com的请求执行了hystrix的fallback的逻辑。
源码下载
https://github.com/forezp/SpringCloudLearning/tree/master/sc-2020-chapter2
参考:
- Spring Cloud Gateway 初体验:https://www.fangzhipeng.com/springcloud/2018/11/06/sc-f-gateway1.html
- Spring Cloud Gateway 之Predict篇:https://www.fangzhipeng.com/springcloud/2018/12/05/sc-f-gateway2.html
- spring cloud gateway之filter篇:https://www.fangzhipeng.com/springcloud/2018/12/21/sc-f-gatway3.html
- spring cloud gateway 之限流篇:https://www.fangzhipeng.com/springcloud/2018/12/22/sc-f-gatway4.html
- spring cloud gateway之服务注册与发现:https://www.fangzhipeng.com/springcloud/2018/12/23/sc-f-gateway5.html
- 使用spring cloud gateway作为服务网关:https://www.fangzhipeng.com/springcloud/2021/04/03/sc-2020-gateway.html
- Spring Cloud官方案例:https://spring.io/guides/gs/gateway