1. Spring Cloud Gateway 初体验

这篇文章讲述了如何简单地使用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文件引用工程所需的依赖,

  • 包括spring boot和spring cloud,
  • 以及gateway的起步依赖spring-cloud-starter-gateway,代码如下:
   <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可以让你添加各种predicatesfilters

  • predicates断言的意思,顾名思义就是根据具体的请求的规则,由具体的route去处理,
  • 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

Hystrix是 spring cloud中一个服务熔断降级的组件。

Hystrix是 spring cloud gateway中是以filter的形式使用的,代码如下:

引入pom

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

配置Routes

 	@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逻辑:

	@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个重要的概念predicatesfilters

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

2. Predict

网关作用如下:

  • 协议转换,路由转发
  • 流量聚合,对流量进行监控,日志输出
  • 作为整个系统的前端工程,对流量进行控制,有限流的作用
  • 作为系统的前端边界,外部流量只能通过网关才能访问系统
  • 可以在网关层做权限的判断
  • 可以在网关层做缓存

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发出请求。

  • 如果Gateway Handler Mapping确定请求与路由匹配(这个时候就用到predicate),则将其发送到Gateway web handler处理。
  • Gateway web handler处理请求时会经过一系列的过滤器链。
    • 过滤器链被虚线划分的原因是过滤器链可以在 发送代理请求之前 或 之后执行过滤逻辑。
    • 先执行所有“pre”过滤器逻辑,然后进行代理请求。
      • 在发出代理请求之后,收到代理服务的响应之后执行“post”过滤器逻辑。这跟zuul的处理过程很类似。
    • 在执行所有“pre”过滤器逻辑时,
      • 往往进行了鉴权、限流、日志输出等功能,以及请求头的更改、协议的转换;
    • 转发之后收到响应之后,会执行所有“post”过滤器的逻辑,
      • 在这里可以响应数据进行了修改,比如响应头、协议的转换等。

在上面的处理过程中,有一个重要的点就是讲 请求和 路由进行匹配,这时候就需要用到 predicate,它是决定了一个请求走哪一个路由。

predicate 
英 /ˈpredɪkət/  美 /ˈpredɪkət; ˈpredɪkeɪt/  全球(英国)  
简明 牛津 新牛津  韦氏  柯林斯 例句  百科
n. (语法)谓语;(逻)谓项
v. 使基于,使取决于;表明,断言
adj. (用作)表语的

predicate简介

Predicate来自于java8的接口。

Predicate 接受一个输入参数,返回一个布尔值结果。

该接口包含多种默认方法来 将Predicate组合成其他复杂的逻辑(比如:与,或,非)。

​ 可以用于接口请求参数校验、

​ 判断新老数据是否有变化需要进行更新操作。add–与、or–或、negate–非。

Spring Cloud Gateway内置了许多Predict,

  • 这些Predict的源码在org.springframework.cloud.gateway.handler.predicate包中,

如果读者有兴趣可以阅读一下。现在列举各种Predicate如下图:

img

在上图中,有很多类型的Predicate,

  • 比如说时间类型的Predicated

    • (AfterRoutePredicateFactory BeforeRoutePredicateFactory BetweenRoutePredicateFactory),当只有满足特定时间要求的请求会进入到此predicate中,并交由router处理;
  • cookie类型的CookieRoutePredicateFactory,指定的cookie满足正则匹配,才会进入此router;

  • 以及host、method、path、querparam、remoteaddr类型的predicate,每一种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

2.1 After Route

AfterRoutePredicateFactory,可配置一个时间,

  • 当请求的时间在配置时间之后,才交给 router去处理。否则则报错,不通过路由。
配置

在工程的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指定了

  • 程序的spring的启动文件为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] 会被解析成

    • PredicateDefinition对象 (name =After ,args= 2017-01-20T17:42:47.789-07:00[America/Denver])。
    • 在这里需要注意的是predicates的After这个配置,遵循的契约大于配置的思想,它实际被AfterRoutePredicateFactory这个类所处理,
    • 这个After就是指定了它的Gateway web handler类为AfterRoutePredicateFactory,同理,其他类型的predicate也遵循这个规则。
测试

请求的时间在这个配置的时间之后,请求会被路由到http://httpbin.org:80/get。

  • 如果是 正常存在的请求,不影响

启动工程,在浏览器上访问http://localhost:8081/,会显示http://httpbin.org:80/get返回的结果,此时gateway路由到了配置的uri。

  • 如果我们将 配置的时间设置到(如:2033) 当前时之后(当前2022年,配置不生效),
  • 浏览器会显示404,此时证明没有路由到配置的uri.

