文章目录
- 什么是网关?
 - 搭建实验项目
 - 路由(Route)
 - 断言(Predicate)和断言工厂(Predicate Factory)
 - 过滤器(Filter)
 - 跨域问题解决
 - 与nacos集成
 - 负载均衡
 - 请求过程源码解析
 
什么是网关?
当我们的微服务越来越多,外部需要访问,为了安全,我们需要做身份认证,认证通过,才能访问我们的服务,而当请求过来,我们需要根据请求的不同,路由到不同的服务中,而一个服务会有多个实例,请求过来还需要做负载均衡,还有当请求过多的时候,我们需要做限流,以上这些都需要我们的网关来实现。目前网关有 Zuul 和 Gateway,因为 Zuul 是阻塞式编程,而 Gateway 是基于 Spring webFlux 的响应式编程,所以 Gateway 的吞吐会更好,下面我们就来学习 Gateway!
搭建实验项目
为了学习 Gateway,我们先搭建两个项目,一个是业务服务项目(demo-service),一个是 Gateway 服务项目(gateway-service),初始配置如下:
demo-service
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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.7.13</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.victor.demo</groupId>
	<artifactId>demo-service</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>样例服务</name>
	<description>样例服务</description>
	<properties>
		<java.version>17</java.version>
	</properties>
	<dependencies>
	    <!-- Spring Boot -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
	</dependencies>
</project>
 
application.yml 配置如下:
server:
    #端口号改成8081
    port: 8081
spring:
    application:
        name: demo
 
gateway-service
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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>2.7.13</version>
      <relativePath/> <!-- lookup parent from repository -->
   </parent>
   <groupId>com.victor.gateway</groupId>
   <artifactId>gateway-service</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <name>gateway网关</name>
   <description>gateway网关</description>
   <properties>
      <java.version>17</java.version>
   </properties>
    <dependencies>
        <!-- Gateway 网关 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
            <version>3.1.8</version>
        </dependency>
    </dependencies>
</project>
 
application.yml 配置如下:
spring:
    application:
        name: gateway
 
gateway没有修改端口号,默认8080
尝试简单上手
我们先不使用 gateway,直接在 demo-service 里添加 controller,启动服务
package com.victor.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DemoController {
    @RequestMapping("/hello")
    public String hello() {
        return "hello gateway";
    }
}
 
在浏览器中输入 http://127.0.0.1:8081/hello,效果如下:

