Bootstrap

pigx动态路由分析(二)

pigx动态路由分析(二)

1、动态路由何时生效?

pigx动态路由分析1中,我介绍了pigx是何时定义的动态路由,当时只分析到路由配置是如何从数据库读取到redis,这个配置放到redis和网关还没有建立关系,如下最后一行就是从mysql把route定义转到redis。

//common-gateway模块   DynamicRouteInitRunner类
@Async
	@Order
	@EventListener({ WebServerInitializedEvent.class, DynamicRouteInitEvent.class })
	public void initRoute() {
		log.info("当前线程:"+Thread.currentThread().getName());
		Boolean result = redisTemplate.delete(CacheConstants.ROUTE_KEY);
		log.info("初始化网关路由 {} ", result);

		routeConfService.list().forEach(route -> {
			RouteDefinitionVo vo = new RouteDefinitionVo();
			vo.setRouteName(route.getRouteName());
			vo.setId(route.getRouteId());
			vo.setUri(URI.create(route.getUri()));
			vo.setOrder(route.getOrder());

			JSONArray filterObj = JSONUtil.parseArray(route.getFilters());
			vo.setFilters(filterObj.toList(FilterDefinition.class));
			JSONArray predicateObj = JSONUtil.parseArray(route.getPredicates());
			vo.setPredicates(predicateObj.toList(PredicateDefinition.class));

			log.info("加载路由ID:{},{}", route.getRouteId(), vo);
			redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(RouteDefinitionVo.class));
			redisTemplate.opsForHash().put(CacheConstants.ROUTE_KEY, route.getRouteId(), vo);
		});
		log.debug("初始化网关路由结束 ");
	}

2 分析过程

  1. 先从启动器着手
    先找到gateway模块,进入启动类(ccs是我司项目名,我们购买的是pigx商业版,我们把项目名包名都改成了ccs,和直接下载pigx不一样),可以看到有个@EnableCcsDynamicRoute,这个注解是自定义注解,跟踪进去
@EnableCcsDynamicRoute
@SpringCloudApplication
public class CcsGatewayApplication {

	public static void main(String[] args) {
		SpringApplication.run(CcsGatewayApplication.class, args);
	}

}

  1. EnableCcsDynamicRoute分析
    可以看到此注解啥都没干,就引入了@Import(DynamicRouteAutoConfiguration.class),接着我们再查看DynamicRouteAutoConfiguration
// common-gateway模块   
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(DynamicRouteAutoConfiguration.class)
public @interface EnableCcsDynamicRoute {

}

  1. DynamicRouteAutoConfiguration
    看代码得出一下结论:
  • 这是个配置类(@Configuration)
  • 扫描了com.sonoscape.ccs.common.gateway包
  • @ConditionalOnWebApplication此配置类只在响应式编程(和servlet相对)生效,这个不是注解不是重点,主要是前两个
//common-gateway模块
@Slf4j
@Configuration
@ComponentScan("com.sonoscape.ccs.common.gateway")
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
public class DynamicRouteAutoConfiguration {

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

	/**
	 * redis 监听配置
	 * @param redisConnectionFactory redis 配置
	 * @return
	 */
	@Bean
	public RedisMessageListenerContainer redisContainer(RedisConnectionFactory redisConnectionFactory) {
		RedisMessageListenerContainer container = new RedisMessageListenerContainer();
		container.setConnectionFactory(redisConnectionFactory);
		container.addMessageListener((message, bytes) -> {
			log.warn("接收到重新JVM 重新加载路由事件");
			RouteCacheHolder.removeRouteList();
		}, new ChannelTopic(CacheConstants.ROUTE_JVM_RELOAD_TOPIC));
		return container;
	}

