我的个人网站

  • SpringCloudGateWay的路由存储默认是InMemory,所以只需要改变数据的保存地址即可很方便的解决动态路由
  • 第一种:基于Redis保存
    – 首先SpringCloudGateWay的路由策略类默认为InMemoryRouteDefinitionRepository,实现了RouteDefinitionRepository接口进行的动态路由,那么我们也可以实现此接口
  • 创建RedisRouteDefinitionRepository类,实现RouteDefinitionRepository接口
  • 第二种基于配置中心(Apollo/nacos)
@Component
@Slf4j
public class RedisRouteDefinitionRepository implements RouteDefinitionRepository {

    /**
     * 路由保存Key值
     */
    public static final String ROUTE_KEY="ROUTE_KEY";

    @Autowired
    private RedisTemplate<String,Object> redisTemplate;

    /**
     * 获取路由配置信息
     *
     * @return
     */
    @Override
    public Flux<RouteDefinition> getRouteDefinitions() {
        log.info("[网关系统]动态路由刷新 start");
        List<RouteDefinition> routeDefinitions=new ArrayList<>();
        redisTemplate.opsForHash().values(ROUTE_KEY).stream().forEach(routeDefinition -> {
            routeDefinitions.add(JSON.parseObject(routeDefinition.toString(),RouteDefinition.class));
        });
        return Flux.fromIterable(routeDefinitions);
    }

    /**
     * 保存路由信息
     *
     * @param route
     * @return
     */
    @Override
    public Mono<Void> save(Mono<RouteDefinition> route) {
        return null;
    }

    /**
     * 删除路由信息
     *
     * @param routeId
     * @return
     */
    @Override
    public Mono<Void> delete(Mono<String> routeId) {
        return null;
    }
  • 然后将save和delete方法可以提取到其他的微服务模块中,因为这些操作可能涉及到DB,RouteDefinition类是路由类的参数类,定义了服务的id已经路由的策略,具体含义如下
id:服务的serverId,注册到注册中心的Id信息
uri:lb:使用注册中心方式配置;ws:使用websocket方式配置;http:使用http方式配置,如:ws://cloud-cus;
order:优先级数字类型,越小越优先匹配
predicates:请求路径配置,默认可使用/服务名/**
filters:过滤器配置
	限流配置说明
   		name:限流配置服务类默认使用EpayRateLimiter
        key-resolver:限流方式,支持使用apiKeyResolver[API限流]与hostAddrKeyResolver[用户IP地址限流]
        redis-rate-limiter.replenishRate:每秒允许通过的请求数
        redis-rate-limiter.burstCapacity:令牌桶的最大容量
  • 限流策略类如下
/**
     * 用户IP限流
     *
     * @return KeyResolver
     */
    @Bean("hostAddrKeyResolver")
    @Primary
    public KeyResolver hostAddrKeyResolver() {
        return exchange -> Mono.just(Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getHostName());
    }

    /**
     * API路径限流
     * @return apiKeyResolver
     */
    @Bean("apiKeyResolver")
    public KeyResolver apiKeyResolver() {
        return exchange -> Mono.just(exchange.getRequest().getPath().value());
    }
  • 参考配置如下所示
{
        "id":"test-cus",
        "uri":"lb://test-cus",
        "order":0,
        "predicates":[
           {
               "args":{
                   "pattern":"/test-cus/**"
               },
               "name":"Path"
           }
        ],
        "filters":[
           {
               "name":"RedisRateLimiter",
               "args":{
                   "key-resolver":"#{@hostAddrKeyResolver}",
                   "redis-rate-limiter.replenishRate":"1000",
                   "redis-rate-limiter.burstCapacity":"1000"
               }
           }
        ]
    }
  • 实例化Bean提取为对象
    – GatewayFilterDefinition保存过滤器等信息
    – GatewayPredicateDefinition保存断言信息
    – GatewayRouteDefinition 路由模型信息
@Data
public class GatewayFilterDefinition {

    /**
     * 过滤器名称
     */
    private String name;

    /**
     * 对应的路由规则
     */
    private Map<String, String> args = new LinkedHashMap<>();

}
@Data
public class GatewayPredicateDefinition {

    /**
     * 断言对应的Name
     */
    private String name;
    /**
     * 配置的断言规则
     */
    private Map<String, String> args = new LinkedHashMap<>();

}
@Data
public class GatewayRouteDefinition {

    /**
     * 路由Id唯一
     */
    private String id;
    /**
     * 路由断言集合配置
     */
    private List<GatewayPredicateDefinition> predicates=new ArrayList<>();
    /**
     * 路由过滤器集合配置
     */
    private List<GatewayFilterDefinition> filters=new ArrayList<>();
    /**
     * 元数据
     */
    private Map<String,Object> metadata=new HashMap<>();
    /**
     * 路由规则转发的目标uri
     */
    private String uri;
    /**
     * 路由执行的顺序
     */
    private int order=0;
}
  • 然后是路由对外提供API,可以集成于管理界面操作
@RestController
public class WebRouterManger {