下面我们加入 gateway,通过 gateway 路由到 demo-service 服务,首先修改下 gateway 的application.yml配置文件,如下所示:
spring:
    application:
        name: gateway
    cloud:
        gateway:
            routes:
                -   id: demo-service
                    uri: http://127.0.0.1:8081
                    predicates:
                        - Path=/demo-service/**
                    filters:
                        - StripPrefix=1
 
在浏览器中输入 http://127.0.0.1:8080/demo-service/hello,效果如下:

可以达到一样的效果,成功!gateway 里的配置起作用了,首先我们的请求匹配的上 /demo-service/** ,然后StripPrefix=1 会跳过开头的 /demo-service,所以 http://127.0.0.1:8080/demo-service/hello 就被路由到了 http://127.0.0.1:8081/hello 上。
下面我们就来学习 gateway 的配置, spring.could.gateway 是 gateway 的标准配置前缀,我们重点看 routes,这是一个复数,可以包含多个 route,我们可以配置多个路由转发
路由(Route)
路由下面包含了以下参数(我们可以看看 route 的源码,是一个叫 RouteDefinition 的类):
id:路由的唯一标识符predicates:断言,用于判断请求是否符合条件,符合就转发路由目的地,可以配置多个filters:过滤器,用于处理请求和响应,可以配置多个uri:指定目标服务的 uri,也就是最终转发到的服务,支持 http 和 lb(负载均衡) 两种方式metadata:元数据,key-value 键值对order:多个路由之间排序,数值越小匹配优先级越高
下面结合官方文档 https://docs.spring.io/spring-cloud-gateway/docs/3.1.8/reference/html,具体实际操作一下断言和过滤器的功能
断言(Predicate)和断言工厂(Predicate Factory)
断言(Predicate)用于定义路由规则中的条件匹配逻辑,它会根据请求的属性进行判断,决定是否应用这个路由规则,我们在配置文件里写的断言(Predicate)只是个字符串,最终这个字符串会被一个叫断言工厂(Predicate Factory)的东西解析,转换成真正的判断条件,上面我们实验的例子用到了 Path 方式的断言,其对应的断言工厂类是 PathRoutePredicateFactory,(其实就是配置文件里key,后面 + RoutePredicateFactory)除了这个,Gateway 还提供了许多预定义的断言工厂,如下:
| 名称 | 说明 | 样例 | 
|---|---|---|
| After | 请求必须在某个时间点之后 | - After=2023-09-15T16:00:00.000+08:00[Asia/Shanghai] | 
| Before | 请求必须在某个时间点之前 | - Before=2023-09-15T17:00:00.000+08:00[Asia/Shanghai] | 
| Between | 请求必须在某两个时间点之间 | - Between=2023-09-15T16:00:00.000+08:00[Asia/Shanghai], 2023-09-15T17:00:00.000+08:00[Asia/Shanghai] | 
| Cookie | 请求必须包含某些cookie | - Cookie=chocolate, ch.p | 
| Header | 请求必须包含某些header | - Header=X-Request-Id, \d+ | 
| Host | 请求的主机名或IP地址必须符合规则 | - Host=127.0.0.1:8080 | 
| Method | 请求方式必须是指定的方式 | - Method=POST | 
| Path | 请求路径必须符合指定规则 | - Path=/demo-service/hello | 
| Query | 请求参数必须包含指定参数 | - Query=green | 
| RemoteAddr | 请求者的ip必须符合规则 | - RemoteAddr=127.0.0.1 | 
| Weight | 权重 | - Weight=group, 5 | 
| XForwardedRemoteAddr | 请求者的ip必须符合规则(考虑代理服务器影响) | - XForwardedRemoteAddr=192.168.33.33 | 
gateway自带的断言工厂
After(请求必须在某个时间点之后)
修改 application.yml 配置文件
spring:
    application:
        name: gateway
    cloud:
        gateway:
            routes:
                -   id: demo-service
                    uri: http://127.0.0.1:8081
                    predicates:
                        - After=2023-09-15T16:00:00.000+08:00[Asia/Shanghai]
 
表示在 2023-09-15T16:00:00.000+08:00[Asia/Shanghai] 这个时间之后的请求才会被匹配到这个路由
现在时间是 2023-09-15 16:05,我们用postman试下

可以的,我们改下配置的时间为16号,2023-09-16T16:00:00.000+08:00[Asia/Shanghai]

可以看到报404
Before(请求必须在某个时间点之前)
修改 application.yml 配置文件
spring:
    application:
        name: gateway
    cloud:
        gateway:
            routes:
                -   id: demo-service
                    uri: http://127.0.0.1:8081
                    predicates:
                        - Before=2023-09-15T17:00:00.000+08:00[Asia/Shanghai]
 
表示在 2023-09-15T17:00:00.000+08:00[Asia/Shanghai] 这个时间之前的请求才会被匹配到这个路由
和 After 类似,就不贴例子了,可以自己试试
Between(请求必须在某两个时间点之间)
修改 application.yml 配置文件
spring:
    application:
        name: gateway
    cloud:
        gateway:
            routes:
                -   id: demo-service
                    uri: http://127.0.0.1:8081
                    predicates:
                        - Between=2023-09-15T16:00:00.000+08:00[Asia/Shanghai], 2023-09-15T17:00:00.000+08:00[Asia/Shanghai]
 
表示在 2023-09-15T16:00:00.000+08:00[Asia/Shanghai] 和 2023-09-15T17:00:00.000+08:00[Asia/Shanghai] 两个时间点之间个路由
和 After 、Before类似,就不贴例子了,可以自己试试
Cookie(请求必须包含某些cookie)
修改 application.yml 配置文件
spring:
    application:
        name: gateway
    cloud:
        gateway:
            routes:
                -   id: demo-service
                    uri: http://127.0.0.1:8081
                    predicates:
                        - Cookie=chocolate, ch.p
 
表示 cookie 中包含 chocolate 参数,并且值是 ch.p 的请求才会被匹配到这个路由
我们先不添加 cookie 试一下

不出所料404,然后我们添加一下cookie


成功了!
也可以配置多个,像这样
spring:
    application:
        name: gateway
    cloud:
        gateway:
            routes:
                -   id: demo-service
                    uri: http://127.0.0.1:8081
                    predicates:
                        - Cookie=chocolate, ch.p
                        - Cookie=abc, 123
 
这样必须包含两个 cookie 才行
Header(请求必须包含某些header)
修改 application.yml 配置文件
spring:
    application:
        name: gateway
    cloud:
        gateway:
            routes:
                -   id: demo-service
                    uri: http://127.0.0.1:8081
                    predicates:
                        - Header=X-Request-Id, \d+
 
表示请求头里有一个 key 是 X-Request-Id,值需要匹配到正则表达式 [\d+] 才会匹配到这个路由
我们先不添加头信息

没错,404
然后再加上头信息

成功!
这里也可以这么写 - Header=X-Request-Id,表示只要有这个参数就可以,不在乎值是多少
当然也可以像 cookie 那样配置多个
Host(请求的主机名或IP地址必须符合规则)
修改 application.yml 配置文件
spring:
    application:
        name: gateway
    cloud:
        gateway:
            routes:
                -   id: demo-service
                    uri: http://127.0.0.1:8081
                    predicates:
                        - Host=127.0.0.1:8080
 
请求的主机名或IP地址必须是 127.0.0.1:8080 才会被匹配到这个路由
按照上面的配置请求是成功的,但凡换个别的配置,就404,就不贴图了
Method(请求方式必须是指定的方式)
修改 application.yml 配置文件
spring:
    application:
        name: gateway
    cloud:
        gateway:
            routes:
                -   id: demo-service
                    uri: http://127.0.0.1:8081
                    predicates:
                        - Method=POST
 
如果是 POST 请求才会被匹配到这个路由
这里我的接口是 GET,就会报404,如果要既支持GET,又支持POST,就用逗号分割,像这样- Method=GET,POST
Path(请求路径必须符合指定规则)
修改 application.yml 配置文件
spring:
    application:
        name: gateway
    cloud:
        gateway:
            routes:
                -   id: demo-service
                    uri: http://127.0.0.1:8081
                    predicates:
                        - Path=/hello
 
请求路径中能匹配到 /hello 才会被匹配到这个路由
这个其实是我们最常用的,一般用来根据匹配不同的服务,把请求路由到不同的服务那边
Query(请求参数必须包含指定参数 )
修改 application.yml 配置文件
spring:
    application:
        name: gateway
    cloud:
        gateway:
            routes:
                -   id: demo-service
                    uri: http://127.0.0.1:8081
                    predicates:
                        - Query=color, red
 
请求中必须包含参数 color 并且值是 red 才会被匹配到这个路由

这里也可以这么写 - Query=color,表示只要有这个参数就可以,不在乎值是多少,和 Header 相似,也可以添加多个
RemoteAddr(请求者的IP必须符合规则)
修改 application.yml 配置文件
spring:
    application:
        name: gateway
    cloud:
        gateway:
            routes:
                -   id: demo-service
                    uri: http://127.0.0.1:8081
                    predicates:
                        - RemoteAddr=127.0.0.1
 
请求者的 IP 必须是 127.0.0.1 才会被匹配到这个路由
Weight(权重)
修改 application.yml 配置文件
spring:
    application:
        name: gateway
    cloud:
        gateway:
            routes:
                -   id: demo-service
                    uri: http://127.0.0.1:8081
                    predicates:
                        - Weight=group, 8
                    filters:
                        - AddRequestParameter=color, red
                -   id: demo-service2
                    uri: http://127.0.0.1:8081
                    predicates:
                        - Weight=group, 2
					filters:
                        - AddRequestParameter=color, blue
 
接收两个参数,一个 group,一个 weight,80%的请求会被发送至 demo-service,20%的请求会被发送至demo-service2
配置文件里增加 filters 是为了方便验证,我们再 给 demo-service 增加个 controller
@RequestMapping("/color")
public String hello(@RequestParam("color") String color) {
	return "this color is " + color;
}
 
好,来试下


会发现有时是 this color is red 有时是 this color is blue,但是前者次数更多,因为权重大
XForwarded Remote Addr(请求者的IP必须符合规则(考虑代理服务器影响))
修改 application.yml 配置文件
spring:
    application:
        name: gateway
    cloud:
        gateway:
            routes:
                -   id: demo-service
                    uri: http://127.0.0.1:8081
                    predicates:
                        - XForwardedRemoteAddr=192.168.33.33
 
请求者的 IP 必须是 192.168.33.33 才会被匹配到这个路由
和 RemoteAddr 不一样的是,因为 gateway 前面可能会有代理服务器,使用 RemoteAddr 去匹配的是代理服务器的 IP,所以就有了这个断言,这个断言会通过从头信息中的 X-Forwarded-For 参数去获取客户端原始 IP 地址,然后去匹配。
来试试

果然可以!
疑问
当一个路由下有多个断言,是只需要满足其中任意一个,还是必须都满足?
spring:
    application:
        name: gateway
    cloud:
        gateway:
            routes:
                -   id: demo-service
                    uri: http://127.0.0.1:8081
                    predicates:
                        - After=2023-09-15T16:00:00.000+08:00[Asia/Shanghai]
                        - Cookie=chocolate, ch.p
 
结论:当一个路由下有多个断言时,所有条件都必须满足才能匹配此路由
当有多个路由都匹配的情况,会选择哪一个路由?
首先我们配置两个路由,条件一致
spring:
    application:
        name: gateway
    cloud:
        gateway:
            routes:
                -   id: demo-service
                    uri: http://127.0.0.1:8081
                    predicates:
                        - Method=GET,POST
                    filters:
                        - AddRequestParameter=color, red
                -   id: demo-service2
                    uri: http://127.0.0.1:8081
                    predicates:
                        - Method=GET,POST
                    filters:
                        - AddRequestParameter=color, blue
 
添加参数是为了好区分,还是使用color接口
@RequestMapping("/color")
public String hello(@RequestParam("color") String color) {
	return "this color is " + color;
}
 
结果如下:

始终走的第一个路由 demo-service
我们改动一下,把 demo-service2 的 order 改成 -1,试下
spring:
    application:
        name: gateway
    cloud:
        gateway:
            routes:
                -   id: demo-service
                    uri: http://127.0.0.1:8081
                    predicates:
                        - Method=GET,POST
                    filters:
                        - AddRequestParameter=color, red
                -   id: demo-service2
                    uri: http://127.0.0.1:8081
                    order: -1
                    predicates:
                        - Method=GET,POST
                    filters:
                        - AddRequestParameter=color, blue
 

发现走第二个路由 demo-service2 了
结论:当一个请求满足多个路由的断言(Predicate)条件时,order较小的路由会生效(如果不配置默认是0,如果order一致,排在前面的生效)
自定义断言工厂
gateway 提供了这么多断言工厂,也可能不满足我们的实际应用场景,这时候,就需要我们自己写断言工厂了,我们发现在配置文件里的 key + RoutePredicateFactory 就是断言工厂的名字,所以我们写自定义的断言工厂,也要遵循这个规则,参考了 AfterRoutePredicateFactory 的源码,我决定写一个,通过配置文件中配置星期几,来控制路由的断言工厂
先写个断言工厂类(DayOfWeekRoutePredicateFactory):
package com.victor.gateway.config;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.GatewayPredicate;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import javax.validation.constraints.NotNull;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
@Component
public class DayOfWeekRoutePredicateFactory extends AbstractRoutePredicateFactory<DayOfWeekRoutePredicateFactory.Config> {
    public static final String DAY_OF_WEEK_KEY = "dayOfWeek";
    public DayOfWeekRoutePredicateFactory() {
        super(Config.class);
    }
    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList(DAY_OF_WEEK_KEY);
    }
    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        return new GatewayPredicate() {
            @Override
            public boolean test(ServerWebExchange serverWebExchange) {
                final DayOfWeek currentDayOfWeek = DayOfWeek.from(LocalDate.now());
                return currentDayOfWeek.equals(config.getDayOfWeek());
            }
            @Override
            public Object getConfig() {
                return config;
            }
            @Override
            public String toString() {
                return String.format("DayOfWeek: %s", config.getDayOfWeek());
            }
        };
    }
    public static class Config {
        @NotNull
        private DayOfWeek dayOfWeek;
        public DayOfWeek getDayOfWeek() {
            return dayOfWeek;
        }
        public void setDayOfWeek(DayOfWeek dayOfWeek) {
            this.dayOfWeek = dayOfWeek;
        }
    }
}
 
然后修改配置文件:
spring:
    application:
        name: gateway
    cloud:
        gateway:
            routes:
                -   id: demo-service
                    uri: http://127.0.0.1:8081
                    predicates:
                        - DayOfWeek=MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY
 
意思就是当工作日的时候,会匹配此路由
过滤器(Filter)
在 Gateway中,过滤器是用于在请求进入网关和离开网关之间执行一些逻辑的组件。分为局部过滤器(Gateway Filter)和全局过滤器(Global Filter)
局部过滤器(Gateway Filter)
局部过滤器只对特定的路由起作用,Gateway 内部提供了30多种 Gateway Filter,这里就简单举几个例子,等需要用到的时候,查阅官方文档:https://docs.spring.io/spring-cloud-gateway/docs/3.1.8/reference/html/#gatewayfilter-factories,下面取自官方文档中的例子:
AddRequestHeader
spring:
  cloud:
    gateway:
      routes:
      - id: add_request_header_route
        uri: https://example.org
        filters:
        - AddRequestHeader=X-Request-red, blue
 
所有匹配到这个路由的请求的请求头都添加: 名字是 Header X-Request-red,值是 blue
AddRequestParameter
spring:
  cloud:
    gateway:
      routes:
      - id: add_request_parameter_route
        uri: https://example.org
        filters:
        - AddRequestParameter=red, blue
 
所有匹配到这个路由的请求都添加一个参数: 名字是 red,值是 blue
AddResponseHeader
spring:
  cloud:
    gateway:
      routes:
      - id: add_response_header_route
        uri: https://example.org
        filters:
        - AddResponseHeader=X-Response-Red, Blue
 
所有匹配到这个路由的请求的响应头添加: 名字是 X-Response-Red,值是 Blue
自定义局部过滤器
命名和断言工厂一样,是配置文件里的 key + GatewayFilterFactory ,可以实现接口 GatewayFilterFactory,也可以继承抽象类 AbstractGatewayFilterFactory
package com.victor.gateway.config;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
@Component
public class CustomGatewayFilterFactory extends AbstractGatewayFilterFactory {
    @Override
    public GatewayFilter apply(Object config) {
        return (exchange, chain) -> {
            //----------------处理业务逻辑start----------------
            System.out.println("自定义过滤器");
            //----------------处理业务逻辑end----------------
            return chain.filter(exchange);
        };
    }
}
 
配置文件改下
spring:
    application:
        name: gateway
    cloud:
        gateway:
            routes:
                -   id: demo-service
                    uri: http://127.0.0.1:8081
                    predicates:
                        - After=2023-09-15T16:00:00.000+08:00[Asia/Shanghai]
                    filters:
                        - Custom
 
因为没有参数,就直接写成 - Custom
全局过滤器(Global Filter)
全局过滤器对所有请求进行过滤,同样的 Gateway 内部提供了一些全局过滤器,查阅官方文档:https://docs.spring.io/spring-cloud-gateway/docs/3.1.8/reference/html/#global-filters,大致如下:
| 名称 | 作用 | 
|---|---|
| ForwardRoutingFilter | 请求转发 | 
| ReactiveLoadBalancerClientFilter | 客户端负载均衡 | 
| WebClientHttpRoutingFilter | 将HTTP请求通过 WebClient 进行路由 | 
| NettyWriteResponseFilter | 代理响应写回网关的客户端 | 
| RouteToRequestUrlFilter | 请求路由到目标 URL | 
| GatewayMetricsFilter | 收集和记录网关的度量指标 | 
但是,关于有些强业务的过滤器,Gateway本身没办法帮我们实现,这时候就需要我们自己写自定义的全局过滤器
自定义全局过滤器
全局过滤器的命名就没有那么多讲究了,只要实现了 GlobalFilter 接口就可以了,这里写了一个过滤器,校验请求头中是否包含了一个名为 token 值是 123456,如果包含就通过,否则返回 401 错误码
package com.victor.gateway.config;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class CustomGlobalFilter implements GlobalFilter, Ordered {
    /**
     *
     * @param exchange 上下文信息,可以获取request、response等信息
     * @param chain 过滤器链,用来把请求委托给下一个过滤器
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //----------------处理业务逻辑start----------------
        if (!"123456".equals(exchange.getRequest().getHeaders().getFirst("token"))) {
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }
        //----------------处理业务逻辑end----------------
        return chain.filter(exchange);
    }
    @Override
    public int getOrder() {
        return -1;
    }
}
 
可以从 exchange 中获取上下文需要的各类信息,如 request、response 等等,然后从 request、response 里又可以获取 header、cookie、uri,path 等信息,过滤器也是需要分先后执行的,这时候就可以实现 Ordered 接口,重写 getOrder 方法,返回一个 int 值,这个值越小,优先级就越高。也可以通过添加 @Order 注解实现
不知道怎么用的,可以参考上面 Gateway 自带的那些过滤器。
跨域问题解决
在配置文件里如下配置:
spring:
    cloud:
        gateway:
            globalcors:
                add-to-simple-url-handler-mapping: true #解决options请求被拦截问题
                cors-configurations:
                    '[/**]':
                        allowedOrigins: #允许哪些网站的跨域请求
                            - "https://docs.spring.io"
                        allowedMethods: #允许的跨域ajax请求方式
                            - GET
                            - POST
                        allowedHeaders: "*" #允许请求头里携带的信息
                        allowCredentials: true #是否允许携带cookie
                        maxAge: 360000 #这次跨域检测有效期
 
与nacos集成
nacos 是既可以做注册中心,又可以做配置中心的,这边只讲做注册中心,有机会单独写篇文章讲配置中心
首先本地起一个 nacos,默认端口8848,这个教程网上很多,这里就不讲了
第一步,引入依赖
给 gateway-service、demo-service 分别引入 nacos 的依赖,注意还需要引入 loadbalancer 依赖,还有就是 nacos 的配置,我们一般放在 bootstrap.yml 文件里(因为 bootstrap.yml 加载顺序早于 application.yml,用 nacos 做配置中心时,需要先加载 nacos 的配置),所以还需要引入 bootstrap 依赖,如下:
<!-- 负载均衡 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-loadbalancer</artifactId>
    <version>3.1.7</version>
</dependency>
<!-- Nacos 服务发现依赖 -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    <version>2021.0.5.0</version>
</dependency>
<!-- Nacos 配置中心依赖 -->
<!--<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    <version>2021.0.5.0</version>
</dependency>-->
<!-- bootstrap -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bootstrap</artifactId>
    <version>3.1.7</version>
</dependency>
 
用作配置中心的时候还需要引入 spring-cloud-starter-alibaba-nacos-config,这里先注释掉了
第二步,添加 @EnableDiscoveryClient 注解
一般我们把这个注解写在启动类上,在 gateway-service、demo-service 的启动类上增加 @EnableDiscoveryClient 注解
@SpringBootApplication
@EnableDiscoveryClient
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}
 
@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}
 
第三步,修改配置文件
给 gateway-service、demo-service 添加 bootstrap.yml,并增加 nacos 配置
spring:
    cloud:
        # Nacos配置
        nacos:
            discovery:
                server-addr: 127.0.0.1:8848
                namespace: dev
            config:
                enabled: false
 
然后,需要额外改下 gateway 配置中的 uri 改成 lb://demo
spring:
    application:
        name: gateway
    cloud:
        gateway:
            routes:
                -   id: demo-service
                    uri: lb://demo #修改成负载均衡的方式
                    predicates:
                        - Path=/demo-service/**
                    filters:
                        - StripPrefix=1
 
第四步,启动
因为我们配了 nacos 的 namespace 是 dev,所以先要在 nacos 的页面配置这个命名空间,注意 ID 写 dev,然后启动 nacos,gateway-service,demo-service,打开 nacos 的页面 http://localhost:8848/nacos

在服务列表里选择 dev 命名空间(如何没配的话,会在 public 下),可以看到我们注册进来的两个服务
再试着用 postman 调用下接口,是可以的

负载均衡
Gateway 路由配置里 uri 配置了 lb:// 开头,就会走负载均衡的逻辑,具体代码在 ReactiveLoadBalancerClientFilter 的 filter 方法里(ReactiveLoadBalancerClientFilter 是个全局过滤器,上面讲过了)

重点看choose方法

最终会走到 ReactorLoadBalancer 接口的 choose 方法

ReactorLoadBalancer 接口有3个实现,默认走的是 RoundRobinLoadBalancer

负载均衡的代码就在 getInstanceResponse 方法里
如果我们要切换成 NacosLoadBalancer 怎么操作呢,只需要在配置类上加上这么一句 @LoadBalancerClients(defaultConfiguration = NacosLoadBalancerClientConfiguration.class)

自定义负载均衡
如果我们想自己实现一个负载均衡逻辑,怎么操作呢,比如我们要写一个灰度负载均衡,根据请求头信息里的版本号去匹配对应的服务,怎么做?
第一步,给 demo-service 服务配上版本号元数据(gray-version)
spring:
    cloud:
        # Nacos配置
        nacos:
            discovery:
                server-addr: 127.0.0.1:8848
                namespace: dev
                metadata:
                    gray-version: 1.0.0
            config:
                enabled: false
 
第二步,在 gateway-service 服务里模仿 RoundRobinLoadBalancer 或者 NacosLoadBalancer 写一个 GrayLoadBalancer
public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
    private static final Log log = LogFactory.getLog(RandomLoadBalancer.class);
    private static final String GRAY_VERSION = "gray-version";
    private final String serviceId;
    private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
    private final NacosDiscoveryProperties nacosDiscoveryProperties;
    /**
     * @param serviceInstanceListSupplierProvider a provider of
     *                                            {@link ServiceInstanceListSupplier} that will be used to get available instances
     * @param serviceId                           id of the service for which to choose an instance
     */
    public GrayLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
                            String serviceId, NacosDiscoveryProperties nacosDiscoveryProperties) {
        this.serviceId = serviceId;
        this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
        this.nacosDiscoveryProperties = nacosDiscoveryProperties;
    }
    @SuppressWarnings("rawtypes")
    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
                .getIfAvailable(NoopServiceInstanceListSupplier::new);
        return supplier.get(request).next()
                .map(serviceInstances -> processInstanceResponse(supplier, serviceInstances, request));
    }
    private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier, List<ServiceInstance> serviceInstances, Request request) {
        Response<ServiceInstance> serviceInstanceResponse = getInstanceResponse(serviceInstances, request);
        if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
            ((SelectedInstanceCallback) supplier).selectedServiceInstance(serviceInstanceResponse.getServer());
        }
        return serviceInstanceResponse;
    }
    private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances, Request request) {
        if (instances.isEmpty()) {
            if (log.isWarnEnabled()) {
                log.warn("No servers available for service: " + serviceId);
            }
            return new EmptyResponse();
        }
        //具体的选择逻辑
        ServiceInstance instance;
        RequestDataContext context = (RequestDataContext) request.getContext();
        String grayVersion = context.getClientRequest().getHeaders().getFirst(GRAY_VERSION);
        if (StringUtils.isNotEmpty(grayVersion)) {
            List<ServiceInstance> instancesToChoose = instances.stream().filter(i -> grayVersion.equals(i.getMetadata().get(GRAY_VERSION))).collect(Collectors.toList());
            instance = NacosBalancer.getHostByRandomWeight3(instancesToChoose);
        } else {
            instance = NacosBalancer.getHostByRandomWeight3(instances);
        }
        return new DefaultResponse(instance);
    }
}
 
