Skip to content
On this page

服务网关

官网地址:文档地址

当服务开始变多,我们就要开始让服务聚合起来。

API Gateway(APIGW / API 网关),顾名思义,是出现在系统边界上的一个面向 API 的、串行集中式的强管控服务,这里的边界是企业 IT 系统的边界,可以理解为企业级应用防火墙,主要起到隔离外部访问与内部系统的作用。在微服务概念的流行之前,API 网关就已经诞生了,例如银行、证券等领域常见的前置机系统,它也是解决访问认证、报文转换、访问统计等问题的。

API 网关的流行,源于近几年来移动应用与企业间互联需求的兴起。移动应用、企业互联,使得后台服务支持的对象,从以前单一的Web应用,扩展到多种使用场景,且每种使用场景对后台服务的要求都不尽相同。这不仅增加了后台服务的响应量,还增加了后台服务的复杂性。随着微服务架构概念的提出,API网关成为了微服务架构的一个标配组件。

API 网关是一个服务器,是系统对外的唯一入口。API 网关封装了系统内部架构,为每个客户端提供定制的 API。所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有非业务功能。API 网关并不是微服务场景中必须的组件,如下图,不管有没有 API 网关,后端微服务都可以通过 API 很好地支持客户端的访问。

而我们的微服务也是这样,一般情况下,可能并不是所有的微服务都需要直接暴露给外部调用,这时我们就可以使用路由机制,添加一层防护,让所有的请求全部通过路由来转发到各个微服务,并且转发给多个相同微服务实例也可以实现负载均衡。

image-20220325130147758

在之前,路由的实现一般使用Zuul,但是已经停更,而现在新出现了由SpringCloud官方开发的Gateway路由,它相比Zuul不仅性能上得到了一定的提升,并且是官方推出,契合性也会更好,所以我们这里就主要讲解Gateway。

部署网关

创建一个项目gateway 第一个依赖就是网关的依赖,而第二个则跟其他微服务一样,需要注册到Eureka才能生效,注意别添加Web依赖,使用的是WebFlux框架

xml

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
</dependencies>
1
2
3
4
5
6
7
8
9
10
11
yaml
server:
  port: 8500
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8888/eureka
spring:
  application:
    name: gateway
1
2
3
4
5
6
7
8
9

但是现在还没有配置任何的路由功能,我们接着将路由功能进行配置:

yaml
spring:
  cloud:
    gateway:
      # 配置路由,注意这里是个列表,每一项都包含了很多信息
      routes:
        - id: service-book          # 路由名称
          uri: lb://service-book # 路由的地址,lb表示使用负载均衡到微服务,也可以使用http正常转发
          predicates: # 路由规则,断言什么请求会被路由
            - Path=/book/**   # 只要是访问的这个路径,一律都被路由到上面指定的服务
1
2
3
4
5
6
7
8
9

路由规则的详细列表(断言工厂列表)在这里:文档地址 ,可以指定多种类型,包括指定时间段、Cookie携带情况、Header携带情况、访问的域名地址、访问的方法、路径、参数、访问者IP等。 也可以使用配置类进行配置,但是还是推荐直接配置文件,省事。

路由过滤器

路由过滤器支持以某种方式修改传入的 HTTP 请求或传出的 HTTP 响应,路由过滤器的范围是某一个路由,跟之前的断言一样,Spring Cloud Gateway 也包含许多内置的路由过滤器工厂,详细列表:文档地址

比如我们现在希望在请求到达时,在请求头中添加一些信息再转发给我们的服务,那么这个时候就可以使用路由过滤器来完成,我们只需要对配置文件进行修改:

yaml
spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:
        - id: service-book          # 路由名称
          uri: lb://service-book # 路由的地址,lb表示使用负载均衡到微服务,也可以使用http正常转发
          predicates: # 路由规则,断言什么请求会被路由
            - Path=/book/**   # 只要是访问的这个路径,一律都被路由到上面指定的服务
        filters: # 添加过滤器
          - AddRequestHeader=Test, HelloWorld!
1
2
3
4
5
6
7
8
9
10
11
12

比如我们将所有服务都聚合起来,形成同一个api接口,访问书籍的就用/api/book/**,用户就用/api/user

yaml
spring:
  cloud:
    gateway:
      # 配置路由,注意这里是个列表,每一项都包含了很多信息
      routes:
        - id: service-book          # 路由名称
          uri: lb://service-book # 路由的地址,lb表示使用负载均衡到微服务,也可以使用http正常转发
          predicates: # 路由规则,断言什么请求会被路由
            - Path=/api/book/**   # 只要是访问的这个路径,一律都被路由到上面指定的服务
          filters:
            - AddRequestHeader=Test, HelloWorld!
            # 跳过第一个路径前缀,这样就可以统一路径 localhost:8500/api/book/** 转发到 lb:service-book/book/**
            - StripPrefix=1
1
2
3
4
5
6
7
8
9
10
11
12
13

我们把 Path 改成 /api/book,然后filters 里添加 - StripPrefix=1跳过一个路径前缀,这样我们的服务就统一前缀了。

GlobalFilter

除了针对于某一个路由配置过滤器之外,我们也可以自定义全局过滤器,它能够作用于全局。但是我们需要通过代码的方式进行编写:

java

@Order(100)
@Component
public class TestFilter implements GlobalFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //先获取ServerHttpRequest对象,注意不是HttpServletRequest
        ServerHttpRequest request = exchange.getRequest();
        //打印一下所有的请求参数
        System.out.println(request.getQueryParams());
        //判断是否包含test参数,且参数值为1
        List<String> value = request.getQueryParams().get("test");
        if (value != null && value.contains("1")) {
            //将ServerWebExchange向过滤链的下一级传递(跟JavaWeb中介绍的过滤器其实是差不多的)
            return chain.filter(exchange);
        } else {
            //直接在这里不再向下传递,然后返回响应
            return exchange.getResponse().setComplete();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

原理

微信截图_20221013173721.png

Route

Route 是 gateway 中最基本的组件之一,表示一个具体的路由信息载体。

java

public class Route implements Ordered {

    private final String id; //标识符,区别于其他 Route。

    private final URI uri; // 路由指向的目的地 uri,即客户端请求最终被转发的目的地。

    private final int order; // 用于多个 Route 之间的排序,数值越小排序越靠前,匹配优先级越高。

    private final AsyncPredicate<ServerWebExchange> predicate; // 谓语,表示匹配该 Route 的前置条件,即满足相应的条件才会被路由到目的地 uri。

    private final List<GatewayFilter> gatewayFilters; // 过滤器用于处理切面逻辑,如路由转发前修改请求头等。
}
1
2
3
4
5
6
7
8
9
10
11
12
13

GatewayFilter

很多框架都有 Filter 的设计,用于实现可扩展的切面逻辑。

java
public interface GatewayFilter extends ShortcutConfigurable {

    String NAME_KEY = "name";
    String VALUE_KEY = "value";

    /**
     * Process the Web request and (optionally) delegate to the next
     * {@code WebFilter} through the given {@link GatewayFilterChain}.
     * @param exchange the current server exchange
     * @param chain provides a way to delegate to the next filter
     * @return {@code Mono<Void>} to indicate when request processing is complete
     */
    Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

处理Web请求并(可选地)通过给定的{@link GatewayFilterChain}委托给下一个{@code WebFilter}。

Filter 最终是通过 filter chain 来形成链式调用的,每个 filter 处理完 pre filter 逻辑后委派给 filter chain,filter chain 再委派给下一下 filter。

java
public interface GatewayFilterChain {

    /**
     * Delegate to the next {@code WebFilter} in the chain.
     * @param exchange the current server exchange
     * @return {@code Mono<Void>} to indicate when request handling is complete
     */
    Mono<Void> filter(ServerWebExchange exchange);

}
1
2
3
4
5
6
7
8
9
10

委派给链中的下一个WebFilter。

Route 构建的原理

Spring boot 遵循规约大于配置的原则,starter 模块都有对应的以模块名称作前缀,以 "AutoConfiguration" 后缀的自动装配类。同样的还有以模块名前缀,以Properties后缀的配置类作为支持。

Gateway 模块自动装配类为 GatewayAutoConfiguration,对应的配置类为 GatewayProperties。

