Spring Cloud Gateway 动态路由 – 进阶

方案简介

(1)创建一个路由信息维护的项目(dynamic-route),实现增删改查路由信息到mysql
(2)提供发布功能,发布后将路由信息与版本信息保存到redis中,对外提供 rest 接口获取路由信息
(3)网关(gateway-dynamic-route)开启定时任务,定时拉取 rest 接口中发布的最新版本的路由信息,对比版本号,如果网关的版本号与rest接口中的不一致,则获取路由信息后更新网关路由,这样网关发布多个实例后,都会单独的去拉取维护路由信息
(4)整体架构设计如下:

Spring Cloud Gateway动态路由架构设计

根据上面的思路进行代码实现,下面将创建2个项目,都要注册到eureka上

  • dynamic-route,路由管理,维护路由信息,并存储到mysql与redis中
  • gateway-dynamic-route,网关,通过rest接口定时拉取最新路由信息并更新到网关中
  • consumer-service,消费者服务,测试动态路由配置后将请求转发到此服务

参考:https://gitee.com/zhuyu1991/spring-cloud/tree/master/gateway
参考:https://gitee.com/zhuyu1991/spring-cloud/tree/master/dynamic-route

路由管理

创建一个路由信息维护的项目dynamic-route,实现对路由信息的增删改查和发布功能,信息保存到mysql中,发布后保存到redis,对外提供路由数据时返回redis中的路由信息(使用缓存来应对网关定时任务读请求)

有2张表,路由信息表与版本发布表,表结构如下:

-- ----------------------------
-- Table structure for gateway_routes
-- ----------------------------
DROP TABLE IF EXISTS `gateway_routes`;
CREATE TABLE `gateway_routes` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `route_id` varchar(64) DEFAULT NULL COMMENT '路由id',
  `route_uri` varchar(128) DEFAULT NULL COMMENT '转发目标uri',
  `route_order` int(11) DEFAULT NULL COMMENT '路由执行顺序',
  `predicates` text COMMENT '断言JSON字符串集合',
  `filters` text COMMENT '过滤器JSON字符串集合',
  `is_ebl` tinyint(1) DEFAULT NULL COMMENT '是否启用',
  `is_del` tinyint(1) DEFAULT NULL COMMENT '是否删除',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '修改时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Table structure for dynamic_version