第三步,在 gateway-service 服务里写一个 GrayLoadBalancerClientConfiguration 注册 GrayLoadBalancer
@ConditionalOnDiscoveryEnabled
public class GrayLoadBalancerClientConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public ReactorLoadBalancer<ServiceInstance> grayLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory,
                                                                 NacosDiscoveryProperties nacosDiscoveryProperties) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        return new GrayLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name, nacosDiscoveryProperties);
    }
}
 
第四步,在 gateway-service 服务启动类上增加注解 @LoadBalancerClients(defaultConfiguration = GrayLoadBalancerClientConfiguration.class)
@SpringBootApplication
@EnableDiscoveryClient
@LoadBalancerClients(defaultConfiguration = GrayLoadBalancerClientConfiguration.class)
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}
 
这样就可以了,然后我们请求的时候,在头信息中增加 gray-version,值是 1.0.0,就会在配置了元数据 gray-version 等于1.0.0 的服务里选择一个
请求过程源码解析
我们知道 Gateway 用的是 webFlux 响应式编程,webFlux 处理请求的入口方法是 DispatcherHandler 的 handle 方法,我们从这里开始

看到第一个行的 handlerMappings,我就愣住了,这个东西有点眼熟啊,看看在哪里赋值的

这个方法名也眼熟,handlerAdapter 也眼熟,在 SpringMVC 的源码里有见过,原来 Spring webFlux 和 SpringMVC 在设计上有一些相似之处
| 框架 | Spring webFlux | SpringMVC | 
|---|---|---|
| 分发 | DispatcherHandler | DispatcherServlet | 
| 映射 | HandlerMapping | HandlerMapping | 
| 适配 | HandlerAdapter | HandlerAdapter | 
| 处理 | WebHandler | Handler | 
关于 SpringMVC 请求处理流程的源码可以看我的另一篇文章:SpringMVC源码学习笔记之请求处理流程
说回 Gateway 我们继续看 DispatcherHandler 的 handle 方法
//DispatcherHandler
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
   if (this.handlerMappings == null) {
      return createNotFoundError();
   }
   if (CorsUtils.isPreFlightRequest(exchange.getRequest())) {
      return handlePreFlight(exchange);
   }
   return Flux.fromIterable(this.handlerMappings) //遍历handlerMappings
         .concatMap(mapping -> mapping.getHandler(exchange)) //找到对应的WebHandler
         .next()
         .switchIfEmpty(createNotFoundError())
         .flatMap(handler -> invokeHandler(exchange, handler)) //找到适配的HandlerAdapter处理WebHandler
         .flatMap(result -> handleResult(exchange, result));//找到对应的HandlerResultHandler处理result
}
 