跟时间相关的predicates还有

  • Before Route Predicate Factory、
  • Between Route Predicate Factory,读者可以自行查阅官方文档,再次不再演示。

2.2 Header Route

Header Route Predicate Factory需要2个参数,

  • 一个是header名,另外一个header值,该值可以是一个正则表达式。
  • 当此断言匹配了请求的header名和值时,断言通过,进入到router的规则中去。

在工程的配置文件加上以下的配置:

配置
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名,

  • 且header值为数字时,请求会被路由到配置的 uri. 使用curl执行以下命令:
$ curl -H 'X-Request-Id:1' localhost:8081

执行命令后,会正确的返回请求结果,结果省略。

  • 如果在请求中没有带上X-Request-Id的header名,并且值不为数字时,请求就会报404,路由没有被正确转发。
  • 同理 如果带了 这个header,并且值为 数字,就会转发
  • 正常的能请求通的(存在的) 接口,不影响

2.3 Cookie Route

Cookie Route Predicate Factory需要2个参数,一个时cookie名字,另一个时值,可以为正则表达式。

  • 它用于匹配请求中,带有该名称的cookie 和 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 的请求

  • 将都会转发到uri为 http://httpbin.org:80/get的地址上。

使用curl命令进行请求,在请求中带上 cookie,会返回正确的结果,否则,请求报404错误。

$ curl -H 'Cookie:name=forezp' localhost:8081
  • 使用postMan增加一个 这样的header 即可,key为:Cookie

2.4 Host Route

Host Route Predicate Factory需要一个参数即 hostname,它可以使用. * 等去匹配host。

  • 这个参数会匹配请求头中的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
}

2.5 Method Route

Method Route Predicate Factory 需要一个参数,即请求的类型。

  • 比如GET类型的请求都转发到此路由。在工程的配置文件加上以下的配置:
配置

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

2.6 Path Route 重要

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

2.7 Query Route

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过滤器的逻辑,

    • 下篇文章将会为大家介绍Spring Cloud Gateway过滤器相关的内容。
  • 源码
    https://github.com/forezp/SpringCloudLearning/tree/master/sc-f-gateway-predicate

3. filter

Predict决定了请求由哪一个路由处理,

  • 在路由处理之前,需要经过“pre”类型的过滤器处理,处理返回响应之后,
  • 可以由“post”类型的过滤器处理。

3.1 filter的作用和生命周期

由filter工作流程点,可以知道filter有着非常重要的作用,

  • 在“pre”类型的过滤器可以做
    • 参数校验、
    • 权限校验、
    • 流量监控、
    • 日志输出、
    • 协议转换等,
  • 在“post”类型的过滤器中可以做
    • 响应内容、
    • 响应头的修改,
    • 日志的输出,
    • 流量监控等。
  • 首先需要弄清一点为什么需要网关这一层,这就不得不说下filter的作用了。

作用

当我们有很多个服务时,比如下图中的user-service、goods-service、sales-service等服务,客户端请求各个服务的Api时,

每个服务都需要做相同的事情,

  • 比如鉴权、限流、日志输出等。

img

对于这样重复的工作,有没有办法做的更好,答案是肯定的。在微服务的上一层加一个

  • 全局的权限控制、限流、日志输出的Api Gatewat服务,然后再将请求转发到具体的业务服务层。

这个Api Gateway服务就是起到一个服务边界的作用,外接的请求访问系统,必须先通过网关层。

img

生命周期

Spring Cloud Gateway同zuul类似,

  • 有“pre”和“post”两种方式的filter。
  • 客户端的请求先经过“pre”类型的filter,然后将请求转发到具体的业务服务,比如上图中的user-service,
  • 收到业务服务的响应之后,再经过“post”类型的filter处理,最后返回响应到客户端。

img

与zuul不同的是,filter除了分为“pre”和“post”两种方式的filter外,在Spring Cloud Gateway中,filter从作用范围可分为另外两种,

  • 一种是针对于单个路由的gateway filter,它在配置文件中的写法同 predict类似;
  • 另外一种是针对于所有路由的 global gateway filer。现在从作用范围划分的维度来讲解这两种filter。

3.2 gateway filter

过滤器允许以某种方式修改传入的HTTP请求或传出的HTTP响应

  • 过滤器可以限定作用在某些特定请求路径上。
  • Spring Cloud Gateway包含许多内置的GatewayFilter工厂。

