本文将分四部分讲解:

  1. SpringCloud Gateway 实现动态路由必要性
  2. SpringCloud Gateway 动态路由源码解析
  3. SpringCloud Gateway 动态路由配置实现方式
  4. SpringCloud Gateway 动态路由配置注意的事项

SpringCloud Gateway 实现动态路由必要性

在实际的生产环境中,如果采用了微服务架构,每次功能迭代发版上线,经常会遇到需要在网关,添加路由配置,如 zuul

zuul:
 ignored-services: '*'
 routes:
    ddc:
      path: /ddc/**
      serviceId: portal-ddc
    pcm:
      path: /pcm/**
      serviceId: portal-pcm

由于采用的是 yml

所以我们需要实现在不重启网关服务的前提下实现添加服务路由零配置升级

SpringCloud Gateway 动态路由源码解析

查看 Spring Cloud Gateway 官网,不幸的是 Gateway 并没有提供类似于 Nacos 控制台配置管理页面给开发者来管理服务的路由信息。



springcloud动态路由和静态路由 spring cloud gateway 动态添加路由_redis


于是笔者翻阅 Gateway

路由相关源码,其内部是提供了路由 CRUD

相关 API

接口的。

GatewayControllerEndpoint 端点

Gateway 通过 GatewayControllerEndpoint 暴露路由 Endpoint 端点进行 CRUD



springcloud动态路由和静态路由 spring cloud gateway 动态添加路由_redis_02

接下来利用 Postman (据说还有个 Postwomen)进行路由 CRUD

  • 添加路由:actuator/gateway/routes/{id}

springcloud动态路由和静态路由 spring cloud gateway 动态添加路由_spring_03


  • 删除路由:actuator/gateway/routes/{id}

springcloud动态路由和静态路由 spring cloud gateway 动态添加路由_redis_04


  • 查询单条路由:actuator/gateway/routes/{id}

springcloud动态路由和静态路由 spring cloud gateway 动态添加路由_数据库_05

  • 查询所有路由:actuator/gateway/routes

springcloud动态路由和静态路由 spring cloud gateway 动态添加路由_mysql_06


另外,如果想访问 GatewayControllerEndpoint

端点中的方法,需要在 Gateway

添加 spring-boot-starter-actuator

依赖

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

并在 yml

management:
 endpoints:
  web:
   exposure: 
    include: "*"

打开浏览器输入 actuator 地址:http://localhost:8080/actuator/,如果找到 Gateway 端点信息:http://localhost:8080/actuator/gateway,说明可以通过 GatewayControllerEndpoint 进行 CRUD 操作了



springcloud动态路由和静态路由 spring cloud gateway 动态添加路由_redis_07



SpringCloud Gateway 动态路由配置实现方式

除了使用 GatewayControllerEndpoint 可以配置路由之外还可以利用 RouteLocatorBuilder

@SpringBootApplication
 public class DemogatewayApplication {

 @Bean
 public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
   return builder.routes()
     .route("path_route", r -> r.path("/get")
     .uri("http://httpbin.org"))
     .route("host_route", r -> r.host("*.myhost.org")
     .uri("http://httpbin.org"))
     .route("rewrite_route", r -> r.host("*.rewrite.org")
     .filters(f -> f.rewritePath("/foo/(?<segment>.*)", "/${segment}"))
     .uri("http://httpbin.org"))
     .route("hystrix_route", r -> r.host("*.hystrix.org")
     .filters(f -> f.hystrix(c -> c.setName("slowcmd")))
     .uri("http://httpbin.org"))
     .route("hystrix_fallback_route", r -> r.host("*.hystrixfallback.org")
     .filters(f -> f.hystrix(c -> c.setName("slowcmd").setFallbackUri("forward:/hystrixfallback")))
     .uri("http://httpbin.org"))
     .route("limit_route", r -> r
     .host("*.limited.org").and().path("/anything/**")
     .filters(f -> f.requestRateLimiter(c -> c.setRateLimiter(redisRateLimiter())))
     .uri("http://httpbin.org"))
     .build();
  }
}

另外,如果不嫌麻烦,可以利用 RouteDefinitionWriter

public interface RouteDefinitionWriter {
 Mono<Void> save(Mono<RouteDefinition> route);
 Mono<Void> delete(Mono<String> routeId);
}

默认情况下,Spring Cloud Gateway 使用内存方式(HashMap)存储路由信息。



springcloud动态路由和静态路由 spring cloud gateway 动态添加路由_数据库_08

其实现逻辑在 InMemoryRouteDefinitionRepository



springcloud动态路由和静态路由 spring cloud gateway 动态添加路由_redis_09

通过查看类图,我们知道 InMemoryRouteDefinitionRepositoryRouteDefinitionWriter

这里给我们一个很大启发,是否可以利用 RouteDefinitionWriter 自定义实现类,把路由信息存储到 mysqlredis 或者 mongo

答案是可以的。

例如,我们利用 Redis 缓存路由信息,只需在 RouteDefinitionWriter 实现类 RedisRouteDefinitionRepository 中添加 redisTemplate 注解,进行路由信息的 CRUD

@Component
public class RedisRouteDefinitionRepository implements RouteDefinitionRepository {
 public static final String GW_ROUTES = "apis_gateway_routes";
 
  @Autowired
  private StringRedisTemplate redisTemplate;
  
  @Override
  public Flux<RouteDefinition> getRouteDefinitions() {
   List<RouteDefinition> routeDefinitions = new ArrayList<>();
   redisTemplate.opsForHash().values(GW_ROUTES).stream()
  .forEach(routeDefinition -> routeDefinitions.add(JSON.parseObject(routeDefinition.toString(),  RouteDefinition.class)));
   return Flux.fromIterable(routeDefinitions);
  }
  
  @Override
  public Mono<Void> save(Mono<RouteDefinition> route) {
    RouteDefinition definition = new RouteDefinition();
    definition.setId("id");
    URI uri = UriComponentsBuilder.fromHttpUrl("lb://consumer-service").build().toUri();
    definition.setUri(uri);
    PredicateDefinition predicate = new PredicateDefinition();
    predicate.setName("Path");
    Map<String, String> predicateArgs = new HashMap<>();
    predicateArgs.put("pattern", "/consumer/**");
    predicate.setArgs(predicateArgs);
    definition.setPredicates(Arrays.asList(predicate));
    FilterDefinition filter = new FilterDefinition();
    filter.setName("StripPrefix");
    Map<String, String> filterArgs = new HashMap<>();
    filterArgs.put("_genkey_0", "1");
    filter.setArgs(filterArgs);
    definition.setFilters(Arrays.asList(filter));
    redisTemplate.opsForHash().put(GW_ROUTES, "routeKey", JSON.toJSONString(definition));
    return null;
  }
  
  @Override
  public Mono<Void> delete(Mono<String> routeId) {
   return null;
  }
}

提供 REST 对外接口,对路由进行 CRUD 操作,最后,每次完成 save 或者 delete 删除,然后发一个 RefreshRoutesEvent 事件,通知 Gateway

@RestController
@RequestMapping("/routes")
public class RouteController implements ApplicationEventPublisherAware {

  @Autowired
  private RedisRouteDefinitionRepository routeDefinitionWriter;
  
  private ApplicationEventPublisher publisher;

  @Override
  public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
   this.publisher = publisher;
  }
  
  @PostMapping
  public String addRoute(@RequestBody RouteDefinition definition) {
   routeDefinitionWriter.save(Mono.just(definition)).subscribe();
   this.publisher.publishEvent(new RefreshRoutesEvent(this));
   return "0";
  }
  
  @GetMapping("/{id}")
  public String delete(@PathVariable String id) {
    this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
    this.publisher.publishEvent(new RefreshRoutesEvent(this));
    return "0";
  }
}

如果自定义 RouteDefinitionWriter 的实现类,就会替换 InMemoryRouteDefinitionRepository从而当 rest 接口发送 RefreshRoutesEvent 刷新路由事件后, CachingRouteDefinitionLocator 刷新 Gateway

SpringCloud Gateway 动态路由配置注意的事项

在实际的生产环境中,Gateway网关一般是多实例部署,那么基于 InMemoryRouteDefinitionRepository

因为每次通过 Gatewayrest 接口只会更新某个 Gateway

这就解释为什么要用 redis

这样当 Gateway 节点灰度重启或者在 Gateway 内置定时 job 刷新时,就可以通过 RedisRouteDefinitionRepositorygetRouteDefinitions 方法 从 redis

springcloud动态路由和静态路由 spring cloud gateway 动态添加路由_redis_10