整体流程大概就是,遍历所有的 handlerMappings,然后找到对应的 WebHandler,再为 WebHandler 找到适配的 HandlerAdapter,用这个 HandlerAdapter 处理 WebHandler,最后再为结果找到对应的 HandlerResultHandler 处理 result。
我们先看 mapping.getHandler,来到 AbstractHandlerMapping (HandlerMapping 接口的抽象实现)类的 getHandler 方法
//AbstractHandlerMapping
@Override
public Mono<Object> getHandler(ServerWebExchange exchange) {
   //获取匹配的handler
   return getHandlerInternal(exchange).map(handler -> {
      if (logger.isDebugEnabled()) {
         logger.debug(exchange.getLogPrefix() + "Mapped to " + handler);
      }
      ServerHttpRequest request = exchange.getRequest();
      //有配置跨域相关配置的处理
      if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
         CorsConfiguration config = (this.corsConfigurationSource != null ?
               this.corsConfigurationSource.getCorsConfiguration(exchange) : null);
         CorsConfiguration handlerConfig = getCorsConfiguration(handler, exchange);
         config = (config != null ? config.combine(handlerConfig) : handlerConfig);
         if (config != null) {
            config.validateAllowCredentials();
         }
         if (!this.corsProcessor.process(config, exchange) || CorsUtils.isPreFlightRequest(request)) {
            return NO_OP_HANDLER;
         }
      }
      return handler;
   });
}
 