GatewayFilter工厂同上一篇介绍的Predicate工厂类似,都是在配置文件application.yml中配置,遵循了约定大于配置的思想,只需要在配置文件配置GatewayFilter Factory的名称,而不需要写全部的类名,

  • 比如AddRequestHeaderGatewayFilterFactory只需要在配置文件中写AddRequestHeader,
  • 而不是全部类名。
  • 在配置文件中配置的GatewayFilter Factory最终都会相应的器工厂类处理。

Spring Cloud Gateway 内置的过滤器工厂一览表如下:

img

现在挑几个常见的过滤器工厂来讲解,每一个过滤器工厂在官方文档都给出了详细的使用案例,

如果不清楚的还可以在org.springframework.cloud.gateway.filter.factory看每一个过滤器工厂的源码。

AddRequestHeader 和 响应的

  • AddRequestHeader GatewayFilter 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配置中,

  • 配置了roter的id为add_request_header_route,
  • 路由地址为http://httpbin.org:80/get,
    • 该router有AfterPredictFactory,
  • 有一个filter为AddRequestHeaderGatewayFilterFactory(约定写成AddRequestHeader),
    • AddRequestHeader过滤器工厂会在请求头加上一对请求头,
    • 名称为X-Request-Foo,值为Bar。

为了验证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 ,

  • 在新的ServerHttpRequest加了一个请求头,
  • 然后创建新的 ServerWebExchange ,提交过滤器链继续过滤。

启动工程,通过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过滤器工厂,在此就不再重复。

RewritePath

  • RewritePath GatewayFilter Factory

在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过滤器工厂,

  • 此工厂将/foo/(?.)重写为{segment},然后转发到https://blog.csdn.net。
    • 比如在网页上请求localhost:8081/foo/forezp,
      • 此时会将请求转发到https://blog.csdn.net/forezp的页面,
    • 比如在网页上请求localhost:8081/foo/forezp/1,页面显示404,
      • 就是因为不存在https://blog.csdn.net/forezp/1这个页面。

自定义过滤器

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()

  • 方法是来给过滤器设定优先级别的,值越大则优先级越低。
  • 还有有一个filterI(exchange,chain)方法,在该方法中,
    • 先记录了请求的开始时间,并保存在ServerWebExchange中,此处是一个“pre”类型的过滤器,
    • 然后再chain.filter的内部类中的run()方法中相当于”post”过滤器,在此处打印了请求所消耗的时间。
  • 然后将该过滤器注册到router中,代码如下:
注册到流程里
    @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

3.3 自定义过滤器工厂

在上面的自定义过滤器中,有没有办法自定义过滤器工厂类呢?

  • 这样就可以在配置文件中配置过滤器了。
  • 现在需要实现一个过滤器工厂,在打印时间的时候,可以设置参数来决定是否打印请参数。

查看GatewayFilterFactory的源码,可以发现GatewayFilterfactory的层级如下:

img

过滤器工厂的顶级接口是GatewayFilterFactory,我们可以直接

  • 继承它的两个抽象类来简化开发AbstractGatewayFilterFactory和AbstractNameValueGatewayFilterFactory,
  • 这两个抽象类的区别就是前者接收一个参数(像StripPrefix和我们创建的这种),后者接收两个参数(像AddResponseHeader)。

-------------------------文字重复了-----------------------------------

有2个两个较接近具体实现的抽象类,

  • 这2个类前者接收一个参数,比如它的实现类RedirectToGatewayFilterFactory;

  • 后者接收2个参数,比如它的实现类

    • AddRequestHeaderGatewayFilterFactory类。

现在需要将请求的日志打印出来,需要使用一个参数,这时可以参照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

 @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,可以在控制台上看到,日志输出了请求消耗的时间和请求参数。

3.4 global filter

Spring Cloud Gateway根据作用范围划分为GatewayFilter和GlobalFilter,二者区别如下:

  • GatewayFilter : 需要通过spring.cloud.routes.filters 配置在具体路由下,只作用在当前路由上或通过spring.cloud.default-filters配置在全局,作用在所有路由上
  • GlobalFilter : 全局过滤器,不需要在配置文件中配置,作用在所有的路由上,最终通过GatewayFilterAdapter包装成GatewayFilterChain可识别的过滤器,它为请求业务以及路由的URI转换为真实业务服务的请求地址的核心过滤器,不需要配置,系统初始化时加载,并作用在每个路由上。

Spring Cloud Gateway框架内置的GlobalFilter如下:

img

上图中每一个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

@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”的逻辑。

  • 有了token参数,才可以访问:http://localhost:8081/customer/123?token=1232132

3.5 总结

