SpringCloud-gateway Redis动态路由及 各EndPoint介绍

Spring Cloud Gateway 微服务网关

  • 创建项目(命名 为 Gateway), 添加pom 依赖

    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-gateway</artifactId>
      <version>2.2.0.RELEASE</version>
    </dependency>
    
    <dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
      <version>2.2.0.RELEASE</version>
    </dependency>
    
  • Router 路由器:包括,11种路由模式,具体参见官网

    • 添加路由配置: 意思是所有路径匹配到 /acc/** ,都会路由到 lb://account-service

      spring:
        cloud:
          gateway:
            routes:
            - id: host_route
              uri: lb://account-service
              predicates:
              - Path=/acc/**
      
    • 在启动类添加注解,服务注册和发现

      @EnableDiscoveryClient
      public class GatewayApplication {
      }
      
    • 测试

      • 默认路由地址:http://localhost:9001/account-service/acc/user?id=2
      • 使用Path路由配置:http://localhost:9001/acc/user?id=2
    • 打开gateway 监控端点 ,各端点介绍,参见Gateway管理API

      <!-- 添加 actuator 依赖 -->
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
      </dependency>
      
      #将 gateway 端点暴露
      management.endpoint.gateway.enabled=true 
      management.endpoints.web.exposure.include=gateway
      
      查询全局过滤器, http://localhost:9001/actuator/gateway/globalfilters
      查询路由中的过滤器, http://localhost:9001/actuator/gateway/routefilters
      刷新路由缓存, http://localhost:9001/actuator/gateway/refresh
      
      展示路由列表,  http://localhost:9001/actuator/gateway/routes
      获取单个路由的信息, http://localhost:9001/actuator/gateway/routes/host_route
      创建路由,发送Post请求,  ../actuator/gateway/routes/{id_route_to_create}
      删除路由,发送Delete请求: ../actuator/gateway/routes/{id_route_to_delete}
      
  • Filter 过滤器

    • 网关经常需要对路由请求进行过滤,如鉴权之后构造头部之类的,过滤的种类很多,如增加请求头、增加请求参数 、增加响应头和断路器等等功能,这就用到了Spring Cloud Gateway 的 Filter。

    • Filter 的作用:

      • 当我们有很多个服务时,客户端请求各个服务的Api时,每个服务都需要做相同的事情,比如鉴权、限流、日志输出等。
      • 因此可以将这些功能等转移到网关处理以减少重复开发。
    • Filter 的生命周期

      • PRE: 这种过滤器在被代理的微服务(Proxied Service)执行之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
      • POST:这种过滤器在被代理的微服务(Proxied Service)执行完成后执行。这种过滤器可用来为响应添加标准的 HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
    • Filter 的分类

    • GatewayFilter:应用到单个路由或者一个分组的路由上。自定义的GatewayFilter,同样需要指定某个路由器

    • GlobalFilter:应用到所有的路由上。当请求到来时,Filtering Web Handler 处理器会添加所有 GlobalFilter 实例和匹配的 GatewayFilter 实例到过滤器链中。因此,自定义的GlobalFilter,不需要什么额外的配置。

    • 自定义 GatewayFilter,有两种实现方式

      • 一种是直接 实现GatewayFilter接口,不过还需要手动将自定义的GatewayFilter 注册到 router中

        // 统计某个或者某种路由的处理时长
        public class CustomerGatewayFilter implements GatewayFilter, Ordered {
            private static final Logger log = LoggerFactory.getLogger( CustomerGatewayFilter.class );
            private static final String COUNT_START_TIME = "countStartTime";
        
            @Override
            public Mono<Void> filter(ServerWebExchange exch, GatewayFilterChain chain) {
                return chain.filter(exch).then(
                    Mono.fromRunnable(() -> {
                      long startTime = exch.getAttribute(COUNT_START_TIME);
                      long endTime=(Instant.now().toEpochMilli() - startTime);
                      log.info(exch.getRequest().getURI().getRawPath() + ": " + endTime + "ms");
                  })
                );
            }
        
            @Override
            public int getOrder() {
                return 0;
          }
        }
        
      • 另一种是 自定义过滤器工厂(继承AbstractGatewayFilterFactory类) , 选择自定义过滤器工厂的方式,可以在配置文件中配置过滤器

        @Component
        public class CustomerGatewayFilterFactory extends AbstractGatewayFilterFactory<CustomerGatewayFilterFactory.Config> {
        
            private static final Logger log = LoggerFactory.getLogger( CustomerGatewayFilterFactory.class );
            private static final String COUNT_START_TIME = "countStartTime";
        
            @Override
            public List<String> shortcutFieldOrder() {
                return Arrays.asList("enabled");
            }
        
            public CustomerGatewayFilterFactory() {
                super(Config.class);
                log.info("Loaded GatewayFilterFactory [CustomerGatewayFilterFactory]");
            }
        
            @Override
            public GatewayFilter apply(Config config) {
                return (exchange, chain) -> {
                    if (!config.isEnabled()) {
                      return chain.filter(exchange);
                    }
                    exchange.getAttributes().put(COUNT_START_TIME, 
                                                 System.currentTimeMillis());
                  
                  return chain.filter(exchange).then(
                        Mono.fromRunnable(() -> {
                            Long startTime = exchange.getAttribute(COUNT_START_TIME);
                            if (startTime != null) {
                                StringBuilder sb = new StringBuilder(
                                  exchange.getRequest().getURI().getRawPath()
                                ).append(": ")
                                 .append(System.currentTimeMillis() - startTime)
                                 .append("ms");
        
                                sb.append(" params:")
                                  .append(exchange.getRequest().getQueryParams());
        
                                log.info(sb.toString());
                            }}));
                };
            }
        
            public static class Config {
                //控制是否开启统计
                private boolean enabled;
                public Config() {}
                // getter setter 省略
            }
        }
        
        spring:
          cloud:
            gateway:
              routes:
                - id: elapse_route
                  uri: lb://account-service
                  filters:
                  - Customer=true
                  predicates:
                  - Method=GET
        
    • 自定义GlobalFilter : 可以在这里实现授权、日志等功能

      @Component
      public class AuthorizeFilter implements GlobalFilter, Ordered {
          private static final Logger log = LoggerFactory.getLogger(AuthorizeFilter.class);
          private static final String AUTHORIZE_TOKEN = "token";
      
          @Override
          public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain){
              String token = exchange.getRequest().getQueryParams()
                .getFirst(AUTHORIZE_TOKEN);
      
              if ( StringUtils.isBlank( token )) {
                  log.info( "token is empty ..." );
                  exchange.getResponse().setStatusCode( HttpStatus.UNAUTHORIZED );
                  return exchange.getResponse().setComplete();
              }
              return chain.filter(exchange);
          }
      
          @Override
          public int getOrder() {
              return 0;
          }
      }
      
  • Gateway + Nacos 实现动态路由 , 参见 springcloud-demo

    • 路由信息定义在配置文件中,这种方式有一个缺点就是修改路由信息必须重启服务才能生效。网关作为全部流量的入口,可用时间当然越长越好,不重启服务而修改路由是一个更好的选择,结合Nacos可以做到这一点。在Gateway项目中添加 nacos config 依赖,并配置nacos地址

      <dependency>
          <groupId>com.alibaba.cloud</groupId>
          <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
      </dependency>
      
      spring.cloud.nacos.config.server-addr=127.0.0.1:8848
      
  • Spring Cloud Gateway本身还不支持直接从Nacos动态加载路由配置表,需要自己编写监听器监听配置变化并刷新路由表
  • 方法1:(save和delete方法,供使用gateway-endpoint 访问时 添加和删除路由时使用)

    @Component
    public class NacosDynamicRouteService implements ApplicationEventPublisherAware {
        @Override
        public void setApplicationEventPublisher(ApplicationEventPublisher aep) {
            this.applicationEventPublisher = aep;
        }
    
        private ApplicationEventPublisher applicationEventPublisher;
    
        private String dataId = "gateway-router";
        private String group = "DEFAULT_GROUP";
        @Value("${spring.cloud.nacos.config.server-addr}")
        private String serverAddr;
    
        @Autowired
        private RouteDefinitionWriter routeDefinitionWriter;
    
        private static final List<String> ROUTE_LIST = new ArrayList<>();
    
        @PostConstruct
        public void dynamicRouteByNacosListener() {
            try {
                ConfigService configService = NacosFactory
                     .createConfigService(serverAddr);
    
                // springboot 启动时,获取配置信息
                String configInfo = configService.getConfig(dataId, group, 5000);
                configRouter(configInfo);
                // 监听 nacos 上的配置文件,当配置文件被修改后,获取配置信息
                configService.addListener(dataId, group, new Listener() {
                    @Override
                    public void receiveConfigInfo(String configInfo) {
                        configRouter(configInfo);
                    }
    
                    @Override
                    public Executor getExecutor() {
                        return null;
                    }
                });
            } catch (NacosException e) {
                e.printStackTrace();
            }
        }
    
        private void configRouter(String configInfo){
            clearRoute();
            try {
                List<RouteDefinition> gatewayRouteDefinitions = JSONObject.parseArray(
                    configInfo, RouteDefinition.class);
    
                for (RouteDefinition routeDefinition : gatewayRouteDefinitions) {
                    addRoute(routeDefinition);
                }
                publish();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        private void clearRoute() {
            for(String id : ROUTE_LIST) {
                this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
            }
            ROUTE_LIST.clear();
        }
    
        private void addRoute(RouteDefinition definition) {
            try {
                routeDefinitionWriter.save(Mono.just(definition)).subscribe();
                ROUTE_LIST.add(definition.getId());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        private void publish() {
            this.applicationEventPublisher.publishEvent(
                new RefreshRoutesEvent(this.routeDefinitionWriter));
        }
    }
    
  • 方法2:(仅支持集群环境 各机器同步路由配置,无法根据nacos配置主动更新)

  • 自定义RouteDefinitionWriter
    Spring Cloud Gateway默认的RouteDefinitionWriter实现类是org.springframework.cloud.gateway.route.InMemoryRouteDefinitionRepository,Route信息保存在当前实例的内存中,这在集群环境中会存在同步问题。我们可以自定义一个基于Redis的RouteDefinitionWriter。
    
    import java.util.ArrayList;
    import java.util.List;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.cloud.gateway.route.RouteDefinition;
    import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
    import org.springframework.cloud.gateway.support.NotFoundException;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.stereotype.Component;
    
    import com.alibaba.fastjson.JSON;
    
    import reactor.core.publisher.Flux;
    import reactor.core.publisher.Mono;
    
    /**
     * 使用Redis保存自定义路由配置(代替默认的InMemoryRouteDefinitionRepository)
     * <p/>
     * 存在问题:每次请求都会调用getRouteDefinitions,当网关较多时,会影响请求速度,考虑放到本地Map中,使用消息通知Map更新。
     *
     * @since 2018年7月9日 下午2:39:02
     */
    @Component
    public class RedisRouteDefinitionRepository implements RouteDefinitionRepository {
    
        public static final String  GATEWAY_ROUTES = "geteway_routes";
        @Autowired
        private StringRedisTemplate redisTemplate;
    
        @Override
        public Flux<RouteDefinition> getRouteDefinitions() {
            List<RouteDefinition> routeDefinitions = new ArrayList<>();
            redisTemplate.opsForHash().values(GATEWAY_ROUTES).stream().forEach(routeDefinition -> {
                routeDefinitions.add(JSON.parseObject(routeDefinition.toString(), RouteDefinition.class));
            });
            return Flux.fromIterable(routeDefinitions);
        }
    
        @Override
        public Mono<Void> save(Mono<RouteDefinition> route) {
            return route
                    .flatMap(routeDefinition -> {
                        redisTemplate.opsForHash().put(GATEWAY_ROUTES, routeDefinition.getId(),
                                JSON.toJSONString(routeDefinition));
                        return Mono.empty();
                    });
        }
    
        @Override
        public Mono<Void> delete(Mono<String> routeId) {
            return routeId.flatMap(id -> {
                if (redisTemplate.opsForHash().hasKey(GATEWAY_ROUTES, id)) {
                    redisTemplate.opsForHash().delete(GATEWAY_ROUTES, id);
                    return Mono.empty();
                }
                return Mono.defer(() -> Mono.error(new NotFoundException("RouteDefinition not found: " + routeId)));
            });
        }
    
    }
     

     

  • 在 Nacos 管理后台,创建配置文件 gateway-router.json ,内容如下。

    [{
        "id": "account-router",
        "order": 0,
        "predicates": [{"args": {"pattern": "/acc/**"},"name": "Path" }],
        "filters":[{"args": {"pattern": true },"name": "Customer" }],
        "uri": "lb://account-service"
    }]
    
  • 同时删除原来在 application.xml 中关于路由的配置

  • 测试:http://localhost:9001/acc/user?id=2

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值