Spring Cloud Gateway从数据库读取路由配置

Spring

由于运维特殊性,我们没有使用配置中心,仅仅只是使用了Nacos作为注册中心。业务场景对我们提出了需求,动态更新网关路由信息而不重启应用。考虑之下,我们选择了从数据库读取网关路由配置,更新配置到gateway应用。

我们先后经历2个版本,
一是直接实现RouteDefinitionRepository接口;
二是更新路由配置信息到GatewayProperties bean,通过RefreshRoutesEvent刷新路由配置信息

实现RouteDefinitionRepository接口

源码分析

org.springframework.cloud.gateway.config.GatewayAutoConfiguration

	@Bean
	@ConditionalOnMissingBean(RouteDefinitionRepository.class)
	public InMemoryRouteDefinitionRepository inMemoryRouteDefinitionRepository() {
		return new InMemoryRouteDefinitionRepository();
	}

在gateway初始化配置类中,发现默认是使用内存策略实现网关路由源,方法上面还有个@ConditionalOnMissingBean的注解,当实现RouteDefinitionRepository接口,会优先使用它的实现类。所以根据这点,第一版的实现,就是通过实现RouteDefinitionRepository接口。根据默认的处理策略,系统会每隔30秒,调用一次RouteDefinitionRepository#getRouteDefinitions获取路由配置信息,更新到网关处理逻辑里。

实现

数据库设计,把配置文件的值,复制到数据库表对应的字段即可。

