SpringCloud Gateway的核心概念

SpringCloud Gateway是Spring官方最新推出的一款基于SpringFramework5,Project Reactor和SpringBoot 2之上开发的网关。
它与第一代网关Zuul不同的是:gateway是异步非阻塞的(netty + webflux实现);zuul是同步阻塞请求的
Gateway由三大组成部分,分别是路由、断言、过滤器。

谓词Predicate

谓词Predicate对于熟悉Java8函数式编程的开发者来说并不会陌生,这里不做累述。简单地理解,predicate在gateway中是路径相匹配的进行路由。

静态路由配置与动态路由配置

静态路由的配置

静态路由配置写在配置文件中(yml或者properties文件中),端点是:spring.cloud.gateway

静态路由的缺点非常明显,每次改动都要重新部署网关模块。

动态路由配置

路由信息在Nacos中维护,可以实现动态变更

Gateway集成Alibaba Nacos实现动态路由配置

第一步:启动nacos,创建配置文件,如下图所示:

具体的配置信息如下:

[
    {
        "id": "e-commerce-nacos-client-imooc",
        "predicates": [
            {
                "args": {
                    "pattern": "/imooc/ecommerce-nacos-client/**"
                },
                "name": "Path"
                }
         ],
         "uri": "lb://e-commerce-nacos-client"
    }
]
复制代码

第二步:创建Gateway工程

引入的依赖有:

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    <version>2.2.3.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
复制代码

bootstrap.yml的配置如下:

server:
  port: 9001
  servlet:
    context-path: /imooc

spring:
  application:
    name: e-commerce-gateway
  cloud:
    nacos:
      discovery:
        enabled: true # 如果不想使用 Nacos 进行服务注册和发现, 设置为 false 即可
        server-addr: 127.0.0.1:8848 # Nacos 服务器地址
        namespace: fa2e39fc-7e9b-4b8c-bd35-53543d2e1d8a
        metadata:
          management:
            context-path: ${server.servlet.context-path}/actuator
    # 静态路由
  #    gateway:
  #      routes:
  #        - id: path_route # 路由的ID
  #          uri: 127.0.0.1:8080/user/{id} # 匹配后路由地址
  #          predicates: # 断言, 路径相匹配的进行路由
  #            - Path=/user/{id}
  main:
    allow-bean-definition-overriding: true  # 因为将来会引入很多依赖, 难免有重名的 bean

# 这个地方独立配置, 是网关的数据, 代码 GatewayConfig.java 中读取被监听
nacos:
  gateway:
    route:
      config:
        data-id: e-commerce-gateway-router
        group: e-commerce

# 暴露端点
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always
复制代码

第三步:在gateway微服务应用中实现一个配置类,用来读取nacos上的配置信息。

/**
 * 配置类,用来读取nacos相关的配置项,用于配置***
 */
@Configuration
public class GatewayConfig {
    
    /**
     * 读取配置的超时时间
     */
    public static final long DEFAULT_TIMEOUT = 30000;


    public static String NACOS_SERVER_ADDR;

    /**
     * 命名空间
     */
    public static String NACOS_NAMESPACE;

    /**
     * data-id
     */
    public static String NACOS_ROUTE_DATA_ID;


    public static String NACOS_ROUTE_GROUP;

    @Value("${spring.cloud.nacos.discovery.server-addr}")
    public  void setNacosServerAddr(String nacosServerAddr){
        NACOS_SERVER_ADDR = nacosServerAddr;
    }

    @Value("${spring.cloud.nacos.discovery.namespace}")
    public void setNacosNamespace(String nacosNamespace){
        NACOS_NAMESPACE = nacosNamespace;
    }

    @Value("${nacos.gateway.route.config.data-id}")
    public void setNacosRouteDataId(String nacosRouteDataId){
        NACOS_ROUTE_DATA_ID = nacosRouteDataId;
    }

    @Value("${nacos.gateway.route.config.group}")
    public void setNacosRouteGroup(String nacosRouteGroup){
        NACOS_ROUTE_GROUP = nacosRouteGroup;
    }
}
复制代码

第四步:定义一个service的类,DynamicRouteServiceImpl,实现ApplicationEventPublisherAware接口。

ApplicationEventPublisherAware接口是Spring的事件机制,实现这个类需要实现句柄的初始化,而句柄是用来推送消息的。事件机制有点想消息队列以及kafka,即可以推送消息,也有监听者进行监听,一旦监听者监听到消息就会处理相应的逻辑。
推送消息到gateway的路由配置只需要一行代码:

this.publisher.publishEvent(new RefreshRoutesEvent(this));
复制代码

我们可以看到RefreshRoutesEvent就是我们要推送的事件的一个类,如果是我们自己定义的事件推送的话,就需要我们自己定义事件类,并且定义一个监听类来监听这个事件。 但是在Spring Cloud中已经帮我们实现了RefreshRoutesEvent这个事件类以及***RouteRefreshListener。 在源码中如下图所示:

所以说,我们推送消息到网关中的时候只需要上面的一行代码就可以了。
更新route路由的实现类具体代码如下:

**
 * 事件推送Aware:动态更新路由网关Service
 */
@Slf4j
@Service
@SuppressWarnings("all")
public class DynamicRouteServiceImpl implements ApplicationEventPublisherAware {

    /**
     * 写路由定义
     */
    private final RouteDefinitionWriter routeDefinitionWriter;

    /**
     * 获取路由定义
     */
    private final RouteDefinitionLocator routeDefinitionLocator;

    /**
     * 事件发布
     */
    private ApplicationEventPublisher publisher;

    public DynamicRouteServiceImpl(RouteDefinitionWriter routeDefinitionWriter,
                                   RouteDefinitionLocator routeDefinitionLocator){
        this.routeDefinitionWriter = routeDefinitionWriter;
        this.routeDefinitionLocator  = routeDefinitionLocator;
    }

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher
                                                         applicationEventPublisher) {
        // 完成事件推送句柄的初始化
        this.publisher = applicationEventPublisher;
    }

    /**
     * 增加路由定义
     * @param definition
     * @return
     */
    public String addRouteDefinition(RouteDefinition definition){
        log.info("gateway add route: [{}]",definition);

        //保存路由配置并发布
        routeDefinitionWriter.save(Mono.just(definition)).subscribe();
        this.publisher.publishEvent(new RefreshRoutesEvent(this));

        return "success";
    }

    /**
     * 更新路由
     * @param definitions
     * @return
     */
    public String updateList(List<RouteDefinition> definitions){
        log.info("gateway update route: [{}]",definitions);

        //先拿到当前Gateway 中存储的路由定义
        List<RouteDefinition> routeDefinitionsExits =
                routeDefinitionLocator.getRouteDefinitions().buffer().blockFirst();

        if (!CollectionUtils.isEmpty(routeDefinitionsExits)){
            //清除掉之前所有的“旧的”路由定义
            routeDefinitionsExits.forEach(rd ->{
                log.info("delete route definition: [{}]",rd);
                deleteById(rd.getId());
            });

            //把更新的路由定义同步到gateway中
            definitions.forEach(definition -> updateByRouteDefinition(definition));

        }
        return "success";
    }

    /**
     * 根据路由id删除路由配置
     */
    private String deleteById(String id){


        try {
            log.info("gateway delete route id: [{}]",id);
            this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
            //发布事件通知给gateway 更新路由定义
            this.publisher.publishEvent(new RefreshRoutesEvent(this));
            return "delete success";
        }catch (Exception ex){
            log.error("gateway delete route fail:[{}]",ex.getMessage(),ex);
            return "delete fail";
        }
    }


    /**
     * 更新路由
     * 更新的实现策略比较简单: 删除 + 新增 = 更新
     * @param definition
     * @return
     */
    private String updateByRouteDefinition(RouteDefinition definition){

        try{
            log.info("gateway update route: [{}]",definition);
            this.routeDefinitionWriter.delete(Mono.just(definition.getId())).subscribe();
        }catch (Exception ex){
            return "update fail, not find route routeId: " + definition.getId();
        }

        try{
            this.routeDefinitionWriter.save(Mono.just(definition)).subscribe();
            this.publisher.publishEvent(new RefreshRoutesEvent(this));

            return "update success";
        }catch (Exception ex){
            return "update route fail";
        }
    }
}
复制代码

我们在该类中定义了写路由定义routeDefinitionWriter,用来往路由中进行更新以及保存的操作。为了能够读取路由的配置,我们还定义了读路由定义routeDefinitionLocator,用来读取路由的相关配置,因为在更新路由的时候我们要先读取旧的路由配置,然后把就的路由配置删除,再写入新的路由配置,所以需要用来读路由的配置。

第五步:定义一个类来监听Nacos上配置文件的变化
我们定义一个类叫做DynamicRouteServiceImplByNacos,其中这个类的上面有一个注解是

