SpringCloud(Greenwich版)新一代API网关Gateway

时间:2024-02-15 22:06:33

推荐以下稳定版本号:

Spring Boot: 2.1.9.RELEASE

Spring Cloud: Greenwich.SR3

一、Zuul网关存在的问题

  在实际使用中我们会发现直接使用 Zuul 会存在诸多问题,包括:

  比如性能问题,Zuul1.x版本本质上就是一个同步的 Servlet,采用多线程阻塞模型进行请求转发。简单讲,每来一个请求,Servlet 容器要为该请求分配一个线程专门负责处理这个请求,直到响应返回客户端这个线程才会被释放返回容器线程池。如果后台服务调用比较耗时,那么这个线程就会被阻塞,阻塞期间线程资源被占用,不能干其他事情。我们知道 Servlet 容器线程池的大小是有限制的,当前端请求量大,而后台服务比较多时,很容易耗尽容器线程池内的线程,造成容器无法接受新的请求。

  不支持任何长链接,例如websocket。

 

二、Gateway简介

  Gateway 是在 Spring 生态系统之上构建的API网关服务,基于 Spring 5,Spring Boot 2 和 Project Reactor 等技术。Spring Cloud Gateway 旨在提供一种简单而有效的统一的 API 路由管理方式,统一访问接口。Spring Cloud Gateway 作为 Spring Cloud 生态系统中的网关,目标就是代替 Netflix Zuul。以及提供一些强大的过滤器功能,例如:熔断、限流、重试等。

一句话概括:Spring Cloud Gateway 使用的 Webflux 中的 reactor-netty 响应式编程组件,底层使用了 Netty 通信框架。

Spring Cloud GateWay 具有如下特性:

  • 基于 Spring Framework 5,Project Reactor 和 Spring Boot 2.0 进行构建;

  • 动态路由:能够匹配任何请求属性;

  • 可以对路由指定 Predicate (断言) 和 Filter (过滤器);

  • 集成 Hystrix 的断路器功能;

  • 集成 Spring Cloud 服务发现功能;

  • 易于编写的 Predicate (断言) 和 Filter (过滤器);

  • 请求限流功能;

  • 支持路径重写。

 

三、核心概念

  • Route(路由):路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由。

  • Predicate(断言):参考的是 Java8 的 java.util.function.Predicate (函数式编程) 开发人员可以匹配 HTTP 请求中的所有内容 (例如请求头和请求参数),如果请求与断言相匹配则进行路由。

  • Filter(过滤器):指的是 Spring 框架中 GatewayFilter 的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。

 

四、工作流程

  下图从总体上概述了 Spring Cloud Gateway 的工作方式:

 

  客户端向 Spring Cloud Gateway 发出请求。然后 Gateway Handler Mapping (处理映射) 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,之后返回。过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前 ("pre") 或之后 ("post") 执行业务逻辑。Filter 在 "pre" 类型的过滤器可以做参数效验、权限效验、流量监控、日志输出和协议转换等,Filter 在 "post" 类型的过滤器中可以做响应内容、响应头的修改、日志的输出和流量监控等有着非常重要的作用。核心逻辑:路由转发 + 执行过滤器链。

 

五、路由与断言配置

使用 Spring Cloud Gateway 你需要准备一个注册中心和服务提供者。

1)build.gradle项目依赖

创建gradle模块api-gateway并添加gateway依赖, Gateway 是使用 netty+webflux 实现因此不需要再引入 web 模块

dependencies {
   compile group: \'org.springframework.cloud\', name: \'spring-cloud-starter-gateway\'
}

2)两种不同的网关路由配置,个人推荐使用yaml方式

使用 application.yaml 进行配置:

server:
  port: 5001
spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes:
        - id: provider-route
          uri: http://localhost:8081
          predicates:
            - Path=/client/**
View Code

spring.cloud.gateway 路由下的字段含义如下:

  • id:路由的ID,没有固定规则但要求唯一,建议配合服务名。

  • uri:转发到微服务 (或服务提供者) 的地址。

  • predicates:路由条件,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。

  • Path:断言,路径相匹配的进行路由。两个星符号代表的是Controller中client模块下的某个请求。

使用 Java 编码的方式进行配置:

需要在配置类注入 RouteLocator 的 Bean,代码的道理和配置文件是一样的,就不进行解释了。

package org.wesson.springcloud.gateway.config;

import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class GatewayConfig {

    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder) {
        RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();

        routes.route("provider-route", r -> r.path("/client/**").uri("http://localhost:8081")).build();

        return routes.build();
    }

}
View Code

3)测试

Step1:运行 eureka-server 启动类,端口为8761

Step2:运行 provider-service 启动类,端口为8081

Step3:运行 api-gateway 启动类,端口为5001

Step4:访问http://localhost:8761/,注册到的服务如下图:

Step5:访问添加网关之前路径:http://localhost:8081/client/info,直接获得返回结果:

  • hello, service provider port is from:8081

Step6:访问添加网关之后路径:http://localhost:5001/client/info,可以成功通过5001转发到8081返回数据:

  • hello, service provider port is from:8081

路由转发规则有点类似于按照上面的配置,慢慢得淡化真实访问地址以及端口号。

 

六、动态路由

1)build.gradle项目依赖

Gateway 使用动态路由前需要在api-gateway项目中添加eureka客户端依赖

compile group: \'org.springframework.cloud\', name: \'spring-cloud-starter-netflix-eureka-client\'

2)更改之前application.yaml配置文件

server:
  port: 5001
spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
          lower-case-service-id: true #微服务名称以小写形式呈现,默认大写微服务名
      routes:
        - id: provider-route1
          uri: lb://provider-service #lb:// 根据微服务名称从注册中心获取服务请求路径
          predicates:
            - Path=/client/**
        - id: provider-route2
          uri: lb://provider-service #lb:// 根据微服务名称从注册中心获取服务请求路径
          predicates:
            - Path=/client/**
eureka:
  instance:
    hostname: localhost
  client:
    fetch-registry: true
    register-with-eureka: true
    service-url:
      defaultZone: http://localhost:8761/eureka/
View Code

3)启动类ApiGatewayApplication.java

在启动类上添加@EnableDiscoveryClient注解,需要被注册中心发现

package org.wesson.springcloud.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient
@SpringBootApplication
public class ApiGatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(ApiGatewayApplication.class, args);
    }

}
View Code

4)测试

Step1:运行 provider-service 启动类2个实例,端口为8081、8082

Step2:访问http://localhost:8761/,注册到的服务如下图:

Step3:多次访问http://localhost:5001/provider-service/client/info,请求返回结果会以循环的方式实现负载均衡功能:

  • hello, service provider port is from:8081

  • hello, service provider port is from:8082

 

七、过滤器

  Spring Cloud Gateway 除了具备请求路由功能之外,也支持对请求的过滤。其实与 Zuul 网关类似,也是通过过滤器的形式来实现的。那么接下来一起来研究一下 Gateway 中的过滤器。

过滤器的生命周期

Spring Cloud Gateway 的 Filter 的生命周期不像 Zuul 那么丰富,他只有两个:"pre" 和 "post"。

  • PRE:转发到微服务之前执行的过滤器。

  • POST:在执行微服务获取返回值之后执行的过滤器。

过滤器的类型

Spring Cloud Gateway 的 Filter 从作用范围可分为另外两种 GatewayFilter 和 GlobalFilter。

  • GatewayFilter:应用到单个路由或者一个分组的路由上。

  • GlobalFilter:应用到所有的路由上。

认证过滤器

package org.wesson.springcloud.gateway.filter;

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/*
 * 自定义全局过滤器
 *    需要实现 GlobalFilter, Ordered 两个接口中的方法
 */
@Component
public class LoginFilter implements GlobalFilter, Ordered {
    /**
     *功能描述:执行过滤器中的业务逻辑
     *      ServerWebExchange此对象相当于请求和响应的上下文 (Zuul中的 RequestContext)
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 获取请求参数 access-token
        String token = exchange.getRequest().getQueryParams().getFirst("access-token"); // 对请求参数中的 access-token 进行判断
        // 判断 access-token 参数是否存在
        if (token == null) {
            System.out.println("用户未登录");
            // 如果不存在此参数:认证失败
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete(); // 请求结束
        }
        // 如果存在此参数,认证成功
        return chain.filter(exchange); // 执行后续操作
    }

    /**
     *功能描述:指定过滤器的执行顺序
     * 返回值越小,执行顺序越高
     */
    @Override
    public int getOrder() {
        return 1;
    }
}
View Code