-- ----------------------------
DROP TABLE IF EXISTS `dynamic_version`;
CREATE TABLE `dynamic_version` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键、自动增长、版本号',
  `create_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4;

路由列表:http://localhost:9600/gateway-routes/list
添加路由信息:http://localhost:9600/gateway-routes/edit?id=2
获取版本信息:http://localhost:9600/version/lastVersion
获取路由信息:http://localhost:9600/gateway-routes/routes

网关先获取版本号,与本地版本号对比,如不一致则通过此接口拉取路由信息

Spring Cloud Gateway动态路由管理

网关项目

重点是网关项目,创建 gateway-dynamic-route 项目,网关启动时设置默认的版本号为0,通过定时任务每60秒拉取一次远程路由项目提供最新版本号,与网关的版本号对比,如不一致,则拉取远程路由项目最新的路由信息,更新到网关路由上去,同时把本地版本号覆盖为最新的版本号

(1)启动定时任务,注册到erueka,添加RestTemplate Bean且以负载均衡形式访问路由项目

@EnableScheduling
@EnableDiscoveryClient
@SpringBootApplication
public class GatewayDynamicRouteApplication {

    public static void main(String[] args) {
        SpringApplication.run(GatewayDynamicRouteApplication.class, args);
    }

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

(2)定时任务实现类

/**
 * 定时任务,拉取路由信息
 * 路由信息由路由项目单独维护
 */
@Component
public class DynamicRouteScheduling {

    @Autowired private RestTemplate restTemplate;
    @Autowired private DynamicRouteService dynamicRouteService;  //动态路由实现类,与前篇博客中的实现类代码是一样的

    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    private static final String dynamicRouteServerName = "dynamic-route-service";

    //发布路由信息的版本号
    private static Long versionId = 0L;

    //每60秒中执行一次
    //如果版本号不相等则获取最新路由信息并更新网关路由
    @Scheduled(cron = "*/60 * * * * ?")
    public void getDynamicRouteInfo() {
        try {
            System.out.println("拉取时间:" + dateFormat.format(new Date()));
            //先拉取版本信息,如果版本号不想等则更新路由
            Long resultVersionId = restTemplate.getForObject("http://"+ dynamicRouteServerName + "/version/lastVersion", Long.class);
            System.out.println("路由版本信息:本地版本号:" + versionId + ",远程版本号:" + resultVersionId);
            if(resultVersionId != null && versionId != resultVersionId) {
                System.out.println("开始拉取路由信息......");
                String resultRoutes = restTemplate.getForObject("http://"+ dynamicRouteServerName + "/gateway-routes/routes", String.class);
                System.out.println("路由信息为:" + resultRoutes);
                if (!StringUtils.isEmpty(resultRoutes)) {
                    List<GatewayRouteDefinition> list = JSON.parseArray(resultRoutes, GatewayRouteDefinition.class);
                    for (GatewayRouteDefinition definition : list) {
                        //更新路由
                        RouteDefinition routeDefinition = assembleRouteDefinition(definition);
                        dynamicRouteService.update(routeDefinition);
                    }
                    versionId = resultVersionId;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //把前端传递的参数转换成路由对象
    private RouteDefinition assembleRouteDefinition(GatewayRouteDefinition gwdefinition) {
        RouteDefinition definition = new RouteDefinition();
        definition.setId(gwdefinition.getId());
        definition.setOrder(gwdefinition.getOrder());

        //设置断言
        List<PredicateDefinition> pdList=new ArrayList<>();
        List<GatewayPredicateDefinition> gatewayPredicateDefinitionList=gwdefinition.getPredicates();
        for (GatewayPredicateDefinition gpDefinition: gatewayPredicateDefinitionList) {
            PredicateDefinition predicate = new PredicateDefinition();
            predicate.setArgs(gpDefinition.getArgs());
            predicate.setName(gpDefinition.getName());
            pdList.add(predicate);
        }
        definition.setPredicates(pdList);

        //设置过滤器
        List<FilterDefinition> filters = new ArrayList();
        List<GatewayFilterDefinition> gatewayFilters = gwdefinition.getFilters();
        for (GatewayFilterDefinition filterDefinition : gatewayFilters) {
            FilterDefinition filter = new FilterDefinition();
            filter.setName(filterDefinition.getName());
            filter.setArgs(filterDefinition.getArgs());
            filters.add(filter);
        }
        definition.setFilters(filters);

        URI uri = null;
        if (gwdefinition.getUri().startsWith("http")) {
            uri = UriComponentsBuilder.fromHttpUrl(gwdefinition.getUri()).build().toUri();
        } else {
            uri = URI.create(gwdefinition.getUri());
        }
        definition.setUri(uri);
        return definition;
    }
}

(3)网关提供获取所有路由信息的Controller

/**
 * 查询网关的路由信息
 */
@RestController
@RequestMapping("/route")
public class DynamicRouteController {

    @Autowired
    private RouteDefinitionLocator routeDefinitionLocator;

    //获取网关所有的路由信息
    @RequestMapping("/routes")
    public Flux<RouteDefinition> getRouteDefinitions() {
        return routeDefinitionLocator.getRouteDefinitions();
    }
}

(4)启动eurekadynamic-routegateway-dynamic-routeconsumer-service

(5)通过dynamic-route项目新增一条路由信息,路由的 uri 是consumer-service,且发布路由

发布路由时,会将版本表最新的版本号与路由信息添加到redis中,网关定时获取版本信息与路由信息都是先从redis中取出来的

(6)因网关没有配置路由信息,可通过 http://localhost:9999/route/routes 获取所有路由信息,此时有两条路由信息,是网关从eureka上拉下来的默认的路由信息,不是配置的路由信息

(7)等待60秒后,网关通过 RestTemplate 拉取dynamic-route项目的最新版本号,发现不一致,则拉取最新路由信息,并更新到网关路由中

(8)通过网关访问consumer-service,验证路由信息是否已更新到网关中,访问:http://localhost:9999/appblog/hello?name=joe

(9)再次查看所有路由信息:http://localhost:9999/route/routes ,可看到consumer-service已被设置到网关路由中了

(10)每隔60秒再次对比版本号,当有新的版本号发布后,就会拉取维护的路由信息,没有则网关不会更新路由

(11)修改一下路由信息,把断言Path=/appblog/**,改为Path=/consumer/**,并发布,隔了60秒后,网关的控制台拉取到最新的路由信息

(12)此时访问 http://localhost:9999/appblog/hello?name=joe 报错,因为路由断言path被更改了路由不到

(13)需要通过 http://localhost:9999/consumer/hello?name=joe 访问,正常返回

改进:可将定时任务改为MQ发送、主动请求推送、长连接请求等方案

版权声明:
作者:Joe.Ye
链接:https://www.appblog.cn/index.php/2023/04/01/spring-cloud-gateway-dynamic-routing-advanced/
来源:APP全栈技术分享
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
打赏
海报
Spring Cloud Gateway 动态路由 – 进阶
方案简介 (1)创建一个路由信息维护的项目(dynamic-route),实现增删改查路由信息到mysql (2)提供发布功能,发布后将路由信息与版本信息保存到redis中,……
<<上一篇
下一篇>>
文章目录
关闭
目 录