如何实现Spring Gateway路由的动态加载和刷新?

作者:Sample HubSpot User | Mar 28, 2024 6:29:03 AM

作者:陆永剑  职位:后端工程师

一、前言

在微服务化过程中,客户端可能需要调用多个服务的接口才能完成一个业务需求。网关作为后端微服务的统一入口,利用路由转发可以方便客户端快速接入各个微服务。

Spring Gateway (以下简称gateway)路由配置默认情况下是写在配置文件中的,有一个新的服务接入时,需要修改配置文件,然后重启网关才能生效。然而目前更多的需求是通过提供可视化页面,在前端页面增删改来使网关路由动态生效。

结合实际业务需求,接下来从原理和实践两个方面介绍如何上述需求。

二、原理

我们分两部分来说,先说动态加载,再说动态刷新。

2.1 动态加载

要想实现动态加载,我们要先弄清楚gateway是如何加载现有的配置信息的。

目前配置路由主要有两种方式,一种是用yml配置文件,一种是写代码里。

  1. # yml配置文件形式
  2. spring:
  3. cloud:
  4. gateway:
  5. routes:
  6. - id: v1
  7. uri: http://xxx/v1
  8. predicates:
  9. - Path=/xxx
 
  1. // 代码形式
  2. @Bean
  3. public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
  4. return builder.routes()
  5. .route("v1", r -> r.path("/xxx")
  6. .uri("http://xxx/v1"))
  7. .build();
  8. }

无论是yml还是代码,这些配置最终都是被封装到RouteDefinition对象中的。所有路由信息在系统启动时就被加载装配好了,并存到了内存里。

参考代码如下:

  1. //GatewayAutoConfiguration
  2. @Bean
  3. @ConditionalOnMissingBean
  4. public PropertiesRouteDefinitionLocator
  5. propertiesRouteDefinitionLocator(
  6. GatewayProperties properties) {
  7. return new PropertiesRouteDefinitionLocator(properties);
  8. }
  9. @Bean
  10. @ConditionalOnMissingBean(RouteDefinitionRepository.class)
  11. public InMemoryRouteDefinitionRepository
  12. inMemoryRouteDefinitionRepository() {
  13. return new InMemoryRouteDefinitionRepository();
  14. }
  15. //PropertiesRouteDefinitionLocator
  16. public class PropertiesRouteDefinitionLocator implements
  17. RouteDefinitionLocator {
  18. private final GatewayProperties properties;
  19. ...
  20. @Override
  21. public Flux<RouteDefinition> getRouteDefinitions() {
  22. return Flux.fromIterable(this.properties.getRoutes());
  23. }
  24. }

重点类为 RouteDefinitionLocator,此接口有多个实现类,分别对应不同方式配置的路由方式。其中 RouteDefinitionRepository 是从存储器中(例如:mysql、redis 等)读取 RouteDefinition。可以结合 RouteDefinitionRepository 来实现动态路由加载。其他几种获取路由方式不是本文重点,暂不详说。

这样,我们就可以在网关启动时从一个动态数据源(数据库等,而非代码和配置文件)加载配置数据,并通过 RouteDefinitionRepository 的实现类 InMemoryRouteDefinitionRepository 加载为路由信息了。

2.2 动态刷新

通过2.1我们找到了gateway从数据库加载路由的方式,但是还停留在gateway启动时加载数据的方式。接下来就看下如何实现在用户操作数据的时候就能完成gateway路由的刷新。

其实gateway是提供了刷新的事件的,那就是RefreshRoutesEvent

参考代码如下:

  1. public class RefreshRoutesEvent extends ApplicationEvent {
  2. /**
  3. * Create a new ApplicationEvent.   
  4. * @param source the object on which the event initially occurred
  5. (never {@code null})  
  6. */  
  7. public RefreshRoutesEvent(Object source) {    
  8. super(source);  
  9. }
  10. } 

我们再来深挖下触发了RefreshRoutesEvent事件后,gateway是如何处理的。

  1. @Override
  2. public void onApplicationEvent(ApplicationEvent event) {
  3. ...
  4. else if (event instanceof RefreshRoutesEvent && routeLocator != null)
  5. {  
  6. // forces initialization  
  7. routeLocator.ifAvailable(locator ->
  8. locator.getRoutes().subscribe());  
  9. }
  10. }

所以,我们的思路是,在用户操作路由数据,在数据保存到数据库之后,代码向gateway发送刷新事件就可以了。