重点看 getHandlerInternal 方法是怎么获取匹配的 webHandler 的,来到 AbstractHandlerMapping 的子类 RoutePredicateHandlerMapping 的 getHandlerInternal 方法
//RoutePredicateHandlerMapping
@Override
protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {
   // don't handle requests on management port if set and different than server port
   if (this.managementPortType == DIFFERENT && this.managementPort != null
         && exchange.getRequest().getLocalAddress() != null
         && exchange.getRequest().getLocalAddress().getPort() == this.managementPort) {
      return Mono.empty();
   }
   exchange.getAttributes().put(GATEWAY_HANDLER_MAPPER_ATTR, getSimpleName());
   return lookupRoute(exchange)
         // .log("route-predicate-handler-mapping", Level.FINER) //name this
         .flatMap((Function<Route, Mono<?>>) r -> {
            exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);
            if (logger.isDebugEnabled()) {
               logger.debug("Mapping [" + getExchangeDesc(exchange) + "] to " + r);
            }
			//把路由放入到上下文中
            exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r);
            return Mono.just(webHandler);
         }).switchIfEmpty(Mono.empty().then(Mono.fromRunnable(() -> {
            exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);
            if (logger.isTraceEnabled()) {
               logger.trace("No RouteDefinition found for [" + getExchangeDesc(exchange) + "]");
            }
         })));
}
 
