赞
踩
这篇文章讲述了如何简单地使用Spring Cloud Gateway,来源于Spring Cloud官方案例,地址https://spring.io/guides/gs/gateway 。
Spring Cloud Gateway是Spring Cloud官方推出的第二代网关框架,取代Zuul网关。网关作为流量的,在微服务系统中有着非常作用,网关常见的功能有
本文首先用官方的案例带领大家来体验下Spring Cloud的一些简单的功能,在后续文章我会使用详细的案例和源码解析来详细讲解Spring Cloud Gateway.
本案例的的源码下载于官方案例,也可以在我的Github上下载。工程使用的Spring Boot版本为2.0.5.RELEASE,Spring Cloud版本为Finchley.SR1。
新建一个工程,取名为sc-f-gateway-first-sight在工程的pom文件引用工程所需的依赖,
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.5.RELEASE</version> </parent> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Finchley.SR1</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency>
在spring cloud gateway中使用RouteLocator的Bean进行路由转发,将请求进行处理,最后转发到目标的下游服务。在本案例中,会将请求转发到http://httpbin.org:80这个地址上。代码如下:
@SpringBootApplication @RestController public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @Bean public RouteLocator myRoutes(RouteLocatorBuilder builder) { // /get请求被 转发,并且加了 header 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,
上面创建的route可以让请求
“/get”请求都转发到“http://httpbin.org/get”。
在route配置上,我们添加了一个filter,
该filter会将请求添加一个header,key为hello,value为world。
启动springboot项目,在浏览器上http://localhost:8080/get,浏览器显示如下:
"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中一个服务熔断降级的组件。
Hystrix是 spring cloud gateway中是以filter的形式使用的,代码如下:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
@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,
现在写的一个“/fallback”的l逻辑:
@RequestMapping("/fallback")
public Mono<String> fallback() {
return Mono.just("fallback");
}
Mono是一个Reactive stream,对外输出一个“fallback”字符串。
使用curl执行以下命令:
curl --dump-header - --header 'Host: www.hystrix.com' http://localhost:8080/delay/3
返回的响应为:
fallback
可见,带hostwww.hystrix.com的请求执行了hystrix的fallback的逻辑。
在spring cloud gateway中有2个重要的概念predicates和filters
https://github.com/forezp/SpringCloudLearning/tree/master/sc-f-gateway-first-sight
使用postMan
http://localhost:8080/delay/311111 ,这里不明白为何 /delay 开头的才会跳转,不懂。 header头为:Host 值为:www(开头随意).hystrix.com 随意访问一个请求: 不带host或带的host不正确 http://localhost:8080/delay1/1 { "timestamp": "2022-05-07T07:56:55.622+0000", "path": "/delay1/1", "status": 404, "error": "Not Found", "message": null } 带的host的正确的时候:(说明,host带的正确放行了),下面遇到此情况 Host Route 会跳转 httpbin。 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> <title>404 Not Found</title> <h1>Not Found</h1> <p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p> 访问:http://localhost:8080/delay/1 ,带Host错误的时候。正确的时候 会fallback { "timestamp": "2022-05-07T08:02:16.125+0000", "path": "/delay/1", "status": 404, "error": "Not Found", "message": null } 而下面:Host Route 如访问:http://localhost:8080/delay1/1 ,只要Host带的正确,都会被跳转 httpbin
网关作用如下:
Spring Cloud Gateway作为Spring Cloud框架的第二代网关,在功能上要比Zuul更加的强大,性能也更好。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8kMDBx7q-1652537638682)(https://raw.githubusercontent.com/spring-cloud/spring-cloud-gateway/master/docs/src/main/asciidoc/images/spring_cloud_gateway_diagram.png)]
如上图所示,客户端向Spring Cloud Gateway发出请求。
在上面的处理过程中,有一个重要的点就是讲 请求和 路由进行匹配,这时候就需要用到 predicate,它是决定了一个请求走哪一个路由。
predicate
英 /ˈpredɪkət/ 美 /ˈpredɪkət; ˈpredɪkeɪt/ 全球(英国)
简明 牛津 新牛津 韦氏 柯林斯 例句 百科
n. (语法)谓语;(逻)谓项
v. 使基于,使取决于;表明,断言
adj. (用作)表语的
Predicate来自于java8的接口。
Predicate 接受一个输入参数,返回一个布尔值结果。
该接口包含多种默认方法来 将Predicate组合成其他复杂的逻辑(比如:与,或,非)。
可以用于接口请求参数校验、
判断新老数据是否有变化需要进行更新操作。add–与、or–或、negate–非。
Spring Cloud Gateway内置了许多Predict,
如果读者有兴趣可以阅读一下。现在列举各种Predicate如下图:
在上图中,有很多类型的Predicate,
比如说时间类型的Predicated
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 ;如果有任何问题欢迎和我联系,和我讨论。
创建一个工程,在工程的pom文件引入spring cloud gateway 的起步依赖spring-cloud-starter-gateway,spring cloud版本和spring boot版本,代码如下:
boot 2.0.5
cloud Finchiley.SR1
spring-cloud-starter-gateway
AfterRoutePredicateFactory,可配置一个时间,
在工程的application.yml配置如下:
server: port: 8081 spring: profiles: active: after_route --- spring: cloud: gateway: routes: - id: after_route uri: http://httpbin.org:80/get predicates: - After=2017-01-20T17:42:47.789-07:00[America/Denver] #解释:就是当请求不存在时(存在的请求 不关注) #现在的时间是 2022年,是在 2017年之后的,会转发 http://httpbin.org:80/get #如:现在是 2016年(现在还没到配置的时间),那就不转发。展示:Whitelabel Error Page profiles: after_route
在上面的配置文件中,配置了服务的端口为8081,
配置spring.profiles.active:after_route指定了
在application.yml再建一个配置文件,语法是三个横线,在此配置文件中通过spring.profiles来配置文件名,和spring.profiles.active一致,然后配置spring cloud gateway 相关的配置,
id标签配置的是router的id,每个router都需要一个唯一的id,
uri配置的是将请求路由到哪里,本案例全部路由到http://httpbin.org:80/get。
predicates: After=2017-01-20T17:42:47.789-07:00[America/Denver] 会被解析成
当请求的时间在这个配置的时间之后,请求会被路由到http://httpbin.org:80/get。
启动工程,在浏览器上访问http://localhost:8081/,会显示http://httpbin.org:80/get返回的结果,此时gateway路由到了配置的uri。
跟时间相关的predicates还有
Header Route Predicate Factory需要2个参数,
在工程的配置文件加上以下的配置:
spring:
profiles:
active: header_route
---
spring:
cloud:
gateway:
routes:
- id: header_route
uri: http://httpbin.org:80/get
predicates:
- Header=X-Request-Id, \d+
profiles: header_route
在上面的配置中,当请求的Header中有X-Request-Id的header名,
$ curl -H 'X-Request-Id:1' localhost:8081
执行命令后,会正确的返回请求结果,结果省略。
Cookie Route Predicate Factory需要2个参数,一个时cookie名字,另一个时值,可以为正则表达式。
在配置文件添加以下配置:
spring:
profiles:
active: cookie_route
---
spring:
cloud:
gateway:
routes:
- id: cookie_route
uri: http://httpbin.org:80/get
predicates:
- Cookie=name, forezp
profiles: cookie_route
在上面的配置中,请求带有cookie名为 name, cookie值为forezp 的请求
使用curl命令进行请求,在请求中带上 cookie,会返回正确的结果,否则,请求报404错误。
$ curl -H 'Cookie:name=forezp' localhost:8081
Host Route Predicate Factory需要一个参数即 hostname,它可以使用. * 等去匹配host。
在工程的配置文件,加上以下配置:
spring:
profiles:
active: host_route
---
spring:
cloud:
gateway:
routes:
- id: host_route
uri: http://httpbin.org:80/get
predicates:
- Host=**.fangzhipeng.com
profiles: host_route
在上面的配置中,请求头中含有Host为fangzhipeng.com的请求将会被路由转发转发到配置的uri。 启动工程,执行以下的curl命令,请求会返回正确的请求结果:
curl -H 'Host:www.fangzhipeng.com' localhost:8081
如果没有 此header (并且路径无此接口),为:
{
"timestamp": "2022-05-07T09:14:43.611+0000",
"path": "/test1",
"status": 404,
"error": "Not Found",
"message": null
}
Method Route Predicate Factory 需要一个参数,即请求的类型。
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类型的请求,会得到正确的返回结果。
$ curl localhost:8081
使用 curl命令模拟 post请求,则返回404结果。
$ curl -XPOST localhost:8081
Path Route Predicate Factory 需要一个参数: 一个spel表达式,应用匹配路径。
在工程的配置文件application.yml文件中,做以下的配置:
spring:
profiles:
active: path_route
---
spring:
cloud:
gateway:
routes:
- id: path_route
uri: http://httpbin.org:80/get
predicates:
- Path=/foo/{segment}
profiles: path_route
在上面的配置中,所有的请求路径满足/foo/{segment}的请求将会匹配并被路由,比如/foo/1 、/foo/bar的请求,将会命中匹配,并成功转发。
使用curl模拟一个请求localhost:8081/foo/dew,执行之后会返回正确的请求结果。
$ curl localhost:8081/foo/dew
Query Route Predicate Factory 需要2个参数:一个参数名 和 一个参数值的正则表达式。在工程的配置文件application.yml做以下的配置:
spring:
profiles:
active: query_route
---
spring:
cloud:
gateway:
routes:
- id: query_route
uri: http://httpbin.org:80/get
predicates:
- Query=foo, ba.
profiles: query_route
在上面的配置文件中,配置了请求中含有参数foo,并且foo的值匹配ba.,则请求命中路由,比如一个请求中含有参数名为foo,值的为bar,能够被正确路由转发。
模拟请求的命令如下:
$ curl localhost:8081?foo=bar
Query Route Predicate Factory也可以只填一个参数,填一个参数时,则只匹配参数名,即请求的参数中含有配置的参数名,则命中路由。比如以下的配置中,配置了请求参数中含有参数名为foo 的参数将会被请求转发到uri为http://httpbin.org:80/get。
spring:
cloud:
gateway:
routes:
- id: query_route
uri: http://httpbin.org:80/get
predicates:
- Query=foo
profiles: query_route
在本篇文章中,首先介绍了Spring Cloud Gateway的工作流程和原理,
然后介绍了gateway框架内置的predict及其分类,最后以案例的形式重点讲解了几个重要的Predict。
Predict作为断言,它决定了请求会被路由到哪个router 中。在断言之后,请求会被进入到filter过滤器的逻辑,
源码
https://github.com/forezp/SpringCloudLearning/tree/master/sc-f-gateway-predicate
Predict决定了请求由哪一个路由处理,
由filter工作流程点,可以知道filter有着非常重要的作用,
当我们有很多个服务时,比如下图中的user-service、goods-service、sales-service等服务,客户端请求各个服务的Api时,
每个服务都需要做相同的事情,
对于这样重复的工作,有没有办法做的更好,答案是肯定的。在微服务的上一层加一个
这个Api Gateway服务就是起到一个服务边界的作用,外接的请求访问系统,必须先通过网关层。
Spring Cloud Gateway同zuul类似,
与zuul不同的是,filter除了分为“pre”和“post”两种方式的filter外,在Spring Cloud Gateway中,filter从作用范围可分为另外两种,
过滤器允许以某种方式修改传入的HTTP请求或传出的HTTP响应。
GatewayFilter工厂同上一篇介绍的Predicate工厂类似,都是在配置文件application.yml中配置,遵循了约定大于配置的思想,只需要在配置文件配置GatewayFilter Factory的名称,而不需要写全部的类名,
Spring Cloud Gateway 内置的过滤器工厂一览表如下:
现在挑几个常见的过滤器工厂来讲解,每一个过滤器工厂在官方文档都给出了详细的使用案例,
如果不清楚的还可以在org.springframework.cloud.gateway.filter.factory看每一个过滤器工厂的源码。
创建工程,引入相关的依赖,包括spring boot 版本2.0.5,spring Cloud版本Finchley,gateway依赖如下:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
在工程的配置文件中,加入以下的配置:
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 // 响应 增加 header --- spring: cloud: gateway: routes: - id: add_response_header_route uri: http://httpbin.org:80/get filters: - AddResponseHeader=X-Response-Foo, Bar predicates: - After=2017-01-20T17:42:47.789-07:00[America/Denver] profiles: add_response_header_route
在上述的配置中,工程的启动端口为8081,配置文件为add_request_header_route,
在add_request_header_route配置中,
为了验证AddRequestHeaderGatewayFilterFactory是怎么样工作的,查看它的源码,AddRequestHeaderGatewayFilterFactory的源码如下:
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 ,
启动工程,通过curl命令来模拟请求:
curl localhost:8081
最终显示了从 http://httpbin.org:80/get得到了请求,响应如下:
{
"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过滤器工厂生效。
跟AddRequestHeader过滤器工厂类似的还有AddResponseHeader过滤器工厂,在此就不再重复。
在Nginx服务启中有一个非常强大的功能就是重写路径,
Spring Cloud Gateway默认也提供了这样的功能,这个功能是Zuul没有的。在配置文件中加上以下的配置:
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过滤器工厂,
Spring Cloud Gateway内置了19种强大的过滤器工厂,能够满足很多场景的需求,那么能不能自定义自己的过滤器呢,当然是可以的。在spring Cloud Gateway中,过滤器需要实现GatewayFilter和Ordered2个接口。写一个RequestTimeFilter,代码如下:
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) { //相当于”post”过滤器 log.info(exchange.getRequest().getURI().getRawPath() + ": " + (System.currentTimeMillis() - startTime) + "ms"); } }) ); } //设定优先级别的,值越大则优先级越低 @Override public int getOrder() { return 0; } }
在上面的代码中,Ordered中的int getOrder()
@Bean public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) { // @formatter:off return builder.routes() //customer开头的请求,增加RequestTimeFilter ,添加:ResponseHeader .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命令模拟请求:
curl localhost:8081/customer/123
在程序的控制台输出一下的请求信息的日志:
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,我们可以直接
-------------------------文字重复了-----------------------------------
有2个两个较接近具体实现的抽象类,
这2个类前者接收一个参数,比如它的实现类RedirectToGatewayFilterFactory;
后者接收2个参数,比如它的实现类
现在需要将请求的日志打印出来,需要使用一个参数,这时可以参照RedirectToGatewayFilterFactory的写法。
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() { //一定要调用下父类的构造器把Config类型传过去 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()); } }) ); }; } //静态内部类类Config就是为了接收那个boolean类型的参数服务的 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。
@Bean
public RequestTimeGatewayFilterFactory elapsedGatewayFilterFactory() {
return new RequestTimeGatewayFilterFactory();
}
然后可以在配置文件中配置如下:
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,可以在控制台上看到,日志输出了请求消耗的时间和请求参数。
Spring Cloud Gateway根据作用范围划分为GatewayFilter和GlobalFilter,二者区别如下:
Spring Cloud Gateway框架内置的GlobalFilter如下:
上图中每一个GlobalFilter都作用在每一个router上,能够满足大多数的需求。但是如果遇到业务上的定制,可能需要编写满足自己需求的GlobalFilter。在下面的案例中将讲述如何编写自己GlobalFilter,该GlobalFilter会校验请求中是否包含了请求参数“token”,如何不包含请求参数“token”则不转发路由,否则执行正常的逻辑。代码如下:
public class TokenFilter implements GlobalFilter, Ordered { Logger logger=LoggerFactory.getLogger( TokenFilter.class ); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { //根据ServerWebExchange获取ServerHttpRequest String token = exchange.getRequest().getQueryParams().getFirst("token"); // if (token == null || token.isEmpty()) { logger.info( "token is empty..." ); //否含有参数token,如果没有则完成请求,终止转发 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容器中,代码如下:
@Bean
public TokenFilter tokenFilter(){
return new TokenFilter();
}
启动工程,使用curl命令请求:
curl localhost:8081/customer/123
可以看到请没有被转发,请求被终止,并在控制台打印了如下日志:
2018-11-16 15:30:13.543 INFO 19372 --- [ctor-http-nio-2] gateway.TokenFilter : token is empty...
上面的日志显示了请求进入了没有传“token”的逻辑。
本篇文章讲述了Spring Cloud Gateway中的过滤器,
包括GatewayFilter和GlobalFilter。
从官方文档的内置过滤器讲起,然后讲解自定义GatewayFilter、GatewayFilterFactory以及自定义的GlobalFilter。
有很多内置的过滤器并没有讲述到,比如限流过滤器,这个我觉得是比较重要和大家关注的过滤器,将在之后的文章讲述。
源码下载
https://github.com/forezp/SpringCloudLearning/tree/master/sc-f-gateway-predicate
在高并发的系统中,往往需要在系统中做限流,
常见的限流方式,
一般限流都是在网关这一层做,比如Nginx、Openresty、kong、zuul、Spring Cloud Gateway等;
本文详细探讨在 Spring Cloud Gateway 中如何实现限流。
计数器算法采用计数器 实现限流有点简单粗暴,一般我们会限制
atomic
英 /əˈtɒmɪk/ 美 /əˈtɑːmɪk/ 全球(美国)
简明 牛津 新牛津 韦氏 柯林斯 例句 百科
adj. 原子的,与原子有关的;原子能的,核能的
漏桶算法为了消除”突刺现象”,可以采用漏桶算法实现限流,漏桶算法这个名字就很形象,
在算法实现方面,可以准备一个队列,用来保存请求,另外通过一个线程池(ScheduledExecutorService)来定期从队列中获取请求并执行,可以一次性获取多个并发执行。
这种算法,在使用过后也存在弊端:无法应对短时间的突发流量。
从某种意义上讲,令牌桶算法是对漏桶算法的一种改进,桶算法能够限制请求调用的速率,
实现思路:可以准备一个队列,用来保存令牌,另外通过一个线程池定期生成令牌放到队列中,每来一个请求,就从队列中获取一个令牌,并继续执行。
在Spring Cloud Gateway中,有Filter过滤器,因此可以在“pre”类型的Filter中自行实现上述三种过滤器。但是限流作为网关最基本的功能,Spring Cloud Gateway官方就提供了
limiter
英 /ˈlɪmɪtə(r)/ 美 /lɪmətər/ 全球(英国)
简明 新牛津 柯林斯 例句 百科
n. [机][电] 限制器,[电子] 限幅器;限制者
具体源码不打算在这里讲述,读者可以自行查看,代码量较少,先以案例的形式来讲解如何在Spring Cloud Gateway中使用内置的限流过滤器工厂来实现限流。
首先在工程的pom文件中引入gateway的起步依赖和redis的reactive依赖,代码如下:
<artifactId>spring-cloud-starter-gateway</artifactId>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifatId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
在配置文件中做以下的配置:
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}' # 用于限流的键的解析器的 Bean 对象的名字 redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充平均速率 redis-rate-limiter.burstCapacity: 3 # 令牌桶总容量 application: name: gateway-limiter redis: host: localhost port: 6379 database: 0
在上面的配置文件,指定程序的端口为8081,配置了 redis的信息,并配置了RequestRateLimiter的限流过滤器,该过滤器需要配置三个参数:
replenish 英 /rɪˈplenɪʃ/ 美 /rɪˈplenɪʃ/ 全球(英国) 简明 牛津 新牛津 韦氏 柯林斯 例句 百科 v. 补充,重新装满;补足(原有的量) burst 英 /bɜːst/ 美 /bɜːrst/ 全球(美国) 简明 牛津 新牛津 韦氏 柯林斯 例句 百科 v. 爆炸,爆裂,胀开;冲,闯,突然出现;猛然打开;充满,满怀(be bursting with);决堤;突然活跃起来;突然发生,突然发作;分页,断纸 n. 突发,迸发;爆裂,裂口;一阵短促的射击 capacity 英 /kəˈpæsəti/ 美 /kəˈpæsəti/ 全球(英国) 简明 牛津 新牛津 韦氏 柯林斯 例句 百科 n. 能力,才能;容积,容纳能力;职位,职责;功率,容积;生产量,生产能力 adj. 无虚席的,满场的
KeyResolver需要实现resolve方法,
实现完KeyResolver之后,需要将这个类的Bean注册到Ioc容器中。
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代码如下:
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();
}
也可以以用户的维度去限流:
@Bean
KeyResolver userKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
}
用jmeter进行压测,配置10thread去循环请求localhost:8081,循环间隔1s。从压测的结果上看到有部分请求通过,由部分请求失败。通过redis客户端去查看redis中存在的key。如下:
可见,RequestRateLimiter是使用Redis来进行限流的,并在redis中存储了2个key。关注这两个key含义可以看lua源代码。
https://github.com/forezp/SpringCloudLearning/tree/master/sc-f-gateway-limiter
Gateway的Predict(断言)、Filter(过滤器)
如何配合服务注册中心进行路由转发
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,
eureka-server、service-hi这两个工程直接复制于我的另外一篇文章https://blog.csdn.net/forezp/article/details/81040925 ,在这就不在重复,可以查看源码,源码地址见文末链接。 其中,service-hi服务对外暴露了一个RESTFUL接口“/hi”接口。现在重点讲解service-gateway。
在gateway工程中引入项目所需的依赖,包括eureka-client的起步依赖和gateway的起步依赖,代码如下:
<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的配置等信息,配置信息如下:
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,
spring.cloud.gateway.discovery.locator.lowerCaseServiceId
在浏览器上请求输入localhost:8081/service-hi/hi?name=1323,网页获取以下的响应:
hi 1323 ,i am from port:8762
在上面的例子中,向gateway-service发送的请求时,url必须带上服务名service-hi这个前缀,才能转发到service-hi上,转发之前会将service-hi去掉。
那么我能不能自定义请求路径呢,
毕竟根据服务名有时过于太长,或者历史的原因不能根据服务名去路由,需要由自定义路径并转发到具体的服务上。
答案是肯定的是可以的,只需要修改工程的配置文件application.yml,具体配置如下:
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
Prefix的filter 在转发之前将/demo去掉。
同时将spring.cloud.gateway.discovery.locator.enabled改为false,
在浏览器上请求localhost:8081/demo/hi?name=1323,浏览器返回以下的响应:
hi 1323 ,i am from port:8762
返回的结果跟我们预想的一样。
https://github.com/forezp/SpringCloudLearning/tree/master/sc-f-gateway-cloud
个人博客纯净版:https://www.fangzhipeng.com/springcloud/2018/08/01/sc-f1-eureka.html
一、spring cloud简介
鉴于《史上最简单的Spring Cloud教程》很受读者欢迎,再次我特意升级了一下版本,目前支持的版本为Spring Boot版本2.0.3.RELEASE,Spring Cloud版本为Finchley.RELEASE。
Finchley版本的官方文档如下:
http://cloud.spring.io/spring-cloud-static/Finchley.RELEASE/single/spring-cloud.html
spring cloud 为开发人员提供了快速构建分布式系统的一些工具,包括配置管理、服务发现、断路器、路由、微代理、事件总线、全局锁、决策竞选、分布式会话等等。它运行环境简单,可以在开发人员的电脑上跑。另外说明spring cloud是基于springboot的,所以需要开发中对springboot有一定的了解,如果不了解的话可以看这篇文章:2小时学会springboot。另外对于“微服务架构” 不了解的话,可以通过搜索引擎搜索“微服务架构”了解下。
二、创建服务注册中心
在这里,我还是采用Eureka作为服务注册与发现的组件,至于Consul 之后会出文章详细介绍。
2.1 首先创建一个maven主工程。
首先创建一个主Maven工程,在其pom文件引入依赖,spring Boot版本为2.0.3.RELEASE,Spring Cloud版本为Finchley.RELEASE。这个pom文件作为父pom文件,起到依赖版本控制的作用,其他module工程继承该pom。这一系列文章全部采用这种模式,其他文章的pom跟这个pom一样。再次说明一下,以后不再重复引入。代码如下:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> //gav <groupId>com.forezp</groupId> <artifactId>sc-f-chapter1</artifactId> <version>0.0.1-SNAPSHOT</version> //pnd <packaging>pom</packaging> <name>sc-f-chapter1</name> <description>Demo project for Spring Boot</description> <parent>//boot 2.0.3 <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.3.RELEASE</version> <relativePath/> </parent> <modules>//子类 <module>eureka-server</module> <module>service-hi</module> </modules> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <spring-cloud.version>Finchley.RELEASE</spring-cloud.version> //F版本 </properties> <dependencies> <dependency> //测试引用 <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency>//cloud引用 <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin>//maven 插件 <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
**2.2 然后创建2个model工程:**一个model工程作为服务注册中心,即Eureka Server,另一个作为Eureka Client。
下面以创建server为例子,详细说明创建过程:
右键工程->创建model-> 选择spring initialir 如下图:
下一步->选择cloud discovery->eureka server ,然后一直下一步就行了。
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.forezp</groupId> <artifactId>eureka-server</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> //jar <name>eureka-server</name> <description>Demo project for Spring Boot</description> <parent>//父类 <groupId>com.forezp</groupId> <artifactId>sc-f-chapter1</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <dependencies> <dependency> //eureka-server <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> </dependencies> </project>
2.3 启动一个服务注册中心,只需要一个注解@EnableEurekaServer,这个注解需要在springboot工程的启动application类上加:
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run( EurekaServerApplication.class, args );
}
}
**2.4 **eureka是一个高可用的组件,它没有后端缓存,每一个实例注册之后需要向注册中心发送心跳(因此可以在内存中完成),在默认情况下erureka server也是一个eureka client ,必须要指定一个 server。eureka server的配置文件appication.yml:
server:
port: 8761
eureka:
instance:
hostname: localhost
client:
registerWithEureka: false #不注册到其他
fetchRegistry: false #不抓取 注册信息
serviceUrl: #eureka的defaultZone ,localhost:8761/eureka
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
spring:
application:
name: eurka-server
通过eureka.client.registerWithEureka:false和fetchRegistry:false来表明自己是一个eureka server.
2.5 eureka server 是有界面的,启动工程,打开浏览器访问:
http://localhost:8761 ,界面如下:
No application available 没有服务被发现 ……_
因为没有注册服务当然不可能有服务被发现了。
三、创建一个服务提供者 (eureka client)
当client向server注册时,它会提供一些元数据,例如主机和端口,URL,主页等。Eureka server 从每个client实例接收心跳消息。 如果心跳超时,则通常将该实例从注册server中删除。
创建过程同server类似,创建完pom.xml如下:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.forezp</groupId> <artifactId>service-hi</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>service-hi</name> <description>Demo project for Spring Boot</description> <parent> <groupId>com.forezp</groupId> <artifactId>sc-f-chapter1</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <dependencies> <dependency> //eureka-client <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> //web <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
通过注解@EnableEurekaClient 表明自己是一个eurekaclient.
@SpringBootApplication @EnableEurekaClient @RestController public class ServiceHiApplication { public static void main(String[] args) { SpringApplication.run( ServiceHiApplication.class, args ); } @Value("${server.port}") String port; @RequestMapping("/hi") public String home(@RequestParam(value = "name", defaultValue = "forezp") String name) { return "hi " + name + " ,i am from port:" + port; } }
仅仅@EnableEurekaClient是不够的,还需要在配置文件中注明自己的服务注册中心的地址,application.yml配置文件如下:
server:
port: 8762
spring:
application:
name: service-hi
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
需要指明spring.application.name,这个很重要,这在以后的服务与服务之间相互调用一般都是根据这个name 。
启动工程,打开http://localhost:8761 ,即eureka server 的网址:
你会发现一个服务已经注册在服务中了,服务名为SERVICE-HI ,端口为7862
这时打开 http://localhost:8762/hi?name=forezp ,你会在浏览器上看到 :
hi forezp,i am from port:8762
源码下载:https://github.com/forezp/SpringCloudLearning/tree/master/sc-f-chapter1
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。