	@Bean
	@ConditionalOnProperty(value = "spring.redis.cluster.enable", havingValue = "true")
	public LettuceConnectionFactory redisConnectionFactory(RedisProperties redisProperties) {
		RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration(
				redisProperties.getCluster().getNodes());

		// https://github.com/lettuce-io/lettuce-core/wiki/Redis-Cluster#user-content-refreshing-the-cluster-topology-view
		ClusterTopologyRefreshOptions clusterTopologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
				.enablePeriodicRefresh().enableAllAdaptiveRefreshTriggers().refreshPeriod(Duration.ofSeconds(5))
				.build();

		ClusterClientOptions clusterClientOptions = ClusterClientOptions.builder()
				.topologyRefreshOptions(clusterTopologyRefreshOptions).build();

		// https://github.com/lettuce-io/lettuce-core/wiki/ReadFrom-Settings
		LettuceClientConfiguration lettuceClientConfiguration = LettuceClientConfiguration.builder()
				.readFrom(ReadFrom.REPLICA_PREFERRED).clientOptions(clusterClientOptions).build();

		return new LettuceConnectionFactory(redisClusterConfiguration, lettuceClientConfiguration);
	}

}
  1. 分析到这里好像也没发现啥?? 我分析到这就卡克了
    别忘了,我们第一篇文章中有关键的一步,设置了路由到redis,具体代码是redisTemplate.opsForHash().put(CacheConstants.ROUTE_KEY, route.getRouteId(), vo);,CacheConstants.ROUTE_KEY,这个是redis里面的key,既然这里有设置,那么必然有地方需要获取路由,从而使用它,只需要查询CacheConstants.ROUTE_KEY,在哪里被使用了即可,因为获取值肯定还是根据key获取。还是老办法,在CacheConstants.ROUTE_KEY,上Alt+F7,查找哪里用到。果不出其然被我找到了,如下图,我们定位到了调用的位置在common-gateway模块里面的RedisRouteDefinitionWriter类,他用了@Component注解,说明是一个bean,下面接着分析
    在这里插入图片描述
  2. RedisRouteDefinitionWriter
    我们只看关键方法getRouteDefinitions,定位到这里已经可以很明显的看到了pigx作者的注释,这是动态路由入口!!!!
/**
	 * 动态路由入口
	 * <p>
	 * 1. 先从内存中获取 2. 为空加载Redis中数据 3. 更新内存
	 * @return
	 */
	@Override
	public Flux<RouteDefinition> getRouteDefinitions() {
		List<RouteDefinitionVo> routeList = RouteCacheHolder.getRouteList();
		if (CollUtil.isNotEmpty(routeList)) {
			log.debug("内存 中路由定义条数: {}, {}", routeList.size(), routeList);
			return Flux.fromIterable(routeList);
		}

		redisTemplate.setKeySerializer(new StringRedisSerializer());
		redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(RouteDefinitionVo.class));
		List<RouteDefinitionVo> values = redisTemplate.opsForHash().values(CacheConstants.ROUTE_KEY);
		log.debug("redis 中路由定义条数: {}, {}", values.size(), values);

		RouteCacheHolder.setRouteList(values);
		return Flux.fromIterable(values);
	}
  1. 我迫不及待的在getRouteDefinitions方法打上断点,重新debug启动网关模块,哈哈,启动时自动就进入了断点,为啥会自动进入呢???下面贴个完整代码,可以看到它实现了RouteDefinitionRepository,这个类名很熟悉的赶脚,对我等靠增删改查吃饭的码畜来首再熟悉不过了。
@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(CacheConstants.ROUTE_KEY, r.getId(), vo);
			redisTemplate.convertAndSend(CacheConstants.ROUTE_JVM_RELOAD_TOPIC, "新增路由信息,网关缓存更新");
			return Mono.empty();
		});
	}

	@Override
	public Mono<Void> delete(Mono<String> routeId) {
		routeId.subscribe(id -> {
			log.info("删除路由信息{}", id);
			redisTemplate.setKeySerializer(new StringRedisSerializer());
			redisTemplate.opsForHash().delete(CacheConstants.ROUTE_KEY, id);
		});
		redisTemplate.convertAndSend(CacheConstants.ROUTE_JVM_RELOAD_TOPIC, "删除路由信息,网关缓存更新");
		return Mono.empty();
	}

	/**
	 * 动态路由入口
	 * <p>
	 * 1. 先从内存中获取 2. 为空加载Redis中数据 3. 更新内存
	 * @return
	 */
	@Override
	public Flux<RouteDefinition> getRouteDefinitions() {
		List<RouteDefinitionVo> routeList = RouteCacheHolder.getRouteList();
		if (CollUtil.isNotEmpty(routeList)) {
			log.debug("内存 中路由定义条数: {}, {}", routeList.size(), routeList);
			return Flux.fromIterable(routeList);
		}

		redisTemplate.setKeySerializer(new StringRedisSerializer());
		redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(RouteDefinitionVo.class));
		List<RouteDefinitionVo> values = redisTemplate.opsForHash().values(CacheConstants.ROUTE_KEY);
		log.debug("redis 中路由定义条数: {}, {}", values.size(), values);

		RouteCacheHolder.setRouteList(values);
		return Flux.fromIterable(values);
	}

}

参考

springcloudgateway动态路由

;