重点是 lookupRoute 方法,这个方法会过滤出符合请求的路由,并通过 exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r); 方法把匹配到的路由放入上下文中,后续有用,我们进去看看 lookupRoute 方法
//RoutePredicateHandlerMapping
protected Mono<Route> lookupRoute(ServerWebExchange exchange) {
   return this.routeLocator.getRoutes()
         // individually filter routes so that filterWhen error delaying is not a
         // problem
         .concatMap(route -> Mono.just(route).filterWhen(r -> {
            // add the current route we are testing
            exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, r.getId());
            return r.getPredicate().apply(exchange);
         })
               // instead of immediately stopping main flux due to error, log and
               // swallow it
               .doOnError(e -> logger.error("Error applying predicate for route: " + route.getId(), e))
               .onErrorResume(e -> Mono.empty()))
         // .defaultIfEmpty() put a static Route not found
         // or .switchIfEmpty()
         // .switchIfEmpty(Mono.<Route>empty().log("noroute"))
         .next()
         // TODO: error handling
         .map(route -> {
            if (logger.isDebugEnabled()) {
               logger.debug("Route matched: " + route.getId());
            }
            validateRoute(route, exchange);
            return route;
         });
   /*
    * TODO: trace logging if (logger.isTraceEnabled()) {
    * logger.trace("RouteDefinition did not match: " + routeDefinition.getId()); }
    */
}
 
