SpringCloud-Gateway

1. 简介

1.1 什么是网关

​ API Gateway,是系统的唯一对外的入口,介于客户端和服务器端之间的中间层,处理非业务功能,提供路由请求、鉴权、监控、缓存、限流等功能

​ 官网:https://spring.io/projects/spring-cloud-gateway

​ 基于Spring5+Reactor技术开发的网关,性能强劲基于Reactor+WebFlux、功能多样。

1.2 Gateway项目创建

  • 创建module,名称为api-gateway
  • 修改pom,添加依赖
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
55
56
57
58
59
60
61
62
63
64
65
66
67
<?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>cn.aacopy.demo.alibabacloud</groupId>
<artifactId>spring-cloud-alibaba-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>api-gateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>api-gateway</name>
<description>api-gateway</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>

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

<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

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

<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

  • 创建bootstrap.yml
1
2
3
4
5
6
7
8
spring:
cloud:
nacos:
server-addr: 127.0.0.1:8848
config:
file-extension: yaml
application:
name: api-gateway
  • 在nacos网关中创建api-gateway.yaml配置
1
2
3
4
5
6
7
8
9
10
11
server:
port: 8888

spring:
cloud:
gateway:
routes:
- id: order-service #路由唯一标示
uri: lb://order-service #从nacos获取名称转发,lb是负载均衡轮训策略
predicates:
- Path=/order-service/**

1.3 测试

  • 在IDEA中Edit Configurations,勾选OrderService配置中的Allow parallel run
  • 启动order-service,这时候端口为8080
  • 在nacos中修改端口为8081,再次启动一个order-service,这时候有两个order-service服务,模拟双节点
  • 启动api-gateway服务。
  • 重复请求http://127.0.0.1:8888/order-service/api/v1/order/demo/sayHello?name=aacopy
  • 在两个order服务的控制台可以看到轮询打印日志

2. 自定义负载均衡

2.1 需求:

多人开发同一个微服务时,同时注册了多个相同的服务在注册中心上,通过网关调用服务时,不希望由自带的负载均衡路由,需要调到指定的服务,通过在请求头里加上version标识,在网关路由转发时,只要请求头里的标识和注册到nacos上的服务元数据内的标识匹配就路由到该服务

2.2 代码实现:

  1. 首先为了避免新增或减少一个微服务,需要修改gateway的配置文件和重启服务,使用spring约定大于配置的思想,加上通用的服务发现配置,gateway自己根据请求的名称,去注册中心匹配对应的服务

    1
    2
    3
    4
    5
    6
    spring:
    cloud:
    gateway:
    discovery:
    locator:
    enabled: true
  2. 重写负载均衡拦截器

    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
    @Component
    @ConditionalOnProperty(name = "env-dev", havingValue = "true")
    public class CustomLoadBalancerFilter extends LoadBalancerClientFilter {

    @Autowired
    private DiscoveryClient nacosDiscoveryClient;

    public CustomLoadBalancerFilter(LoadBalancerClient loadBalancer, LoadBalancerProperties properties) {
    super(loadBalancer, properties);
    }

    @Override
    protected ServiceInstance choose(ServerWebExchange exchange) {
    HttpHeaders headers = exchange.getRequest().getHeaders();
    String version = headers.getFirst("version");
    if(StringUtils.isNotBlank(version)) {
    String serviceId = ((URI) exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR)).getHost();
    List<ServiceInstance> instances = nacosDiscoveryClient.getInstances(serviceId);
    if(instances != null && instances.size() > 0) {
    for(ServiceInstance serviceInstance: instances) {
    Map<String, String> metadata = serviceInstance.getMetadata();
    if(metadata.containsKey("version")) {
    if(StringUtils.equals(version, metadata.get("version"))) {
    return serviceInstance;
    }
    }
    }
    }
    }
    return super.choose(exchange);
    }
    }
  3. 解决问题的过程

    1. 首先确定是需要修改负载均衡器

    2. 查看gateway的自动配置类GatewayAutoConfiguration,找到有一个GatewayLoadBalancerClientAutoConfiguration的负载均衡配置类

    3. 在负载均衡的配置类里,可以看到创建了一个LoadBalancerClientFilter的过滤器

    4. 查看过滤器类,其实就是一个全局过滤器的实现

    5. 通过debug,可以发现网关通过final ServiceInstance instance = choose(exchange);这段代码实现从服务列表里选出一个服务实例

    6. 查看choose方法,发现刚好是protected,说明gateway本来就是留好了方便重写的入口,我们只需要继承这个过滤器LoadBalancerClientFilter,并且重写choose方法,就可以实现自定义的负载均衡。

    7. 添加@ConditionalOnProperty(name = "env-dev", havingValue = "true")这段代码主要是为了根据环境区分是否需要加载这个bean,在配置文件中配置了env-dev=true,就走自定义的负载均衡,不配做还是走原来的,不过这个类基本上也没有复杂逻辑,只要请求头里不加version,也不影响性能

2.3 代码地址

https://gitee.com/aacopy/gateway-test.git

3. 聚合微服务swagger

参考:《重新定义Spring Cloud实战》

UI:knife4j

  1. 引入依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.7.0</version>
    </dependency>
    <dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.7.0</version>
    </dependency>
    <dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>swagger-bootstrap-ui</artifactId>
    <version>1.9.5</version>
    </dependency>
  2. 因为swagger不支持webflux,不能在gateway里配置config,需要实现SwaggerResourcesProvider接口,用于获取SwaggerResource,此处参考书上的配置,一直获取不到swagger的API资源,主要是从配置文件读取路由节点时,一直为空,gatewayProperties.getRoutes(),测试发现当网关配置文件配置为从注册中心以服务发现方式获取路由资源时,获取不到,只有挨个配置route才可以获取到,不知道具体原因。

    • 参考书上的获取资源方式版本

      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
      @Component
      @Primary
      public class GatewaySwaggerProvider implements SwaggerResourcesProvider {
      public static final String API_URI = "/v2/api-docs";
      private final RouteLocator routeLocator;
      private final GatewayProperties gatewayProperties;
      public GatewaySwaggerProvider(RouteLocator routeLocator, GatewayProperties gatewayProperties) {
      this.routeLocator = routeLocator;
      this.gatewayProperties = gatewayProperties;
      }
      @Override
      public List<SwaggerResource> get() {
      List<SwaggerResource> resources = new ArrayList<>();
      List<String> routes = new ArrayList<>();
      //获取gateway中的route
      routeLocator.getRoutes().subscribe(route -> routes.add(route.getId()));
      //结合配置文件只获取有效route节点
      gatewayProperties.getRoutes().stream().filter(routeDefinition -> routes.contains(routeDefinition.getId())).forEach(route ->
      route.getPredicates().stream()
      .filter(predicateDefinition -> ("Path").equalsIgnoreCase(predicateDefinition.getName()))
      .forEach(predicateDefinition -> resources.add(swaggerResource(route.getId(),
      predicateDefinition.getArgs().get(NameUtils.GENERATED_NAME_PREFIX + "0")
      .replace("/**", API_URI)))));
      return resources;
      }
      private SwaggerResource swaggerResource(String name, String location) {
      SwaggerResource swaggerResource = new SwaggerResource();
      swaggerResource.setName(name);
      swaggerResource.setLocation(location);
      swaggerResource.setSwaggerVersion("2.0");
      return swaggerResource;
      }
      }
    • 这里需要改为通过服务发现的方式获取路由列表

      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
      @Component
      @Primary
      public class GatewaySwaggerProvider implements SwaggerResourcesProvider {

      public static final String API_URI = "/v2/api-docs";

      private static final String NACOS_ROUTE_ID_PREFIX = "ReactiveCompositeDiscoveryClient_";
      private static final String ROUTE_ID_PREFIX = "CompositeDiscoveryClient_";

      private final DiscoveryClientRouteDefinitionLocator discoveryClientRouteDefinitionLocator;

      public GatewaySwaggerProvider(DiscoveryClientRouteDefinitionLocator discoveryClientRouteDefinitionLocator) {
      this.discoveryClientRouteDefinitionLocator = discoveryClientRouteDefinitionLocator;
      }

      @Override
      public List<SwaggerResource> get() {
      List<SwaggerResource> resources = new ArrayList<>();
      //获取gateway中的route
      Flux<RouteDefinition> routeDefinitions = discoveryClientRouteDefinitionLocator.getRouteDefinitions();
      Flux<SwaggerResource> pattern = routeDefinitions.map(routeDefinition -> swaggerResource(routeDefinition.getId().replace(NACOS_ROUTE_ID_PREFIX, "").replace(ROUTE_ID_PREFIX, ""),
      routeDefinition.getPredicates().get(0).getArgs().get("pattern").replace("/**", API_URI)));
      Disposable subscribe = pattern.subscribe(resources::add);
      int i = 0;
      //最多锁10秒,10秒还没结果就返回空
      while (i<10000) {
      //查看任务是否已完成
      if(subscribe.isDisposed()) {
      return resources;
      }
      try {
      Thread.sleep(100);
      } catch (InterruptedException e) {
      e.printStackTrace();
      }
      i += 100;
      }
      //主动释放任务
      subscribe.dispose();
      return Collections.emptyList();
      }

      private SwaggerResource swaggerResource(String name, String location) {
      SwaggerResource swaggerResource = new SwaggerResource();
      swaggerResource.setName(name);
      swaggerResource.setLocation(location);
      swaggerResource.setSwaggerVersion("2.0");
      return swaggerResource;
      }
      }
  3. 创建SwaggerHeaderFilter,主要是为了解决路由规则为admin/test/{a}/{b}时,缺少根节点admin的情况

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @Component
    public class SwaggerHeaderFilter extends AbstractGatewayFilterFactory {
    private static final String HEADER_NAME = "X-Forwarded-Prefix";

    @Override
    public GatewayFilter apply(Object config) {
    return (exchange, chain) -> {
    ServerHttpRequest request = exchange.getRequest();
    String path = request.getURI().getPath();
    if (!StringUtils.endsWithIgnoreCase(path, GatewaySwaggerProvider.API_URI)) {
    return chain.filter(exchange);
    }
    String basePath = path.substring(0, path.lastIndexOf(GatewaySwaggerProvider.API_URI));
    ServerHttpRequest newRequest = request.mutate().header(HEADER_NAME, basePath).build();
    ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
    return chain.filter(newExchange);
    };
    }
    }
  4. 创建swagger的访问入口

    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
    @RestController
    @RequestMapping("/swagger-resources")
    public class SwaggerController {

    @Autowired(required = false)
    private SecurityConfiguration securityConfiguration;
    @Autowired(required = false)
    private UiConfiguration uiConfiguration;
    private final SwaggerResourcesProvider swaggerResources;

    @Autowired
    public SwaggerController(SwaggerResourcesProvider swaggerResources) {
    this.swaggerResources = swaggerResources;
    }

    @GetMapping("/configuration/security")
    public Mono<ResponseEntity<SecurityConfiguration>> securityConfiguration() {
    return Mono.just(new ResponseEntity<>(
    Optional.ofNullable(securityConfiguration).orElse(new SecurityConfiguration(null, null, null, null, null, ApiKeyVehicle.HEADER, "api_key", ",")), HttpStatus.OK));
    }

    @GetMapping("/configuration/ui")
    public Mono<ResponseEntity<UiConfiguration>> uiConfiguration() {
    return Mono.just(new ResponseEntity<>(
    Optional.ofNullable(uiConfiguration).orElse(new UiConfiguration(null)), HttpStatus.OK));
    }

    @GetMapping("")
    public Mono<ResponseEntity> swaggerResources() {
    return Mono.just((new ResponseEntity<>(swaggerResources.get(), HttpStatus.OK)));
    }
    }
  5. 访问地址:网关ip:port/doc.html

代码地址

https://gitee.com/aacopy/gateway-test.git