java

@ConfigurationProperties("spring.cloud.gateway") // 表明以 "spring.cloud.gateway" 前缀的 properties 会绑定 GatewayProperties。
@Validated
public class GatewayProperties {

    /**
     * List of Routes
     */
    @NotNull
    @Valid
    private List<RouteDefinition> routes = new ArrayList<>(); //  用来对 Route 进行定义。

    /**
     * List of filter definitions that are applied to every route.
     */
    private List<FilterDefinition> defaultFilters = new ArrayList<>(); // 用于定义默认的 Filter 列表,默认的 Filter 会应用到每一个 Route 上,gateway 处理时会将其与 Route 中指定的 Filter 进行合并后并逐个执行。
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

RouteDefinition

该组件用来对 Route 信息进行定义,最终会被 RouteLocator 解析成 Route

java
public class RouteDefinition {
    @NotEmpty
    private String id = UUID.randomUUID().toString(); // 定义 Route 的 id,默认使用 UUID。

    @NotEmpty
    @Valid
    private List<PredicateDefinition> predicates = new ArrayList<>();  // 定义 Predicate。

    @Valid
    private List<FilterDefinition> filters = new ArrayList<>();  // 定义 Filter。

    @NotNull
    private URI uri;  // 定义目的地 URI。

    private int order = 0; // 定义 Route 的序号。
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

LoadBalancerClientFilter

java
public class LoadBalancerClientFilter implements GlobalFilter, Ordered {

    private static final Log log = LogFactory.getLog(LoadBalancerClientFilter.class);
    public static final int LOAD_BALANCER_CLIENT_FILTER_ORDER = 10100;

    private final LoadBalancerClient loadBalancer;

    public LoadBalancerClientFilter(LoadBalancerClient loadBalancer) {
        this.loadBalancer = loadBalancer;
    }

    @Override
    public int getOrder() {
        return LOAD_BALANCER_CLIENT_FILTER_ORDER;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 获得 URL 
        URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
        if (url == null || !url.getScheme().equals("lb")) {
            return chain.filter(exchange);
        }
        // 添加 原始请求URI 到 GATEWAY_ORIGINAL_REQUEST_URL_ATTR
        //preserve the original url
        addOriginalRequestUrl(exchange, url);

        log.trace("LoadBalancerClientFilter url before: " + url);

        // 获取 服务实例
        final ServiceInstance instance = loadBalancer.choose(url.getHost());
        if (instance == null) {
            throw new NotFoundException("Unable to find instance for " + url.getHost());
        }
     
     	/*URI uri = exchange.getRequest().getURI();
 		URI requestUrl = loadBalancer.reconstructURI(instance, uri);*/
        //
        URI requestUrl = UriComponentsBuilder.fromUri(url)
                .scheme(instance.isSecure() ? "https" : "http") //TODO: support websockets
                .host(instance.getHost())
                .port(instance.getPort())
                .build(true)
                .toUri();
        log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);

        // 添加 请求URI 到 GATEWAY_REQUEST_URL_ATTR
        exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);

        // 提交过滤器链继续过滤
        return chain.filter(exchange);
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
java
URI url=exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
if(url==null||!url.getScheme().equals("lb")){
    return chain.filter(exchange);
}
1
2
3
4

获得 URL 。只处理 lb:// 为前缀( Scheme )的地址。

java
final ServiceInstance instance = loadBalancer.choose(url.getHost());
1

调用 LoadBalancerClient#choose(String) 方法,获得一个服务实例( ServiceInstance ) ,从而实现负载均衡。

java
URI requestUrl = UriComponentsBuilder.fromUri(url)
        .scheme(instance.isSecure() ? "https" : "http") //TODO: support websockets
        .host(instance.getHost())
        .port(instance.getPort())
        .build(true)
        .toUri();
log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
1
2
3
4
5
6
7

创建 requestUrl 。 微信截图_20221014101448.png

设置 requestUrl 到 GATEWAY_REQUEST_URL_ATTR 。后面 Routing 相关的 GatewayFilter 会通过该属性,发起请求。

提交过滤器链继续过滤。注意,这里不需要创建新的 ServerWebExchange

Released under the MIT License.