Gateway如何使用多个源来达成动态路由

2024-08-07 14:41:33 浏览数 (2)

Gateway如何使用多个源来达成动态路由

一、介绍

在前面的文章,我介绍了如何从Nacos读取json文件来动态生成路由

随着文件的变更,同时刷新路由

但在文章的结尾,我并不满足于仅仅只在Nacos配置动态路由,我想要在多个源上配置信息,任何一处地方修改了配置,Gateway照样能够刷新路由。

那么如何使用多个源来达成动态路由?本篇文章使用了Nacosjson文件,和MySQL数据表,两个配置源来达成动态路由

二、代码

首先,分析了上篇文章的RouteDefinitionRepository.java接口,之前的Nacos配置源也是实现了这个接口

主要是里面的这个方法,获取到所有的RouteDefinition对象,每一个对象就是一个路由

1)BaseDynamicRouter

那么这样就好办了,我们先定义一个接口,BaseDynamicRouter.java,里面有个方法获取到RouteDefinition对象列表

代码语言:java复制
 package com.banmoon.route;
 ​
 import org.springframework.cloud.gateway.route.RouteDefinition;
 ​
 import java.util.List;
 ​
 /**
  * @author banmoon
  * @date 2024/08/05 19:17:43
  */
 @FunctionalInterface
 public interface BaseDynamicRouter {
 ​
     /**
      * 获取所有的路由信息
      */
     List<RouteDefinition> getAllRouterDefinitions();
 ​
 }

实现这个接口BaseDynamicRouter.java,代表着不同的配置源,我们这边有NacosMySQL,代码如下

代码语言:java复制
 package com.banmoon.route.impl;
 ​
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.extra.spring.SpringUtil;
 import com.alibaba.cloud.nacos.NacosConfigManager;
 import com.alibaba.cloud.nacos.NacosConfigProperties;
 import com.alibaba.nacos.api.config.listener.AbstractSharedListener;
 import com.alibaba.nacos.api.exception.NacosException;
 import com.banmoon.route.BaseDynamicRouter;
 import com.banmoon.route.DynamicRouterRepository;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.DeserializationFeature;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
 import org.springframework.cloud.gateway.route.RouteDefinition;
 import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.stereotype.Component;
 ​
 import javax.annotation.PostConstruct;
 import java.util.Collections;
 import java.util.List;
 ​
 /**
  * @author banmoon
  * @date 2024/08/05 19:22:05
  */
 @Slf4j
 @Component
 @RequiredArgsConstructor
 public class NacosDynamicRouter implements BaseDynamicRouter {
 ​
     private final ApplicationEventPublisher publisher;
     private final NacosConfigProperties nacosConfigProperties;
     private final NacosConfigManager nacosConfigManager;
 ​
     /**
      * Nacos DATA_ID
      */
     private static final String DATA_ID = "gateway-router.json";
 ​
     /**
      * json序列化
      */
     private static final ObjectMapper objectMapper = new ObjectMapper();
 ​
     @PostConstruct
     public void init() throws NacosException {
         nacosConfigManager.getConfigService().addListener(DATA_ID, nacosConfigProperties.getGroup(), new AbstractSharedListener() {
             @Override
             public void innerReceive(String dataId, String group, String configInfo) {
                 DynamicRouterRepository dynamicRouterRepository = SpringUtil.getBean(DynamicRouterRepository.class);
                 publisher.publishEvent(new RefreshRoutesEvent(dynamicRouterRepository));
             }
         });
     }
 ​
     @Override
     public List<RouteDefinition> getAllRouterDefinitions() {
         try {
             String routeConfig = nacosConfigManager.getConfigService().getConfig(DATA_ID, nacosConfigProperties.getGroup(), 3000);
             if (StrUtil.isNotBlank(routeConfig)) {
                 objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
                 return objectMapper.readValue(routeConfig, new TypeReference<List<RouteDefinition>>() {
                 });
             }
         } catch (Exception e) {
             log.error("nacos获取路由配置异常", e);
         }
         return Collections.emptyList();
     }
 ​
 }
