Gateway基于Nacos动态路由

需求

​ 由于每次增减服务路由,网关服务都需要重启。由于网关服务作为所有服务的http接口的入口,我们更想让它热更新,每次增减服务路由,修改网关的路由配置信息即可,无需重启网关服务。

​ 用户更加无感知,切换路由更新顺滑。

实现思路

​ 将服务路由配置信息放置单独的一个配置data中「之前我们是放在网关服务data」

​ 然后往Nacos注册监听器,监听服务路由配置信息,一旦服务路由配置信息变动,就基于Gateway的API进行路由的CRUD操作

相关环境

​ JDK :1.8

​ Spring Boot: 2.3.2.RELEASE

​ Spring Cloud:Hoxton.SR8

​ Spring Cloud Alibaba: 2.2.3.RELEASE

相关代码

pom
<?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>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.2.RELEASE</version>
    </parent>
    <groupId>com.xxx</groupId>
    <artifactId>xxx-gateway</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>xxx-gateway</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR8</spring-cloud.version>
        <spring.boot.version>2.3.2.RELEASE</spring.boot.version>
    </properties>

    <dependencies>

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

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

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

        <!-- dubbo hession 协议依赖 -->
        <dependency>
            <groupId>com.caucho</groupId>
            <artifactId>hessian</artifactId>
        </dependency>

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

        <!-- lombok  -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>


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

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

        <!-- REDIS  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!--鉴权-->
        <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>



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

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

        </dependencies>

    </dependencyManagement>

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

</project>

网关相关配置类
/**
 * 网关相关配置
 * @author: cyw
 * @CreateTime: 2021/8/18 11:59
 **/
@Data
@NoArgsConstructor
@Accessors(chain = true)
@Configuration
public class GatewayConfig implements Serializable {

    private static final long serialVersionUID = -745598752826462768L;

    /** 读取配置的超时时间 */
    public static final long DEFAULT_TIMEOUT = 30000;

    /** Nacos 服务器地址 */
    public static String NACOS_SERVER_ADDR;

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

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

    /** 分组 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:gateway-router}")
    public void setNacosRouteDataId(String nacosRouteDataId) {
        NACOS_ROUTE_DATA_ID = nacosRouteDataId;
    }

    @Value("${nacos.gateway.route.config.group:xxx-gateway}")
    public void setNacosRouteGroup(String nacosRouteGroup) {
        NACOS_ROUTE_GROUP = nacosRouteGroup;
    }

}
网关路由相关操作类
/**
 * 网关路由相关操作
 *
 * @author: cyw
 * @CreateTime: 2021/8/18 13:35
 **/
@Slf4j
@Component
public class DynamicRouteManger implements ApplicationEventPublisherAware {

    @Resource
    private RouteDefinitionWriter routeDefinitionWriter;
    @Resource
    private RouteDefinitionLocator routeDefinitionLocator;

    @Resource
    private ApplicationEventPublisher publisher;

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public String addRouteDefinition(RouteDefinition definition) {
        log.info("[网关服务]-[添加路由]: [{}]", definition);

        routeDefinitionWriter.save(Mono.just(definition)).subscribe();
        this.publisher.publishEvent(new RefreshRoutesEvent(this));
        return "success";
    }

    public List<RouteDefinition> list() {
        return routeDefinitionLocator.getRouteDefinitions().buffer().blockFirst();
    }

    public String updateList(List<RouteDefinition> definitions) {
        log.info("[网关服务]-[刷新路由]: [{}]", definitions);

        List<RouteDefinition> routeDefinitionsExits = list();

        if (isNotEmpty(routeDefinitionsExits)) {
            routeDefinitionsExits.forEach(this::deleteById);
        }
        Map<String, RouteDefinition> existMap = StreamUtils.propMap(routeDefinitionsExits, RouteDefinition::getId, Function.identity());

        for (RouteDefinition definition : definitions) {
            updateByRouteDefinition(definition, !existMap.containsKey(definition.getId()));
        }
        return "success";
    }

    private String deleteById(RouteDefinition definition) {
        try {
            if (definition.getId().startsWith("ReactiveCompositeDiscoveryClient")) {
                log.info("[网关服务]-[删除路由]: id包含ReactiveCompositeDiscoveryClient跳过:{}", definition.getId());
                return "success";
            }
            this.routeDefinitionWriter.delete(Mono.just(definition.getId())).subscribe();
            this.publisher.publishEvent(new RefreshRoutesEvent(this));
        } catch (Exception ex) {
            log.error("[网关服务]-[删除路由失败]: id:{}, ex:[{}]", definition.getId(), ex.getMessage(), ex);
        }
        log.info("[网关服务]-[删除路由成功]: [{}]", definition);
        return "success";
    }

