spring-cloud动态路由“动态”的理解

  • 非动态
  • 可以通过硬编码来配置路由
  • 读取yml文件配置路由
  • 动态
  • 动态路由接口RouteDefinitionRepository
  • 实现RouteDefinitionRepository接口自定义路由配置规则
  • 采用数据库+redis配置路由信息
  • 自定义RedisRouteDefinitionWriter路由操作类
  • 动态路由引申出的事件监听器
  • 开启debugger调用链反追踪
  • RefreshRoutesEvent事件
  • 监听
  • 发布
  • HeartbeatEvent事件
  • 监听
  • 发布
  • 动态路由引申出的相关线程
  • 线程CacheRefreshThread
  • 线程TimedSupervisorTask


非动态

众所周知,gateway配置最后会被封装成RouteDefinition

可以通过硬编码来配置路由

读取yml文件配置路由

@Configuration
public class DynamicRouteAutoConfiguration {
	/**
	 * 配置文件设置为空
	 * redis 加载为准
	 *
	 * @return
	 */
	@Bean
	public PropertiesRouteDefinitionLocator propertiesRouteDefinitionLocator() {
		return new PropertiesRouteDefinitionLocator(new GatewayProperties());
	}
}

但是这两种方式并不能动态的增删改路由信息,必须要改配置文件还要重启服务。。。

动态

即,数据库+缓存

动态路由接口RouteDefinitionRepository

RouteDefinitionRepository为路由定义定位器,动态路由入口都是基于此接口,内部是只有一个getRouteDefinitions方法

Java 动态路由 spring动态路由_路由配置

实现RouteDefinitionRepository接口自定义路由配置规则

RouteDefinitionRepository 通过继承自 RouteDefinitionLocatorRouteDefinitionWriter,封装了对路由定义信息的获取、增加、删除操作,在网关内置API端点接口时会用到这些操作。

采用数据库+redis配置路由信息

自定义RedisRouteDefinitionWriter路由操作类

这里省略将数据库路由配置信息加载到redis的代码片段,直接看将redis中的路由信息加载到gateway服务中

@Slf4j
@Component
@AllArgsConstructor
public class RedisRouteDefinitionWriter implements RouteDefinitionRepository {
	private final RedisTemplate redisTemplate;

	@Override
	public Mono<Void> save(Mono<RouteDefinition> route) {
		return route.flatMap(r -> {
			RouteDefinitionVo vo = new RouteDefinitionVo();
			BeanUtils.copyProperties(r, vo);
			log.info("保存路由信息{}", vo);
			redisTemplate.setKeySerializer(new StringRedisSerializer());
			redisTemplate.opsForHash().put(CommonConstants.ROUTE_KEY, r.getId(), vo);
			return Mono.empty();
		});
	}

	@Override
	public Mono<Void> delete(Mono<String> routeId) {
		routeId.subscribe(id -> {
			log.info("删除路由信息{}", id);
			redisTemplate.setKeySerializer(new StringRedisSerializer());
			redisTemplate.opsForHash().delete(CommonConstants.ROUTE_KEY, id);
		});
		return Mono.empty();
	}

	/**
	 * 动态路由入口
	 *
	 * @return
	 */
	@Override
	public Flux<RouteDefinition> getRouteDefinitions() {
		redisTemplate.setKeySerializer(new StringRedisSerializer());
		redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(RouteDefinitionVo.class));
		List<RouteDefinitionVo> values = redisTemplate.opsForHash().values(CommonConstants.ROUTE_KEY);
		List<RouteDefinition> definitionList = new ArrayList<>();
		values.forEach(vo -> {
			RouteDefinition routeDefinition = new RouteDefinition();
			BeanUtils.copyProperties(vo, routeDefinition);
			definitionList.add(vo);
		});
		log.debug("redis 中路由定义条数: {}, {}", definitionList.size(), definitionList);
		return Flux.fromIterable(definitionList);
	}
}

getRouteDefinitions就是获取路由配置的方法,每隔一段时间系统会自动调用

Java 动态路由 spring动态路由_redis_02


所以,通过接口往数据库中操作路由配置信息时(同时更新到redis),gateway服务中的路由信息也会得到更新,这应该就是动态路由了。

动态路由引申出的事件监听器

当我看到每隔一段时间打印出路由信息,而且是稳定的30s,这不禁引起我的疑问。getRouteDefinitions方法是在哪里触发的

开启debugger调用链反追踪

Java 动态路由 spring动态路由_路由配置_03

RefreshRoutesEvent事件

忽略不重要的过程,看到一段源码WeightCalculatorWebFilter.java

监听

Java 动态路由 spring动态路由_redis_04


这里可以看出是通过监听RefreshRoutesEvent事件来触发获取路由信息的,继续往下跟,找到RouteRefreshListener.java

发布

Java 动态路由 spring动态路由_Java 动态路由_05


是在这里发布的RefreshRoutesEvent事件

@Override
	public void onApplicationEvent(ApplicationEvent event) {
		if (event instanceof ContextRefreshedEvent
				|| event instanceof RefreshScopeRefreshedEvent
				|| event instanceof InstanceRegisteredEvent) {
			reset();
		}
		else if (event instanceof ParentHeartbeatEvent) {
			ParentHeartbeatEvent e = (ParentHeartbeatEvent) event;
			resetIfNeeded(e.getValue());
		}
		else if (event instanceof HeartbeatEvent) {
			HeartbeatEvent e = (HeartbeatEvent) event;
			resetIfNeeded(e.getValue());
		}
	}

	private void resetIfNeeded(Object value) {
		if (this.monitor.update(value)) {
			reset();
		}
	}

	private void reset() {
		this.publisher.publishEvent(new RefreshRoutesEvent(this));
	}

可以看到,这里又有一个onApplicationEvent事件监听器,通过判断事件来执行不同方法。

HeartbeatEvent事件

这里关注HeartbeatEvent事件:心跳事件

监听

Java 动态路由 spring动态路由_路由配置_06


继续往下跟,找到CloudEurekaClient.java

发布

在这里发布的HeartbeatEvent事件

protected void onCacheRefreshed() {
        super.onCacheRefreshed();
        if (this.cacheRefreshedCount != null) {
            long newCount = this.cacheRefreshedCount.incrementAndGet();
            log.trace("onCacheRefreshed called with count: " + newCount);
            this.publisher.publishEvent(new HeartbeatEvent(this, newCount));
        }

    }

动态路由引申出的相关线程

线程CacheRefreshThread

顾名思义,应该是缓存刷新的时候执行onCacheRefreshed,继续往下跟,找到DiscoveryClient.class的内部类,它是一个线程

Java 动态路由 spring动态路由_Java 动态路由_07

线程TimedSupervisorTask

这个线程是什么时候执行的,继续往下跟,找到TimedSupervisorTask.class,它自己是一个时间周期线程,内部维护了一个线程池去执行CacheRefreshThread

Java 动态路由 spring动态路由_路由配置_08


那么TimedSupervisorTask在哪里被调用,找到DiscoveryClient.class

Java 动态路由 spring动态路由_数据库_09


这里的心跳时间配置就是

  • eureka.client.registry-fetch-interval-seconds:指示从eureka服务器获取注册表信息的频率(默认30s)

后面就是怎么加载配置信息了。。。

大概链路:
项目启动——>运行TimedSupervisorTask——>内部线程池执行CacheRefreshThread——>触发HeartbeatEvent事件——>RefreshRoutesEvent事件——>执行getRouteDefinitions