this.routeLocator.getRoutes() 先获取所有 yml 文件中的路由信息,包含下面的断言等信息,并且按照 order 排好序返回,然后调用 Mono.just(route).filterWhen(r -> {...}),根据路由中断言条件来判断是否当前请求是否匹配这个路由的断言规则,next() 会获取到第一个匹配的路由,最后返回这个路由,找到后,返回上一个方法里,会返回一个FilteringWebHandler 类型的 webHandler ,到这 getHandler 结束了,找到了匹配的 webHandler
继续看下面,我这边再贴一次 DispatcherHandler 的 handle 方法

继续看 invokeHandler 方法
//DispatcherHandler
private Mono<HandlerResult> invokeHandler(ServerWebExchange exchange, Object handler) {
   if (ObjectUtils.nullSafeEquals(exchange.getResponse().getStatusCode(), HttpStatus.FORBIDDEN)) {
      return Mono.empty();  // CORS rejection
   }
   if (this.handlerAdapters != null) {
      for (HandlerAdapter handlerAdapter : this.handlerAdapters) {
         if (handlerAdapter.supports(handler)) {
            return handlerAdapter.handle(exchange, handler);
         }
      }
   }
   return Mono.error(new IllegalStateException("No HandlerAdapter: " + handler));
}
 
可以看到这里循环所有的 handlerAdapters 来找到匹配 webHandler 的 HandlerAdapter