CREATE TABLE t_route_config (
  route_id varchar(50) NOT NULL,
  route_name varchar(200) DEFAULT NULL COMMENT '名称',
  uri varchar(200) DEFAULT NULL COMMENT '网关url',
  predicates text COMMENT '网关断言',
  filters text COMMENT '网关过滤器',
  metadata text COMMENT '元数据信息',
  route_order text COMMENT '规则顺序',
  sys_create_time timestamp NULL DEFAULT NULL COMMENT '创建时间',
  sys_update_time timestamp NULL DEFAULT NULL COMMENT '修改时间',
  sys_status int(11) DEFAULT NULL COMMENT '数据标识',
  sys_remark varchar(500) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (route_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

实现类

/**
 * @author huangliuyu
 * @date 2022-08-23
 * @description
 */
@Data
public class RouteConfig implements Serializable {
    private String routeId;
    private String routeName;
    private String uri;
    private String predicates;
    private String filters;
    private String metadata;
    private Integer routeOrder;
    //更新时间
    private LocalDateTime sysUpdateTime;
}

RouteDefinitionRepository实现类,实现getRouteDefinitions方法,获取网关路由配置,通过@Repository注册到Spring Bean就可以加载处理了。

@Repository
public class OldDatabaseRouteDefinitionRepository implements RouteDefinitionRepository {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public Flux<RouteDefinition> getRouteDefinitions() {
        List<RouteDefinition> routeDefinitions = this.getRouteConfigs();
        return Flux.fromIterable(routeDefinitions);
    }

    public List<RouteDefinition> getRouteConfigs() {
        String sql = "select * from t_route_config t where t.sys_status=0";
        List<RouteConfig> rules = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(RouteConfig.class));
        if (null == rules || rules.size() <= 0) {
            return Collections.EMPTY_LIST;
        }

        List<RouteDefinition> routeDefinitions = new ArrayList<>();
        for (RouteConfig rule : rules) {
            RouteDefinition routeDefinition = new RouteDefinition();
            routeDefinition.setId(rule.getRouteId());
            routeDefinition.setUri(URI.create(rule.getUri()));
            routeDefinition.setPredicates(this.getPredicates(rule.getPredicates()));
            routeDefinition.setFilters(this.getFilters(rule.getFilters()));
            routeDefinition.setMetadata(this.getMetadata(rule.getMetadata()));
            Integer ruleOrder = rule.getRouteOrder();
            if (null != ruleOrder) {
                routeDefinition.setOrder(ruleOrder);
            }
            routeDefinitions.add(routeDefinition);
        }
        return routeDefinitions;
    }


    private List<PredicateDefinition> getPredicates(String text) {
        if (StringUtils.isBlank(text)) {
            return Collections.EMPTY_LIST;
        }
        Yaml yaml = new Yaml();
        List<String> predicateList = yaml.loadAs(text, List.class);
        List<PredicateDefinition> predicateDefinitions = new ArrayList<>();
        for (String predicate : predicateList) {
            if (StringUtils.isBlank(predicate)) {
                continue;
            }
            PredicateDefinition definition = new PredicateDefinition(predicate);
            predicateDefinitions.add(definition);
        }
        return predicateDefinitions;
    }

    private List<FilterDefinition> getFilters(String text) {
        if (StringUtils.isBlank(text)) {
            return Collections.EMPTY_LIST;
        }
        Yaml yaml = new Yaml();
        List<String> filterList = yaml.loadAs(text, List.class);
        List<FilterDefinition> filterDefinitions = new ArrayList<>();
        for (String filter : filterList) {
            if (StringUtils.isBlank(filter)) {
                continue;
            }
            FilterDefinition definition = new FilterDefinition(filter);
            filterDefinitions.add(definition);
        }
        return filterDefinitions;
    }

    private Map<String, Object> getMetadata(String text) {
        if (StringUtils.isBlank(text)) {
            return Collections.EMPTY_MAP;
        }
        Yaml yaml = new Yaml();
        return yaml.loadAs(text, Map.class);
    }

通过RefreshRoutesEvent更新

在后期开发从数据库中读取cors跨域配置功能时,发现可以通过RefreshRoutesEvent刷新网关路由配置信息

源码分析

CachingRouteLocator实现ApplicationListener,监听RefreshRoutesEvent事件,当有RefreshRoutesEvent出现,处理以下逻辑更新网关路由。
org.springframework.cloud.gateway.route.CachingRouteLocator#onApplicationEvent

	private final RouteLocator delegate;
	
	private Flux<Route> fetch() {
		return this.delegate.getRoutes().sort(AnnotationAwareOrderComparator.INSTANCE);
	}

	@Override
	public void onApplicationEvent(RefreshRoutesEvent event) {
		try {
			fetch().collect(Collectors.toList()).subscribe(
					list -> Flux.fromIterable(list).materialize().collect(Collectors.toList()).subscribe(signals -> {
						applicationEventPublisher.publishEvent(new RefreshRoutesResultEvent(this));
						cache.put(CACHE_KEY, signals);
					}, this::handleRefreshError), this::handleRefreshError);
		}
		catch (Throwable e) {
			handleRefreshError(e);
		}
	}

由CachingRouteLocator#fetch方法,一直往下看

CompositeRouteLocator是RouteLocator的实现类
org.springframework.cloud.gateway.route.CompositeRouteLocator#getRoutes

@Override
	public Flux<Route> getRoutes() {
		return this.delegates.flatMapSequential(RouteLocator::getRoutes);
	}

org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#getRoutes

public Flux<Route> getRoutes() {
		Flux<Route> routes = this.routeDefinitionLocator.getRouteDefinitions().map(this::convertToRoute);
		//todo ...
		return routes.map(route -> {
			if (logger.isDebugEnabled()) {
				logger.debug("RouteDefinition matched: " + route.getId());
			}
			return route;
		});
	}

RouteDefinitionRouteLocator就是各类RouteDefinitionRepository实现的接口,获取网关路由配置,当然默认的InMemoryRouteDefinitionRepository也不例外。

所以这里,想到改造原来获取网关路由的策略。

实现

public class RouteCorsConfig {
    @Autowired
    private GatewayProperties gatewayProperties;
    @Autowired
    private ApplicationEventPublisher publisher;
    @Autowired
    private RouteConfigRepository routeConfigRepository;
    @Autowired
    private GatewayProfileRepository profileRepository;
    private Integer routeVersion = 0;

    /**
     * 刷新路由配置
     */
    private void refreshRouteConfig() {
    	//这里原来的处理逻辑,获取List<RouteDefinition>,不过不用实现RouteDefinitionRepository接口了。
        List<RouteDefinition> routeDefinitions = routeConfigRepository.getRouteConfigs();
        //更新gatewayProperties中的路由配置信息
        gatewayProperties.setRoutes(routeDefinitions);
        //推送RefreshRoutesEvent事件
        publisher.publishEvent(new RefreshRoutesEvent(this));
        log.info("完成刷新网关路由配置 总数 {}", routeDefinitions.size());
    }

	//这里是更新路由的策略,大家根据自己的情况来就好,定时刷新
	@Scheduled(cron = "0/30 * * * * ?")
    @PostConstruct
    public void refreshConfig() {
        //用了一张表存储配置版本号
        //读取环境属性
        Map<String, String> profileMap = profileRepository.getProfile();
        //路由
        Integer routeVer = MapUtils.getInteger(profileMap, "ROUTE_VERSION", 0);
        //当数据库中版本号,大于内存中版本号时更新路由配置
        if (routeVer > routeVersion) {
            log.info("开始刷新网关路由配置 version={}", routeVer);
            this.refreshRouteConfig();
            routeVersion = routeVer;
        }
        //cors跨域
        //todo ....
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值