代码语言:java复制
 package com.banmoon.route.impl;
 ​
 import cn.hutool.json.JSONUtil;
 import com.banmoon.route.BaseDynamicRouter;
 import com.banmoon.service.SysGatewayRouteService;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.cloud.gateway.filter.FilterDefinition;
 import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
 import org.springframework.cloud.gateway.route.RouteDefinition;
 import org.springframework.stereotype.Component;
 ​
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.List;
 import java.util.Objects;
 import java.util.stream.Collectors;
 ​
 /**
  * @author banmoon
  * @date 2024/08/05 19:21:13
  */
 @Slf4j
 @Component
 @RequiredArgsConstructor
 public class DatabaseDynamicRouter implements BaseDynamicRouter {
 ​
     private final SysGatewayRouteService sysGatewayRouteService;
 ​
     @Override
     public List<RouteDefinition> getAllRouterDefinitions() {
         return sysGatewayRouteService
                        .list()
                        .stream()
                        .map(entity -> {
                            try {
                                List<PredicateDefinition> predicates = JSONUtil.toList(entity.getPredicates(), PredicateDefinition.class);
                                List<FilterDefinition> filters = JSONUtil.toList(entity.getFilters(), FilterDefinition.class);
                                RouteDefinition routeDefinition = new RouteDefinition();
                                routeDefinition.setId(entity.getRouteId());
                                routeDefinition.setPredicates(predicates);
                                routeDefinition.setFilters(filters);
                                routeDefinition.setUri(new URI(entity.getUri()));
                                return routeDefinition;
                            } catch (URISyntaxException e) {
                                log.error("路由:{},uri:{},解析失败", entity.getRouteId(), entity.getUri());
                                return null;
                            }
                        }).filter(Objects::nonNull)
                        .collect(Collectors.toList());
     }
 }

2)配置源

Nacos的配置就不再贴出来了,和前面一篇文章是一样的,主要讲讲MySQL这个配置源

首先,添加一张表

