Spring Cloud Gateway基础及动态路由实现

571 阅读4分钟

更多精彩欢迎关注公众号:闲人张

基本概念

Spring Cloud Gateway旨在为微服务架构提供简单、有效和统一的API路由管理方式,Spring Cloud Gateway作为Spring Cloud生态系统中的网关,目标是替代Netflix Zuul,其不仅提供统一的路由方式,并且还基于Filer链的方式提供了网关基本的功能,例如:安全、监控/埋点、限流等。

实现原理

Spring Cloud Gateway 底层使用了高性能的通信框架Netty,启动一个netty server(默认端口为8080)接受请求,然后通过Routes(每个Route由Predicate(等同于HandlerMapping)和Filter(等同于HandlerAdapter))处理后通过Netty Client发给响应的微服务。 具体配置在org.springframework.cloud.gateway.config.GatewayAutoConfiguration.NettyConfiguration中。

image.png

名词解释

  • Route(路由):路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由。
  • Predicate(断言):参考的是java8的java.util.function.Predicate开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由。
  • Filter(过滤):指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。

实现方式

  • 基础URI一种路由配置方式
spring:
  cloud:
    gateway:
      routes:
        -id: proxy-1
          uri: https://www.baidu.com
          predicates:
            -Path=/bd

id:我们自定义的路由 ID,保持唯一 uri:目标服务地址 predicates:路由条件,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)

  • 和注册中心相结合的路由配置方式
