如何实现Spring Gateway路由的动态加载和刷新?
一、前言
在微服务化过程中,客户端可能需要调用多个服务的接口才能完成一个业务需求。网关作为后端微服务的统一入口,利用路由转发可以方便客户端快速接入各个微服务。
Spring Gateway (以下简称gateway)路由配置默认情况下是写在配置文件中的,有一个新的服务接入时,需要修改配置文件,然后重启网关才能生效。然而目前更多的需求是通过提供可视化页面,在前端页面增删改来使网关路由动态生效。
结合实际业务需求,接下来从原理和实践两个方面介绍如何上述需求。
二、原理
我们分两部分来说,先说动态加载,再说动态刷新。
2.1 动态加载
要想实现动态加载,我们要先弄清楚gateway是如何加载现有的配置信息的。
目前配置路由主要有两种方式,一种是用yml配置文件,一种是写代码里。
# yml配置文件形式spring:cloud:gateway:routes:- id: v1uri: http://xxx/v1predicates:- Path=/xxx
// 代码形式@Beanpublic RouteLocator customRouteLocator(RouteLocatorBuilder builder) {return builder.routes().route("v1", r -> r.path("/xxx").uri("http://xxx/v1")).build();}
无论是yml还是代码,这些配置最终都是被封装到RouteDefinition对象中的。所有路由信息在系统启动时就被加载装配好了,并存到了内存里。
参考代码如下:
//GatewayAutoConfiguration@Bean@ConditionalOnMissingBeanpublic PropertiesRouteDefinitionLocatorpropertiesRouteDefinitionLocator(GatewayProperties properties) {return new PropertiesRouteDefinitionLocator(properties);}@Bean@ConditionalOnMissingBean(RouteDefinitionRepository.class)public InMemoryRouteDefinitionRepositoryinMemoryRouteDefinitionRepository() {return new InMemoryRouteDefinitionRepository();}//PropertiesRouteDefinitionLocatorpublic class PropertiesRouteDefinitionLocator implementsRouteDefinitionLocator {private final GatewayProperties properties;...@Overridepublic Flux<RouteDefinition> getRouteDefinitions() {return Flux.fromIterable(this.properties.getRoutes());}}
重点类为 RouteDefinitionLocator,此接口有多个实现类,分别对应不同方式配置的路由方式。其中 RouteDefinitionRepository 是从存储器中(例如:mysql、redis 等)读取 RouteDefinition。可以结合 RouteDefinitionRepository 来实现动态路由加载。其他几种获取路由方式不是本文重点,暂不详说。
这样,我们就可以在网关启动时从一个动态数据源(数据库等,而非代码和配置文件)加载配置数据,并通过 RouteDefinitionRepository 的实现类 InMemoryRouteDefinitionRepository 加载为路由信息了。
2.2 动态刷新
通过2.1我们找到了gateway从数据库加载路由的方式,但是还停留在gateway启动时加载数据的方式。接下来就看下如何实现在用户操作数据的时候就能完成gateway路由的刷新。
其实gateway是提供了刷新的事件的,那就是RefreshRoutesEvent。
参考代码如下:
public class RefreshRoutesEvent extends ApplicationEvent {/*** Create a new ApplicationEvent.* @param source the object on which the event initially occurred(never {@code null})*/public RefreshRoutesEvent(Object source) {super(source);}}
我们再来深挖下触发了RefreshRoutesEvent事件后,gateway是如何处理的。
@Overridepublic void onApplicationEvent(ApplicationEvent event) {...else if (event instanceof RefreshRoutesEvent && routeLocator != null){// forces initializationrouteLocator.ifAvailable(locator ->locator.getRoutes().subscribe());}}
所以,我们的思路是,在用户操作路由数据,在数据保存到数据库之后,代码向gateway发送刷新事件就可以了。
三、实践

通过原理的分析,了解了网关从存储器中加载和动态刷新的机制。我们来整理下思路。
- 想加载路由数据到gateway内存中,需要调用InMemoryRouteDefinitionRepository 的save()方法。
- 想触发gateway的动态刷新,需要调用RefreshRoutesEvent事件。
接下来从这两点出发,一起开启实践之旅。
3.1 动态加载
动态加载按照顺序可以分为三步,加载路由数据、组装路由、保存到gateway内存中。
3.1.1 加载数据库数
可以通过 feign 调用的方式读取数据,此步骤比较简单,不做详细的描述。
List<GatewayRoute> routeList =gatewayServiceClient.getApiRouteList().getData();
3.1.2 组装路由、保存到gateway内存
刚刚也分析到了RouteDefinition 是路由数据的核心。它大体包括三个部分,基本信息、断言(PredicateDefinition)、过滤器(FilterDefinition)。
接下来根据查询到的路由数据,进行gateway路由数据封装和保存。
//路由封装routeList.forEach(gatewayRoute -> {//RouteDefinition结构RouteDefinition definition = new RouteDefinition();//设置基本信息definition.setId(gatewayRoute.getRouteName());definition.setUri(UriComponentsBuilder.fromUriString("lb://" +gatewayRoute.getServiceId()).build().toUri());//设置断言信息List<PredicateDefinition> predicates = Lists.newArrayList();PredicateDefinition predicatePath = new PredicateDefinition();Map<String, String> predicatePathParams = new HashMap<>(8);predicatePath.setName("Path");predicatePathParams.put("name", gatewayRoute.getRouteName());predicatePathParams.put("pattern", gatewayRoute.getPath());predicatePathParams.put("pathPattern", gatewayRoute.getPath());predicatePath.setArgs(predicatePathParams);predicates.add(predicatePath);definition.setPredicates(predicates);//设置过滤器信息List<FilterDefinition> filters = filters(gatewayRoute);if (!CollectionUtils.isEmpty(filters)) {definition.setFilters(filters);}//重点,保存到gateway内存,其实是放在了Map<String, RouteDefinition> routes里 this.repository.save(Mono.just(definition)).subscribe();});
3.2 动态刷新
动态刷新分为两步。第一步是提供公共方法,允许业务侧更新数据库之后调用刷新网关事件。第二步在gateway端接收到刷新网关事件后,调用RefreshRoutesEvent事件实现刷新。
3.2.1 公共gateway刷新方法
参考代码如下
public class OpenRestTemplate extends RestTemplate {/*** 刷新网关*/public void refreshGateway() {publisher.publishEvent(newRefreshRemoteApplicationEvent(this,busProperties.getId(),null));}}
3.2.2 调用RefreshRoutesEvent事件实现刷新
gateway接收到业务侧路由刷新事件后,调用 RefreshRoutesEvent 即可完成刷新动作。
参考代码如下
/*** 接收业务侧刷新事件** @param event*/@Overridepublic void onApplicationEvent(RefreshRemoteApplicationEvent event) {refresh();}/*** 刷新路由** @return*/public Mono<Void> refresh() {//3.1加载路由的逻辑this.loadRoutes();//触发默认路由刷新事件,刷新缓存路由this.publisher.publishEvent(new RefreshRoutesEvent(this));return Mono.empty();- }
四、结论
结合实际业务需求,从原理和实践两个方面介绍了实现网关路由的动态加载和刷新方式。至此动态加载和刷新的功能就实现了,通过此功能,无论开发、测试、生产环境,都可以通过可视化页面快速的实现路由的动态修改,在配置速度和准确率上都可以得到很大提升。
当身份认证成为开户基础设施,风控的价值重心将移向哪里
ADVANCE.AI 联合建模:让信用评估更贴近真实业务
AI Agent时代,企业将迎来第四种新身份