    public static final String ROUTE_KEY="ROUTE_KEY";
    private final RedisTemplate<String,Object> redisTemplate;

    private final GatewayRoutesMapper routeMapper;

    public WebRouterManger(GatewayRoutesMapper routeMapper,RedisTemplate<String,Object> redisTemplate) {
        this.routeMapper=routeMapper;
        this.redisTemplate=redisTemplate;
    }

    @GetMapping("/router")
    public Flux<GatewayRouteDefinition> list() {
        List<GatewayRouteDefinition> routeDefinitions=new ArrayList<>();
        redisTemplate.opsForHash().values(ROUTE_KEY).forEach(routeDefinition -> routeDefinitions.add(JSON.parseObject(routeDefinition.toString(),GatewayRouteDefinition.class)));
        return Flux.fromIterable(routeDefinitions);
    }


    /**
     * 路由保存
     *
     * @param routeDefinition 路由信息
     * @return CommonResult
     */
    @PostMapping("/router")
    @Transactional(propagation=Propagation.REQUIRES_NEW, rollbackFor=Exception.class)
    public Mono<CommonResult> save(@RequestBody GatewayRouteDefinition routeDefinition) {
        //判断是否存在
        List<GatewayRoutes> routesList=routeMapper.selectByRouteName(routeDefinition.getId());
        if (!CollectionUtils.isEmpty(routesList)) {
            for (GatewayRoutes gatewayRoutes : routesList) {
                int i=routeMapper.deleteByPrimaryKey(gatewayRoutes.getId());
                if (i < 0) {
                    throw new RouterException(HttpStatus.INTERNAL_SERVER_ERROR.value(),"路由保存失败");
                }
            }
        }
        GatewayRoutes gatewayRoutes=new GatewayRoutes();
        gatewayRoutes.setCreateTime(new Date());
        gatewayRoutes.setFilters(JSONArray.toJSONString(routeDefinition.getFilters()));
        gatewayRoutes.setRouteId(routeDefinition.getId());
        gatewayRoutes.setRouteUri(routeDefinition.getUri());
        gatewayRoutes.setRouteOrder(routeDefinition.getOrder());
        gatewayRoutes.setPredicates(JSONArray.toJSONString(routeDefinition.getPredicates()));
        gatewayRoutes.setRouteStatus("00");
        gatewayRoutes.setMetadata(JSON.toJSONString(routeDefinition.getMetadata()));
        if (1 == routeMapper.insertSelective(gatewayRoutes)) {
            redisTemplate.opsForHash().put(ROUTE_KEY,routeDefinition.getId(),JSON.toJSONString(routeDefinition));
        }
        return Mono.just(CommonResult.success(""));
    }

    /**
     * 路由删除
     *
     * @param routeId 微服务Id
     * @param id      列表查询到的主键id
     * @return CommonResult
     */
    @DeleteMapping("/router")
    @Transactional(propagation=Propagation.REQUIRES_NEW, rollbackFor=Exception.class)
    public Mono<CommonResult> delete(@RequestParam("routeId") String routeId,@RequestParam("id") Long id) {

        if (routeMapper.deleteByPrimaryKey(id) == 1) {
            if (redisTemplate.opsForHash().hasKey(ROUTE_KEY,routeId)) {
                redisTemplate.opsForHash().delete(ROUTE_KEY,routeId);
            }
        }
        return Mono.just(CommonResult.success(""));
    }
  • 路由数据保存到数据库,做二级缓存兜底,数据库SQL
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 DEFAULT NULL COMMENT '断言字符串集合,字符串结构:[{\r\n                "name":"Path",\r\n                "args":{\r\n                   "pattern" : "/zy/**"\r\n                }\r\n              }]',
  `metadata` text DEFAULT NULL COMMENT '元数据 JSON格式',
  `filters` text DEFAULT NULL COMMENT '过滤器字符串集合,字符串结构:{\r\n              	"name":"StripPrefix",\r\n              	 "args":{\r\n              	 	"_genkey_0":"1"\r\n              	 }\r\n              }',
  `route_status` char(2) DEFAULT '' COMMENT '00 正常 99 失效',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '修改时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4;
  • 路由刷新时间实际上取的是eureka的探活时间,eureka.client.registry-fetch-interval-seconds,设定60秒后会执行Redis的路由刷新
  • 基于Apollo与nacos配置中心这种形式的会更加的便捷,可以考虑使用配置中心做动态化路由,实现方式也比较简单
  • 只需要将json配置保存后转换为RouteDefinition 然后调用RefreshRoutesEvent重加载路由信息即可,推荐使用这种方式,后面有时间会写一个比较详细点的基于Apollo或nacos的动态路由