可以看到是 SimpleHandlerAdapter ,我们跟进去看
@Override
public Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler) {
   WebHandler webHandler = (WebHandler) handler;
   Mono<Void> mono = webHandler.handle(exchange);
   return mono.then(Mono.empty());
}
 
上面我们知道返回的 webHandler 类型是 FilteringWebHandler,所以这里我们继续进到 FilteringWebHandler 的 handle 方法
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
   //从上下文中获取匹配的路由
   Route route = exchange.getRequiredAttribute(GATEWAY_ROUTE_ATTR);
   //获取路由中的局部过滤器
   List<GatewayFilter> gatewayFilters = route.getFilters();
   //全局过滤器
   List<GatewayFilter> combined = new ArrayList<>(this.globalFilters);
   //把局部过滤器和全局过滤器合并
   combined.addAll(gatewayFilters);
   // TODO: needed or cached?
   //把所有的过滤器排序
   AnnotationAwareOrderComparator.sort(combined);
   if (logger.isDebugEnabled()) {
      logger.debug("Sorted gatewayFilterFactories: " + combined);
   }
   //执行过滤器链中的每一个过滤器方法
   return new DefaultGatewayFilterChain(combined).filter(exchange);
}
 
这个方法,先从上下文中取出之前放入的路由信息,然后从路由信息里取出局部过滤器,和全局过滤器合并,然后排序,最后执行每一个过滤器方法
这里有点好奇,我们记得全局过滤器实现的是 GlobalFilter 接口,为什么可以放入到 List<GatewayFilter> combined 集合里,于是看了下 this.globalFilters 是怎么赋值的
private final List<GatewayFilter> globalFilters;
public FilteringWebHandler(List<GlobalFilter> globalFilters) {
   this.globalFilters = loadFilters(globalFilters);
}
private static List<GatewayFilter> loadFilters(List<GlobalFilter> filters) {
   return filters.stream().map(filter -> {
      GatewayFilterAdapter gatewayFilter = new GatewayFilterAdapter(filter);
      if (filter instanceof Ordered) {
         int order = ((Ordered) filter).getOrder();
         return new OrderedGatewayFilter(gatewayFilter, order);
      }
      return gatewayFilter;
   }).collect(Collectors.toList());
}
 
发现是在实例化 FilteringWebHandler 的时候,通过 loadFilters 方法赋值,这个方法,把 GlobalFilter 转成了 GatewayFilter,可以看到这里对 GlobalFilter 做了一层包装,包装成了 GatewayFilterAdapter,如果是排序的 GlobalFilter ,还要再包一层 OrderedGatewayFilter ,我们看下 GatewayFilterAdapter 的代码
private static class GatewayFilterAdapter implements GatewayFilter {
   private final GlobalFilter delegate;
   GatewayFilterAdapter(GlobalFilter delegate) {
      this.delegate = delegate;
   }
   @Override
   public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
      return this.delegate.filter(exchange, chain);
   }
   @Override
   public String toString() {
      final StringBuilder sb = new StringBuilder("GatewayFilterAdapter{");
      sb.append("delegate=").append(delegate);
      sb.append('}');
      return sb.toString();
   }
}
 
这里用到了适配器模式,用一个适配器类(GatewayFilterAdapter)实现 GatewayFilter 接口,构造函数接收一个 GlobalFilter  对象,把它包装成 GatewayFilter