    private String updateByRouteDefinition(RouteDefinition definition, Boolean skipDelete) {
        if (skipDelete != null && !skipDelete) {
            try {
                this.routeDefinitionWriter.delete(Mono.just(definition.getId()));
                log.info("[网关服务]-[删除路由成功]: [{}]", definition);
            } catch (Exception ex) {
                log.error("[网关服务]-[删除路由失败]: id:{}, ex:[{}]", definition.getId(), ex.getMessage(), ex);
            }
        }
        addRouteDefinition(definition);
        return "success";
    }

}
通过Nacos 动态配置刷新路由
/**
 * 通过 nacos 动态配置刷新路由
 *
 * @author: cyw
 * @CreateTime: 2021/8/18 14:35
 **/
@Slf4j
@Component
@DependsOn({"gatewayConfig"})
public class DynamicRouteComponent {

    private ConfigService configService;
    @Resource
    private DynamicRouteManger dynamicRouteService;

    @PostConstruct
    public void init() {

        log.info("[网关服务]-[初始化路由]");

        try {
            configService = initConfigService();
            if (null == configService) {
                log.info("[网关服务]-[初始化路由失败]-获取Nacos客户端为空");
                return;
            }

            String configInfo = configService.getConfig(
                    GatewayConfig.NACOS_ROUTE_DATA_ID,
                    GatewayConfig.NACOS_ROUTE_GROUP,
                    GatewayConfig.DEFAULT_TIMEOUT
            );

            List<RouteDefinition> definitionList = JsonUtils.toListIfPresent(configInfo, RouteDefinition.class);

            if (isNotEmpty(definitionList)) {
                for (RouteDefinition definition : definitionList) {
                    dynamicRouteService.addRouteDefinition(definition);
                }
            }

        } catch (Exception ex) {
            log.error("[网关服务]-[初始化路由失败]-相关报错信息: {}", ex.getMessage(), ex);
        }

        dynamicRouteByNacosListener(GatewayConfig.NACOS_ROUTE_DATA_ID,
                GatewayConfig.NACOS_ROUTE_GROUP);
    }

    /**
     * <h2>初始化 Nacos Config</h2>
     */
    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.info("[网关服务]-[初始化路由失败]-获取Nacos客户端失败");
            return null;
        }
    }

    /**
     * <h2>监听 Nacos 动态路由配置</h2>
     */
    private void dynamicRouteByNacosListener(String dataId, String group) {

        try {
            configService.addListener(dataId, group, new Listener() {

                @Override
                public Executor getExecutor() {
                    return null;
                }

                @Override
                public void receiveConfigInfo(String configInfo) {
                    List<RouteDefinition> definitionList = JsonUtils.toListIfPresent(configInfo, RouteDefinition.class);
                    dynamicRouteService.updateList(definitionList);
                }
            });
        } catch (NacosException ex) {
            log.error("[网关服务]-[动态刷新路由失败]-error: [{}]", ex.getMessage(), ex);
        }
    }
}

在Nacos上增加 路由配置文件

在这里插入图片描述

配置格式改成json
在这里插入图片描述

具体的 服务路由配置信息 ,根据大家的项目进行配置

[
  {
    "id":"xx",
    "uri":"lb://xxx",
    "predicates":[{
      "name":"Path",
      "args": {
        
      }
    }],
    "filters": [
      {
        
      }
    ]
  }
]
controller测试下
@Slf4j
@RestController
@NotOnProduction
@RequestMapping(value = "/config")
public class DynamicController {

    @Resource
    private GatewayConfig config;

    @Resource
    private DynamicRouteManger dynamicRouteManger;

    @PostMapping
    public Mono<String> config() {
        log.info(
                "config, address= {}, namespace= {}, dataId= {}, group= {}",
                GatewayConfig.NACOS_SERVER_ADDR, GatewayConfig.NACOS_NAMESPACE,
                GatewayConfig.NACOS_ROUTE_DATA_ID, GatewayConfig.NACOS_ROUTE_GROUP
        );
        return Mono.just("config");
    }

    @PostMapping("/list")
    public Mono<String> list() {
        List<RouteDefinition> list = dynamicRouteManger.list();
        log.info("list : {}", JsonUtils.toJsonIfPresent(list));
        return Mono.just("list");
    }

}

相关发布方案

​ 最好在开发环境测了没问题之后,再发布到测试环境,测试环境跑了一段时间之后,再上预发、生产环境…

​ 服务路由一多之后,单独抽出 路由配置文件 还有有点风险了的…尤其是没有制定相关的路由规范…当时我们这边30多个服务,由于我们没有制定相关的路由规范…所以我迁移的时候 挺费劲的…在测试环境跑了段时间,也发现了路由修改错误,好在热更新 直接修改就生效了

​ 要是线上网关不是集群的话…那其实也只能直接发了

​ 我们这边网关服务线上是两节点,所以以下是按两节点的方式发布「其实多节点也一样- - 」

 1. 增加 路由配置文件 启动一台网关初始化成功之后
 2. 再删除配置中心中路由相关的信息,再启动另一台网关服务
 3. 启动成功之后,再重启第一台网关服务「超过两个节点,后面网关服务直接重启就好了」
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值