代码语言:sql复制
 CREATE TABLE `sys_gateway_route` (
   `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
   `route_id` varchar(32) NOT NULL COMMENT '路由ID',
   `uri` varchar(128) DEFAULT NULL COMMENT 'uri',
   `predicates` varchar(1024) DEFAULT NULL COMMENT '断言',
   `filters` varchar(1024) DEFAULT NULL COMMENT '过滤',
   PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统-网关路由'

对应的实体、Mapper

代码语言:java复制
 package com.banmoon.entity;
 ​
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
 import lombok.experimental.Accessors;
 ​
 /**
  * @author banmoon
  * @date 2024/08/05 19:43:06
  */
 @Data
 @NoArgsConstructor
 @EqualsAndHashCode(callSuper = false)
 @Accessors(chain = true)
 @TableName("sys_gateway_route")
 public class SysGatewayRouteEntity {
 ​
     /**
      * 主键ID
      */
     @TableId(type = IdType.AUTO)
     private Integer id;
 ​
     /**
      * 路由ID
      */
     private String routeId;
 ​
     /**
      * uri
      */
     private String uri;
 ​
     /**
      * 断言
      */
     private String predicates;
 ​
     /**
      * 过滤
      */
     private String filters;
 ​
 }
代码语言:java复制
 package com.banmoon.mapper;
 ​
 import com.banmoon.business.mybatis.BanmoonMapper;
 import com.banmoon.entity.SysGatewayRouteEntity;
 import org.apache.ibatis.annotations.Mapper;
 ​
 /**
  * @author banmoon
  * @date 2024/08/05 19:46:06
  */
 @Mapper
 public interface SysGatewayRouteMapper extends BanmoonMapper<SysGatewayRouteEntity> {
 }

对应的service服务层

代码语言:java复制
 package com.banmoon.service;
 ​
 import com.banmoon.business.obj.dto.ResultData;
 import com.banmoon.entity.SysGatewayRouteEntity;
 import com.banmoon.obj.request.GatewayRouteAddRequest;
 import com.banmoon.obj.request.GatewayRouteUpdateRequest;
 import com.baomidou.mybatisplus.extension.service.IService;
 import reactor.core.publisher.Mono;
 ​
 import java.util.List;
 ​
 /**
  * @author banmoon
  * @date 2024/08/05 19:47:17
  */
 public interface SysGatewayRouteService extends IService<SysGatewayRouteEntity> {
 ​
     Mono<ResultData<Object>> add(GatewayRouteAddRequest request);
 ​
     Mono<ResultData<Object>> update(GatewayRouteUpdateRequest request);
 ​
     Mono<ResultData<Object>> delete(List<Integer> idList);
 }
代码语言:java复制
 package com.banmoon.service.impl;
 ​
 import com.banmoon.business.obj.dto.ResultData;
 import com.banmoon.entity.SysGatewayRouteEntity;
 import com.banmoon.mapper.SysGatewayRouteMapper;
 import com.banmoon.obj.request.GatewayRouteAddRequest;
 import com.banmoon.obj.request.GatewayRouteUpdateRequest;
 import com.banmoon.service.SysGatewayRouteService;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.BeanUtils;
 import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.stereotype.Service;
 import reactor.core.publisher.Mono;
 ​
 import java.util.List;
 ​
 /**
  * @author banmoon
  * @date 2024/08/05 19:47:33
  */
 @Slf4j
 @Service
 @RequiredArgsConstructor
 public class SysGatewayRouteServiceImpl extends ServiceImpl<SysGatewayRouteMapper, SysGatewayRouteEntity> implements SysGatewayRouteService {
 ​
     private final ApplicationEventPublisher publisher;
 ​
     @Override
     public Mono<ResultData<Object>> add(GatewayRouteAddRequest request) {
         return Mono.fromCallable(() -> {
             SysGatewayRouteEntity entity = new SysGatewayRouteEntity();
             BeanUtils.copyProperties(request, entity);
             baseMapper.insertOnDuplicateKeyUpdate(entity);
             return ResultData.success();
         }).onErrorReturn(ResultData.fail());
     }
 ​
     @Override
     public Mono<ResultData<Object>> update(GatewayRouteUpdateRequest request) {
         return Mono.fromCallable(() -> {
             SysGatewayRouteEntity entity = new SysGatewayRouteEntity();
             BeanUtils.copyProperties(request, entity);
             baseMapper.updateById(entity);
             return ResultData.success();
         }).onErrorReturn(ResultData.fail());
     }
 ​
     @Override
     public Mono<ResultData<Object>> delete(List<Integer> idList) {
         return Mono.fromCallable(() -> {
             baseMapper.deleteBatchIds(idList);
             return ResultData.success();
         }).onErrorReturn(ResultData.fail());
     }
 }

对应的controller

代码语言:java复制
 package com.banmoon.controller;
 ​
 import com.banmoon.business.obj.dto.ResultData;
 import com.banmoon.obj.request.GatewayRouteAddRequest;
 import com.banmoon.obj.request.GatewayRouteUpdateRequest;
 import com.banmoon.service.SysGatewayRouteService;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 import reactor.core.publisher.Mono;
 ​
 import javax.validation.Valid;
 import javax.validation.constraints.NotEmpty;
 import javax.validation.constraints.NotNull;
 import java.util.List;
 ​
 /**
  * @author banmoon
  * @date 2024/08/01 10:02:24
  */
 @Slf4j
 @Api(tags = "系统-网关路由")
 @Validated
 @RestController
 @RequestMapping("/sys/gateway/route")
 @RequiredArgsConstructor
 public class SysGatewayRouteController {
 ​
     private final SysGatewayRouteService sysGatewayRouteService;
 ​
     @ApiOperation("新增")
     @PostMapping("add")
     public Mono<ResultData<Object>> add(@RequestBody @Valid GatewayRouteAddRequest request) {
         return sysGatewayRouteService.add(request);
     }
 ​
     @ApiOperation("修改")
     @PutMapping("update")
     public Mono<ResultData<Object>> update(@RequestBody @Valid GatewayRouteUpdateRequest request) {
         return sysGatewayRouteService.update(request);
     }
 ​
     @ApiOperation("删除")
     @DeleteMapping("delete")
     public Mono<ResultData<Object>> delete(@RequestBody @NotEmpty List<@NotNull Integer> idList) {
         return sysGatewayRouteService.delete(idList);
     }
 ​
 }

请求实体

代码语言:java复制
 package com.banmoon.obj.request;
 ​
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 ​
 import javax.validation.constraints.NotBlank;
 ​
 /**
  * @author banmoon
  * @date 2024/08/05 19:59:46
  */
 @Data
 @ApiModel("新增路由-入参")
 public class GatewayRouteAddRequest {
 ​
     @NotBlank
     @ApiModelProperty("路由ID")
     private String routeId;
 ​
     @NotBlank
     @ApiModelProperty("uri")
     private String uri;
 ​
     @ApiModelProperty("断言")
     private String predicates;
 ​
     @ApiModelProperty("过滤")
     private String filters;
 ​
 }
代码语言:java复制
 package com.banmoon.obj.request;
 ​
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 ​
 import javax.validation.constraints.NotBlank;
 ​
 /**
  * @author banmoon
  * @date 2024/08/05 19:59:46
  */
 @Data
 @ApiModel("修改路由-入参")
 public class GatewayRouteUpdateRequest {
 ​
     @ApiModelProperty("主键ID")
     private Integer id;
 ​
     @NotBlank
     @ApiModelProperty("路由ID")
     private String routeId;
 ​
     @NotBlank
     @ApiModelProperty("uri")
     private String uri;
 ​
     @ApiModelProperty("断言")
     private String predicates;
 ​
     @ApiModelProperty("过滤")
     private String filters;
 ​
 }

3)RouteDefinitionRepository

我们回来看看这个RouteDefinitionRepository.java接口,只需要实现它,注入上面提到的BaseDynamicRouter.java,实现getRouteDefinitions()方法

代码语言:java复制
 package com.banmoon.route;
 ​
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.cloud.gateway.route.RouteDefinition;
 import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
 import org.springframework.stereotype.Component;
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 ​
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.Map;
 ​
 /**
  * 路由动态更新实现
  *
  * @author banmoon
  * @date 2024/08/01 10:12:22
  */
 @Slf4j
 @Component
 @RequiredArgsConstructor
 public class DynamicRouterRepository implements RouteDefinitionRepository {
 ​
     private final Map<String, BaseDynamicRouter> dynamicRouterMap;
 ​
     /**
      * 存储路由信息
      */
     private static final Map<String, RouteDefinition> routes = Collections.synchronizedMap(new LinkedHashMap<>());
 ​
     @Override
     public Flux<RouteDefinition> getRouteDefinitions() {
         return Flux.fromStream(dynamicRouterMap
                                        .entrySet()
                                        .stream()
                                        .flatMap(entry -> entry.getValue().getAllRouterDefinitions().stream()))
                        .doOnNext(routeDefinition -> routes.put(routeDefinition.getId(), routeDefinition));
     }
 ​
     @Override
     public Mono<Void> save(Mono<RouteDefinition> route) {
         return route.flatMap(routeDefinition -> {
             log.info("新增路由信息:{}", routeDefinition);
             routes.put(routeDefinition.getId(), routeDefinition);
             return Mono.empty();
         });
     }
 ​
     @Override
     public Mono<Void> delete(Mono<String> routeId) {
         return routeId.flatMap(id -> {
             log.info("删除路由ID:{}", id);
             routes.remove(id);
             return Mono.empty();
         });
     }
 }

这样就能实现多个源来配置Gateway的动态路由了

三、添加配置测试

在数据库插入一条路由信息,当然调用上面的接口也是可以的

代码语言:sql复制
 INSERT INTO test.sys_gateway_route (id, route_id, uri, predicates, filters) VALUES (1, 'route_baidu', 'https://www.baidu.com', '[{"name":"Query","args":{"_genkey_0":"url","_genkey_1":"baidu"}}]', '[]');

打开浏览器,输入http://localhost:8080/?url=baidu,可以看到百度的页面了

四、最后

其实,这个还是有点不太对,因为我发现每隔一段时间就发起一次请求更新路由

问题倒不是很大,但这么查询数据库总是一种消耗,后续看看还有没有更好的方法

0 人点赞