三、实践

 

通过原理的分析,了解了网关从存储器中加载和动态刷新的机制。我们来整理下思路。

  • 想加载路由数据到gateway内存中,需要调用InMemoryRouteDefinitionRepository 的save()方法。

  • 想触发gateway的动态刷新,需要调用RefreshRoutesEvent事件。

接下来从这两点出发,一起开启实践之旅。

3.1 动态加载

动态加载按照顺序可以分为三步,加载路由数据、组装路由、保存到gateway内存中。

3.1.1 加载数据库数

可以通过 feign 调用的方式读取数据,此步骤比较简单,不做详细的描述。

  1. List<GatewayRoute> routeList =
  2. gatewayServiceClient.getApiRouteList().getData();

3.1.2 组装路由、保存到gateway内存

刚刚也分析到了RouteDefinition 是路由数据的核心。它大体包括三个部分,基本信息、断言(PredicateDefinition)、过滤器(FilterDefinition)。

接下来根据查询到的路由数据,进行gateway路由数据封装和保存。

  1. //路由封装
  2. routeList.forEach(gatewayRoute -> {
  3.  
  4. //RouteDefinition结构
  5. RouteDefinition definition = new RouteDefinition();
  6.  
  7. //设置基本信息
  8. definition.setId(gatewayRoute.getRouteName());     
  9. definition.setUri(UriComponentsBuilder.fromUriString("lb://" +gatewayRoute.getServiceId()).build().toUri());        
  10.  
  11. //设置断言信息    
  12. List<PredicateDefinition> predicates = Lists.newArrayList();    
  13. PredicateDefinition predicatePath = new PredicateDefinition();  
  14. Map<String, String> predicatePathParams = new HashMap<>(8);      
  15. predicatePath.setName("Path");    
  16. predicatePathParams.put("name", gatewayRoute.getRouteName());    
  17. predicatePathParams.put("pattern", gatewayRoute.getPath());    
  18. predicatePathParams.put("pathPattern", gatewayRoute.getPath());  
  19. predicatePath.setArgs(predicatePathParams);    
  20. predicates.add(predicatePath);    
  21. definition.setPredicates(predicates);        
  22.  
  23. //设置过滤器信息    
  24. List<FilterDefinition> filters = filters(gatewayRoute);    
  25. if (!CollectionUtils.isEmpty(filters)) {      
  26. definition.setFilters(filters);   
  27. }        
  28.  
  29. //重点,保存到gateway内存,其实是放在了Map<String, RouteDefinition> routes里  this.repository.save(Mono.just(definition)).subscribe();
  30. });

3.2 动态刷新

动态刷新分为两步。第一步是提供公共方法,允许业务侧更新数据库之后调用刷新网关事件。第二步在gateway端接收到刷新网关事件后,调用RefreshRoutesEvent事件实现刷新。

3.2.1 公共gateway刷新方法

参考代码如下

  1. public class OpenRestTemplate extends RestTemplate {  
  2. /**   
  3. * 刷新网关
  4. */
  5. public void refreshGateway() {
  6. publisher.publishEvent(
  7. new
  8. RefreshRemoteApplicationEvent(this,busProperties.getId(),null)
  9. );
  10. }
  11. }

3.2.2  调用RefreshRoutesEvent事件实现刷新

gateway接收到业务侧路由刷新事件后,调用 RefreshRoutesEvent 即可完成刷新动作。

参考代码如下

  1. /**
  2. * 接收业务侧刷新事件
  3. *
  4. * @param event
  5. */
  6. @Override
  7. public void onApplicationEvent(RefreshRemoteApplicationEvent event) {
  8. refresh();
  9. }
  10. /**
  11. * 刷新路由
  12. *
  13. * @return
  14. */
  15. public Mono<Void> refresh() {
  16. //3.1加载路由的逻辑
  17. this.loadRoutes();
  18. //触发默认路由刷新事件,刷新缓存路由
  19. this.publisher.publishEvent(new RefreshRoutesEvent(this));
  20. return Mono.empty();
  21. }

四、结论

结合实际业务需求,从原理和实践两个方面介绍了实现网关路由的动态加载和刷新方式。至此动态加载和刷新的功能就实现了,通过此功能,无论开发、测试、生产环境,都可以通过可视化页面快速的实现路由的动态修改,在配置速度和准确率上都可以得到很大提升。