本篇文章讲述了Spring Cloud Gateway中的过滤器,

  • 包括GatewayFilter和GlobalFilter。

  • 从官方文档的内置过滤器讲起,然后讲解自定义GatewayFilter、GatewayFilterFactory以及自定义的GlobalFilter。

  • 有很多内置的过滤器并没有讲述到,比如限流过滤器,这个我觉得是比较重要和大家关注的过滤器,将在之后的文章讲述。

  • 源码下载

https://github.com/forezp/SpringCloudLearning/tree/master/sc-f-gateway-predicate

4. 限流

在高并发的系统中,往往需要在系统中做限流,

  • 一方面是为了防止大量的请求使服务器过载,导致服务不可用,
  • 另一方面是为了防止网络攻击。

常见的限流方式,

  • 比如Hystrix适用线程池隔离,超过线程池的负载,走熔断的逻辑。
  • 在一般应用服务器中,比如tomcat容器也是通过限制它的线程数来控制并发的;
  • 也有通过时间窗口的平均速度来控制流量。
    • 常见的限流纬度有比如通过
      • Ip来限流、
      • 通过uri来限流、
      • 通过用户访问频次来限流。

一般限流都是在网关这一层做,比如Nginx、Openresty、kong、zuul、Spring Cloud Gateway等;

  • 也可以在应用层通过Aop这种方式去做限流。

本文详细探讨在 Spring Cloud Gateway 中如何实现限流。

常见的限流算法

计数器算法

计数器算法采用计数器 实现限流有点简单粗暴,一般我们会限制

  • 一秒钟的能够通过的请求数,比如限流qps为100,
  • 算法的实现思路就是从第一个请求进来开始计时,在接下去的1s内,每来一个请求,就把计数加1,
    • 如果累加的数字达到了100,那么后续的请求就会被全部拒绝。
    • 等到1s结束后,把计数恢复成0,重新开始计数。
  • 具体的实现可以是这样的:对于每次服务调用,
    • 可以通过AtomicLong#incrementAndGet()方法来给计数器加1并返回最新值,
    • 通过这个最新值和阈值进行比较。
    • 这种实现方式,相信大家都知道有一个弊端:
      • 如果我在单位时间1s内的前10ms,已经通过了100个请求,那后面的990ms,
      • 只能眼巴巴的把请求拒绝,我们把这种现象称为“突刺现象”
atomic 
英 /əˈtɒmɪk/  美 /əˈtɑːmɪk/  全球(美国)  
简明 牛津 新牛津  韦氏  柯林斯 例句  百科
adj. 原子的,与原子有关的;原子能的,核能的

漏桶算法

漏桶算法为了消除”突刺现象”,可以采用漏桶算法实现限流,漏桶算法这个名字就很形象,

  • 算法内部有一个容器,类似生活用到的漏斗,
  • 当请求进来时,相当于水倒入漏斗,然后从下端小口慢慢匀速的流出。
  • 不管上面流量多大,下面流出的速度始终保持不变。
    • 不管服务调用方多么不稳定,通过漏桶算法进行限流,每10毫秒处理一次请求。
    • 因为处理的速度是固定的,
    • 请求进来的速度是未知的,可能突然进来很多请求,没来得及处理的请求就先放在桶里,
    • 既然是个桶,肯定是有容量上限,如果桶满了,那么新进来的请求就丢弃。

img

在算法实现方面,可以准备一个队列,用来保存请求,另外通过一个线程池(ScheduledExecutorService)来定期从队列中获取请求并执行,可以一次性获取多个并发执行。

这种算法,在使用过后也存在弊端:无法应对短时间的突发流量。

令牌桶算法

从某种意义上讲,令牌桶算法是对漏桶算法的一种改进,桶算法能够限制请求调用的速率,

  • 而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。
  • 在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。
    • 算法中存在一种机制,以一定的速率往桶中放令牌。
    • 每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,
      • 否则选择选择等待可用的令牌、或者直接拒绝。
    • 放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌,
      • 所以就存在这种情况,桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,
    • 比如设置qps为100,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,这时服务还没完全启动好,
      • 等启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。
      • 所以,只有桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率执行。

img

实现思路:可以准备一个队列,用来保存令牌,另外通过一个线程池定期生成令牌放到队列中,每来一个请求,就从队列中获取一个令牌,并继续执行。

Spring Cloud Gateway限流

lua脚本

在Spring Cloud Gateway中,有Filter过滤器,因此可以在“pre”类型的Filter中自行实现上述三种过滤器。但是限流作为网关最基本的功能,Spring Cloud Gateway官方就提供了

  • RequestRateLimiterGatewayFilterFactory这个类,适用Redis和lua脚本实现了令牌桶的方式。
  • 具体实现逻辑在RequestRateLimiterGatewayFilterFactory类中,lua脚本在如下图所示的文件夹中:
limiter 
英 /ˈlɪmɪtə(r)/  美 /lɪmətər/  全球(英国)  
简明 新牛津  柯林斯 例句  百科
n. [机][电] 限制器,[电子] 限幅器;限制者

img

具体源码不打算在这里讲述,读者可以自行查看,代码量较少,先以案例的形式来讲解如何在Spring Cloud Gateway中使用内置的限流过滤器工厂来实现限流。

首先在工程的pom文件中引入gateway的起步依赖和redis的reactive依赖,代码如下:

引入pom

<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的限流过滤器,该过滤器需要配置三个参数:

  • burstCapacity,令牌桶总容量。
  • replenishRate,令牌桶每秒填充平均速率。
  • key-resolver,用于 限流的键 的解析器的 Bean对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。
    
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方法,

  • 比如根据Hostname进行限流,则需要用hostAddress去判断。

实现完KeyResolver之后,需要将这个类的Bean注册到Ioc容器中。

HostAddress 限流

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限流

可以根据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。如下:

微信截图_20181205172625.png

可见,RequestRateLimiter是使用Redis来进行限流的,并在redis中存储了2个key。关注这两个key含义可以看lua源代码。

  • 源码下载

https://github.com/forezp/SpringCloudLearning/tree/master/sc-f-gateway-limiter

5. gateway服务注册与发现

Gateway的Predict(断言)、Filter(过滤器)

如何配合服务注册中心进行路由转发

工程介绍

boot的版本为2.0.3.RELEASE,spring cloud版本为Finchley.RELEASE。在中涉及到了三个工程, 分别为注册中心eureka-server、服务提供者service-hi、 服务网关service-gateway,如下:

工程名端口作用
eureka-server8761注册中心eureka server
service-hi8762服务提供者 eurka client
service-gateway8081路由网关 eureka client

这三个工程中,其中service-hi、service-gateway向注册中心eureka-server注册。

用户的请求首先经过service-gateway,

  • 根据路径由gateway的predict 去断言进到哪一个 router,
  • router经过各种过滤器处理后,最后路由到具体的业务服务,比如 service-hi。如图:

img

eureka-server、service-hi这两个工程直接复制于我的另外一篇文章https://blog.csdn.net/forezp/article/details/81040925 ,在这就不在重复,可以查看源码,源码地址见文末链接。 其中,service-hi服务对外暴露了一个RESTFUL接口“/hi”接口。现在重点讲解service-gateway。

gateway工程详细介绍

引入pom

在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>

yml配置

在工程的配置文件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,

  • 表明gateway开启服务注册和发现的功能,
  • 并且spring cloud gateway自动根据服务发现为每一个服务创建了一个router,
    • 这个router将以服务名开头的请求路径转发到对应的服务

spring.cloud.gateway.discovery.locator.lowerCaseServiceId

  • 是将请求路径上的服务名配置为小写
    • (因为服务注册的时候,向注册中心注册时将服务名转成大写的了),
    • 比如以/service-hi/*的请求路径被路由转发到服务名为service-hi的服务上。

在浏览器上请求输入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/service-hi/hi?name=1323这样的请求地址也能正常访问,
  • 因为这时为每个服务创建了2个router。

在浏览器上请求localhost:8081/demo/hi?name=1323,浏览器返回以下的响应:

hi 1323 ,i am from port:8762

返回的结果跟我们预想的一样。

  • 源码下载

https://github.com/forezp/SpringCloudLearning/tree/master/sc-f-gateway-cloud

6. 第一篇: Eureka(Finchley版本)

  • 服务的注册与发现

个人博客纯净版: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一样。再次说明一下,以后不再重复引入。代码如下:

父类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 如下图:
img

下一步->选择cloud discovery->eureka server ,然后一直下一步就行了。

img

eureka的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>

    <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>

开启eurekaServer

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:

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 ,界面如下:

img

No application available 没有服务被发现 ……_
因为没有注册服务当然不可能有服务被发现了。

三、创建一个服务提供者 (eureka client)
当client向server注册时,它会提供一些元数据,例如主机和端口,URL,主页等。Eureka server 从每个client实例接收心跳消息。 如果心跳超时,则通常将该实例从注册server中删除。

创建过程同server类似,创建完pom.xml如下:

service-hi 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>

    <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>

开启eurekaClient

通过注解@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配置文件如下:

service-hi 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 的网址:

img

你会发现一个服务已经注册在服务中了,服务名为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

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