@DependsOn({"gatewayConfig"})
复制代码

表示这个类依赖gatewayConfig这个bean,就是说先有gatewayConfig这个bean,因为我们要读取nacos上的配置信息。
其中需要两个类变量,分别是

/**
 * Nacos 配置服务
 */
private ConfigService configService;

private final DynamicRouteServiceImpl dynamicRouteService;

// 通过构造方法注入
public DynamicRouteServiceImplByNacos(DynamicRouteServiceImpl dynamicRouteService) {
    this.dynamicRouteService = dynamicRouteService;
}
复制代码

DynamicRouteServiceImpl就是我们刚才实现的那个类。 configService是Nacos配置服务。需要我们自己实现一个方法去初始化,而不是通过注入的形式。 首先,我们定义初始化Nacos Config配置的方法。

/**
 * 初始化 Nacos Config
 */
private ConfigService initConfigService(){

    try{
        Properties properties = new Properties();
        properties.setProperty("serverAddr", GatewayConfig.NACOS_SERVER_ADDR);
        properties.setProperty("namespace",GatewayConfig.NACOS_NAMESPACE);
        return configService = NacosFactory.createConfigService(properties);

    }catch (Exception ex){
        log.error("init gateway nacos config error: [{}]",ex.getMessage(),ex);
        return null;
    }
}
复制代码

我们再定一个方法区监听Nacos上指定配置文件的方法。

/**
 * 监听 Nacos 下发的动态路由配置
 * @param dataId
 * @param group
 */
private void dynamicRouteByNacosListener(String dataId, String group){
    try {
        //给 Nacos Config客户端增加一个***
        configService.addListener(dataId, group, new Listener() {

            /**
             * 自己提供线程池执行操作
             * @return
             */
            @Override
            public Executor getExecutor() {
                return null;
            }

            /**
             * ***收到配置更新
             * @param configInfo Nacos 中最新的配置定义
             */
            @Override
            public void receiveConfigInfo(String configInfo) {

                log.info("start to update config: [{}]", configInfo);
                List<RouteDefinition> definitionList =
                        JSON.parseArray(configInfo,RouteDefinition.class);
                log.info("update route:[{}]",definitionList.toString());
                dynamicRouteService.updateList(definitionList);
            }
        });
    }catch (NacosException ex){
        log.error("dynamic update gateway config error: [{}]", ex.getMessage(),ex);
    }
}
复制代码

我们可以看到确定唯一的配置文件只需要dataId和group。在receiveConfigInfo就是***收到配置更新的方法,我们的配置文件是json格式的数组,获取到之后就可以调用我们之前写好的更新的方法。
我们同时要将这个类定义为Component,在完成这个bean的初始化之后,我们要执行一个init的方法,在方法上使用注解@PostConstruct来执行这个方法,就是在bean初始化之后就执行。

/**
 * Bean在容器中构造完成之后会执行 init 方法
 */
@PostConstruct
public void init(){

    log.info("gateway route init....");

    try{
        //初始化 Nacos 配置客户端
        configService = initConfigService();
        if (null == configService){
            log.error("init config service fail");
            return;
        }

        //通过 Nacos Config 并指定路由配置去获取路由配置
        String configInfo = configService.getConfig(
                GatewayConfig.NACOS_ROUTE_DATA_ID,
                GatewayConfig.NACOS_ROUTE_GROUP,
                GatewayConfig.DEFAULT_TIMEOUT
        );

        log.info("get current gateway config: [{}]", configInfo);
        List<RouteDefinition> definitionList =
                JSON.parseArray(configInfo, RouteDefinition.class);

        if (CollectionUtil.isNotEmpty(definitionList)){
            for (RouteDefinition definition : definitionList){
                log.info("init gateway config: [{}]",definition.toString());
                dynamicRouteService.addRouteDefinition(definition);
            }
        }

    }catch (Exception ex){
        log.error("gateway route init has some error:[{}]", ex.getMessage(), ex);
    }

    // 设置***
    dynamicRouteByNacosListener(GatewayConfig.NACOS_ROUTE_DATA_ID,
            GatewayConfig.NACOS_ROUTE_GROUP);
}
复制代码

我们启动gateway服务的时候,查看启动的日志,可以看到如下:

这就意味着能够成功读取到配置文件。
尝试在nacos上修改配置文件,看下能否监听到配置文件的修改。

发现是可以监听到的。 以上就是动态监听路由配置的实现。


原文链接:https://juejin.cn/post/7053336525467025439