spring:
  cloud:
    gateway:
    # 以服务名方式路由
      discovery:
        locator:
          # 开启基于eureka服务名路径方式路由(不需要每次配置重启服务)
          enabled: true
          # 路由服务名可以是小写
          lowerCaseServiceId: true
      routes:
        -id: proxy-2
          uri: lb://demo-service.provider
          predicates:
            - Path=/app/**
          # 将以app开头的请求转发到demo-service.provider
          filters:
          # StripPrefix 转发时去掉第一个前缀/app
            - StripPrefix=1
          # PrefixPath 转发时实际路径会加上这个前缀 即 访问/test 实际请求为/mapath/test
            - PrefixPath=/mypath 
          # 访问localhost:8080/demo/v1/api/logs, 请求会转发到localhost:8001/v1/api/logs
            - RewritePath=/demo/v1/api/logs, /v1/api/logs
  • 基于代码的路由配置方式
  @Bean
  public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
    return builder.routes()
        .route(r -> r.path("/bd").uri("http://www.baidu.com"))
        .route("id",r -> r.path("/app/**").filters(f -> f.stripPrefix(1)).uri("lb://demo-service.provider"))
        .build();
  }

动态路由

RouteDefinitionRepository 继承 RouteDefinitionLocator & RouteDefinitionWriter,可通过实现RouteDefinitionLocator 或 RouteDefinitionRepository 自定义路由。

RouteDefinitionLocator 是路由定义定位器的顶级接口,它的主要作用就是读取路由的配置信息,只有getRouteDefinitions()一个方法。 从org.springframework.cloud.gateway.route.RouteDefinition里面读取

RouteDefinitionRouteLocator 与 RouteDefinitionLocator比较容易混淆,前者是一个RouteLocator(路由定位器), 后者是一个RouteDefinitionLocator(路由定义定位器),前者的 RouteDefinitionRouteLocator 主要从后者获取路由定义信息。

在WeightCalculatorWebFilter.java中有 RefreshRoutesEvent 每隔30s 调用 getRouteDefinitions

每次请求都会调用getRouteDefinitions,当网关较多时,会影响请求速度,考虑放到本地Map中,使用消息通知Map更新 RouteDefinitionWriter 用来实现路由的添加与删除。 默认 InMemoryRouteDefinitionRepository数据没有持久化。

路由的配置存储应该加入版本控制。

数据库表结构

CREATE TABLE `route` (
	`id` BIGINT ( 20 ) NOT NULL AUTO_INCREMENT,
	`server_id` VARCHAR ( 50 ) COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '服务Id',
	`uri` VARCHAR ( 100 ) COLLATE utf8mb4_bin NOT NULL COMMENT '服务路径',
	`predicates` VARCHAR ( 500 ) COLLATE utf8mb4_bin NOT NULL COMMENT '服务凭证',
	`filters` VARCHAR ( 1000 ) COLLATE utf8mb4_bin NOT NULL COMMENT '服务过滤',
	`order` INT ( 11 ) NOT NULL DEFAULT '0' COMMENT '调用权重',
	`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
	`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
	`remarks` VARCHAR ( 100 ) COLLATE utf8mb4_bin DEFAULT '' COMMENT '描述',
	`enable` INT ( 11 ) NOT NULL DEFAULT '1' COMMENT '是否生效',
	PRIMARY KEY ( `id` ) USING BTREE,
UNIQUE KEY `unique_server_id` ( `server_id` ) USING BTREE 
) ENGINE = INNODB AUTO_INCREMENT = 1233 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_bin;

动态路由实现

@Component
@Slf4j
public class MyRouteDefinitionRepository implements RouteDefinitionRepository,ApplicationListener<RefreshRoutesEvent> {

  private static final Map<String, RouteDefinition> ROUTES_CACHE = new ConcurrentHashMap<>();

  @Resource
  private RouteMapper routeMapper;

  @Resource
  private ApplicationEventPublisher applicationEventPublisher;

  @Override
  public Flux<RouteDefinition> getRouteDefinitions() {
    return Flux.fromIterable(ROUTES_CACHE.values());
  }

  @Override
  public Mono<Void> save(Mono<RouteDefinition> route) {
    return route.flatMap(routeDefinition -> {
      if(!ROUTES_CACHE.containsKey(routeDefinition.getId())){
        ROUTES_CACHE.put(routeDefinition.getId(),routeDefinition);
      }
      return Mono.empty();
    });
  }

  @Override
  public Mono<Void> delete(Mono<String> routeId) {
    return routeId.flatMap(id -> {
      if(ROUTES_CACHE.containsKey(id)){
        ROUTES_CACHE.remove(id);
      }
      return Mono.empty();
    });
  }

  @Override
  public void onApplicationEvent(RefreshRoutesEvent refreshRoutesEvent) { }

  public void refreshRoutes() {
    getRoutes();
    applicationEventPublisher.publishEvent(new RefreshRoutesEvent(""));
  }

  private void getRoutes(){
    List<Route> routeList = routeMapper.selectAll();
    if(!CollectionUtils.isEmpty(routeList)){
      ROUTES_CACHE.clear();
      routeList.forEach(route -> {
        //RouteDefinition 作为GatewayProperties中的属性,在网关启动的时候读取配置文件中的相关配置信息
        RouteDefinition definition = new RouteDefinition();
        definition.setId(route.getServerId());

        List<PredicateDefinition> predicates = Lists.newArrayList();
        List<FilterDefinition> filters = Lists.newArrayList();

        List<RouteParameters> routePredicates = JSONArray.parseArray(route.getPredicates(),RouteParameters.class);
        routePredicates.forEach(routePredicate -> {
          PredicateDefinition predicateDefinition = new PredicateDefinition();
          //名称是固定的,spring gateway会根据名称找对应的PredicateFactory
          predicateDefinition.setName(routePredicate.getName());
          predicateDefinition.setArgs(routePredicate.getArgs());
          predicates.add(predicateDefinition);
        });
        definition.setPredicates(predicates);

        List<RouteParameters> routeFilters = JSONArray.parseArray(route.getFilters(),RouteParameters.class);
        routeFilters.forEach(routeFilter -> {
          FilterDefinition filterDefinition = new FilterDefinition();
          filterDefinition.setName(routeFilter.getName());
          filterDefinition.setArgs(routeFilter.getArgs());
          filters.add(filterDefinition);
        });
        definition.setFilters(filters);

        URI uri = UriComponentsBuilder.fromUriString(route.getUri()).build().toUri();
        definition.setUri(uri);
        definition.setOrder(route.getOrder());
        save(Mono.just(definition)).subscribe();
      });
    }
  }
}

可通过restful接口刷新,实现路由刷新!

更多精彩欢迎关注公众号:闲人张