Spring Cloud Gateway源码解析-11-扩展RouteDefinitionRepository实现基于Redis的动态路由


系列文章

创作不易,如果对您有帮助,麻烦辛苦下小手点个关注,有任何问题都可以私信交流哈。
祝您虎年虎虎生威。


由来

Spring Cloud Gateway源码解析-10-自定义Predicate实现黑名单中我们自定义了Predicate来实现黑名单,但发现每次更改黑名单规则都要重启项目来实现,因此需要将路由信息存储在外部数据源中,定时刷新SCG内存中的路由信息。

思路

在这里插入图片描述

Spring Cloud Gateway源码解析-03-RouteDefinitionLocator、RouteLocator解析中我们已经介绍过RouteDefinitionRepository,该接口在SCG中只有一个实现InMemoryRouteDefinitionRepository,并且该接口继承了RouteDefinitionWriterRouteDefinitionWriter中定义了save、delete方法,通过方法名称可以知道是用来保存/添加/删除路由信息。

  1. 因此我们可以实现RouteDefinitionRepository用来保存从Redis中获取到的RouteDefinitionRedisRouteDefinitionRepository,由于RouteDefinitionRepository继承了RouteDefinitionLocator,因此会被CompositeRouteDefinitionLocator组合进去,从而被CachingRouteLocator拿到对应的Redis中的RouteDefinition装换成Route。
  2. 有了地方存储Redis中的定义的RouteDefinition,那是不是要有一个角色用来获取Redis中的数据,并组装成RouteDefinition保存到RedisRouteDefinitionRepository中,因此需要定义RedisRouteDefinitionRepositoryOperator用来从Redis中获取到数据库后生成RouteDefinition。可能我们的路由信息以后会放到MySQL、MongoDB等,因此可以抽象出一个从Repository中获取数据转换为RouteDefinition的接口RouteDefinitionRepositoryOperator
  3. 基于上边这些,我们就实现了当SCG启动时从Redis中获取数据转换为RouteDefinition,并保存到RedisRouteDefinitionRepository中,但是想要实现当修改了Redis中的路由信息后同步SCG更新,还不够,需要有一个类似Nacos的心跳机制,定时通知SCG去重新获取一次Redis中的数据。因此可以模仿Nacos的心跳机制实现RedisRouteDefinitionWatch发送心跳事件,触发CachingRouteLocator重新获取RouteDefinition来重新生成Route。

实现

RouteDefinitionRepositoryOperator

/**
 * 定义从不同数据源获取RouteDefinition的抽象
 * @author li.hongjian
 * @email lhj502819@163.com
 * @Date 2021/4/1
 */
public interface RouteDefinitionRepositoryOperator {

	Flux<RouteDefinition> getRouteDefinitions();

}

RedisRouteDefinitionRepositoryOperator

/**
 * Description:用来获取Redis中的RouteDefinition 并保存到{@link RedisRouteDefinitionRepository}
 *
 * @author li.hongjian
 * @email lhj502819@163.com
 * @Date 2021/4/1
 */
public class RedisRouteDefinitionRepositoryOperator implements RouteDefinitionRepositoryOperator {

	private final String REDIS_ROUTE_ID_PREFIX = "route-*";

	private StringRedisTemplate redisTemplate;

	public RedisRouteDefinitionRepositoryOperator(StringRedisTemplate redisTemplate) {
		this.redisTemplate = redisTemplate;
	}


	@Override
	public Flux<RouteDefinition> getRouteDefinitions() {
		//获取指定前缀的RedisKey。Redis的数据结构使用Hash,value的结构为predicates和filters,
		//predicates数据结构JsonArray,可配置多个 
		//  由于PredicateDefinition的构造方法支持传入类似Path=/api/hello这种格式的参数,并会自动封装为name和args,因此我们取巧可以在Redis中存储如下结构
		// 		如:["Path=/api/hello","BlackRemoteAddr=172.17.30.1/18,172.17.31.1/18"],表示PathRoutePredicateFactory和BlackRemoteAddrRoutePredicateFactory
		//filters与predicates一样
		return Flux.fromStream(redisTemplate.keys(REDIS_ROUTE_ID_PREFIX).parallelStream().map(routeId -> {
			RouteDefinition routeDefinition = new RouteDefinition();
			//以RedisKey作为RouteID
			routeDefinition.setId(routeId);
			Map<Object, Object> entries = redisTemplate.opsForHash().entries(routeId);
			String uri = (String) entries.get("uri");
			try {
				routeDefinition.setUri(new URI(uri));
			} catch (URISyntaxException e) {
				e.printStackTrace();
			}
			//初始化PredicateDefinition,并添加到RouteDefinition中
			initPredicate(routeDefinition, entries);

			//初始化FilterDefinition,并添加到RouteDefinition中
			initFilter(routeDefinition, entries);
			return routeDefinition;
		}));
	}

	private void initPredicate(RouteDefinition routeDefinition, Map<Object, Object> entries) {
		Object predicates = entries.get("predicates");
		if (predicates == null) {
			return;
		}
		JSONArray predicateArry = JSONArray.parseArray((String) predicates);
		predicateArry.parallelStream().forEach(predicate -> {
			//遍历predicates,创建RouteDefinition,并添加到RouteDefinition中
			PredicateDefinition predicateDefinition = new PredicateDefinition((String) predicate);
			routeDefinition.getPredicates().add(predicateDefinition);
		});
	}

	private void initFilter(RouteDefinition routeDefinition, Map<Object, Object> entries) {
		Object filters = entries.get("filters");
		if (filters == null) {
			return;
		}
		JSONArray predicateArry = JSONArray.parseArray((String) filters);
		predicateArry.parallelStream().forEach(filter -> {
			//遍历predicates,创建RouteDefinition,并添加到RouteDefinition中
			FilterDefinition filterDefinition = new FilterDefinition((String) filter);
			routeDefinition.getFilters().add(filterDefinition);
		});
	}
}

RedisRouteDefinitionRepository

/**
 * Description:基于Redis作为RouteDefinition Repository
 *
 * @author li.hongjian
 * @email lhj502819@163.com
 * @Date 2021/4/1
 */
public class RedisRouteDefinitionRepository implements RouteDefinitionRepository{

	private final Map<String, RouteDefinition> routes = synchronizedMap(
			new LinkedHashMap<String, RouteDefinition>());

	private RedisRouteDefinitionRepositoryOperator redidRouteDefinitionOperator;

	/**
	 * 将RedisRouteDefinitionRepositoryOperator组装进来
	 * @param redidRouteDefinitionOperator
	 */
	public RedisRouteDefinitionRepository(RedisRouteDefinitionRepositoryOperator redidRouteDefinitionOperator) {
		this.redidRouteDefinitionOperator = redidRouteDefinitionOperator;
	}

	/**
	 * 在{@link CompositeRouteDefinitionLocator#getRouteDefinitions()}调用时 调用redidRouteDefinitionOperator去Redis中取数据
	 * @return
	 */
	@Override
	public Flux<RouteDefinition> getRouteDefinitions() {
		redidRouteDefinitionOperator.getRouteDefinitions().flatMap(r -> save(Mono.just(r))).subscribe();
		return Flux.fromIterable(routes.values());
	}

	@Override
	public Mono<Void> save(Mono<RouteDefinition> route) {
		return route.flatMap(r -> {
			if (StringUtils.isEmpty(r.getId())) {
				return Mono.error(new IllegalArgumentException("id may not be empty"));
			}
			routes.put(r.getId(), r);
			return Mono.empty();
		});
	}

	@Override
	public Mono<Void> delete(Mono<String> routeId) {
		return routeId.flatMap(id -> {
			if (routes.containsKey(id)) {
				routes.remove(id);
				return Mono.empty();
			}
			return Mono.defer(() -> Mono.error(
					new NotFoundException("RouteDefinition not found: " + routeId)));
		});
	}
}

RedisRouteDefinitionWatch

/**
 * @author li.hongjian
 * @email lhj502819@163.com
 * @Date 2021/4/1
 */
public class RedisRouteDefinitionWatch implements ApplicationEventPublisherAware, SmartLifecycle {

	private final TaskScheduler taskScheduler = getTaskScheduler();

	private final AtomicLong redisWatchIndex = new AtomicLong(0);

	private final AtomicBoolean running = new AtomicBoolean(false);

	private ApplicationEventPublisher publisher;

	private ScheduledFuture<?> watchFuture;

	private static ThreadPoolTaskScheduler getTaskScheduler() {
		ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
		taskScheduler.setBeanName("Redis-Watch-Task-Scheduler");
		taskScheduler.initialize();
		return taskScheduler;
	}


	@Override
	public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
		this.publisher = publisher;
	}

	@Override
	public void start() {
		if (this.running.compareAndSet(false, true)) {
			this.watchFuture = this.taskScheduler.scheduleWithFixedDelay(
					this::redisServicesWatch, 30000); //启动一个定时,30s执行一次
		}
	}

	/**
	 * 这里最好是自定义一个事件,因为如果使用了Nacos的话,会冲突,这样的话需要修改SCG的源码,监听自定义的事件
	 * 我们就不这么做了,感兴趣的可以自行实现
	 */
	private void redisServicesWatch() {
		// nacos doesn't support watch now , publish an event every 30 seconds.
		this.publisher.publishEvent( //30s发布一次事件,通知SCG重新拉取
				new HeartbeatEvent(this, redisWatchIndex.getAndIncrement()));
	}

	@Override
	public void stop() {
		if (this.running.compareAndSet(true, false) && this.watchFuture != null) {
			this.watchFuture.cancel(true);
		}
	}

	@Override
	public boolean isRunning() {
		return false;
	}
}

这样就大功告成了,实现了基于Redis配置路由信息并且可动态刷新的功能。

使用

1、Redis中数据:
在这里插入图片描述

2、将RedisRouteDefinitionWatch、RedisRouteDefinitionRepository、RedisRouteDefinitionRepositoryOperator放到Spring容器中,比如@Bean注入
通过以上两步,即可完成。代码写的比较简陋。


大家可自行验证下,亲测有效。代码仓库地址

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 12
    评论
Spring Cloud Gateway中,你可以使用Redis实现动态获取Gateway路由信息。下面是一个基本的实现思路: 1. **配置Redis**:首先,确保你已经正确配置了Redis,并能够通过Spring Boot进行连接和操作。你可以参考Spring Data Redis的官方文档来了解如何配置和使用Redis。 2. **创建自定义的RouteDefinitionLocator**:创建一个自定义的`RouteDefinitionLocator`实现,用于从Redis中获取路由信息。这个实现类需要实现`getRouteDefinitions()`方法,并在该方法中从Redis中获取路由配置信息。 ```java @Component public class RedisRouteDefinitionLocator implements RouteDefinitionLocator { private final RedisTemplate<String, Object> redisTemplate; public RedisRouteDefinitionLocator(RedisTemplate<String, Object> redisTemplate) { this.redisTemplate = redisTemplate; } @Override public Flux<RouteDefinition> getRouteDefinitions() { List<RouteDefinition> routeDefinitions = new ArrayList<>(); // 从Redis中获取路由配置信息,并将其转换为RouteDefinition对象 // 将解析后的RouteDefinition对象添加到routeDefinitions列表中 return Flux.fromIterable(routeDefinitions); } } ``` 3. **配置Gateway使用自定义的RouteDefinitionLocator**:在配置类中,将自定义的`RouteDefinitionLocator`注册为Bean,并将其设置为Gateway的`RouteDefinitionLocator`。 ```java @Configuration public class GatewayConfig { @Bean public RedisTemplate<String, Object> redisTemplate() { // 创建和配置RedisTemplate实例 return redisTemplate; } @Bean public RouteDefinitionLocator routeDefinitionLocator(RedisTemplate<String, Object> redisTemplate) { return new RedisRouteDefinitionLocator(redisTemplate); } @Bean public RouteDefinitionLocatorComposite routeDefinitionLocatorComposite(RouteDefinitionLocator... locators) { return new RouteDefinitionLocatorComposite(Arrays.asList(locators)); } } ``` 在上述配置中,`RedisTemplate`是一个用于与Redis进行交互的Spring Data Redis的组件。你可以根据实际情况进行配置和使用。 4. **配置Redis中的路由信息**:在Redis中存储路由信息,可以使用Hash结构来存储每个路由的详细信息,例如路由ID、URI、谓词、过滤器等。 ```shell HSET gateway_routes route1 '{ "id": "route1", "uri": "http://example.com", "predicates": [{ "name": "Path", "args": { "pattern": "/example" } }] }' HSET gateway_routes route2 '{ "id": "route2", "uri": "http://example.org", "predicates": [{ "name": "Path", "args": { "pattern": "/example2" } }] }' ``` 上述示例中,使用Hash结构存储了两个路由的详细信息,每个路由信息以JSON格式表示。 通过以上步骤,你就可以通过Redis动态获取Gateway路由信息了。每当路由配置信息在Redis中发生变化时,Gateway会自动重新加载路由信息。 需要注意的是,上述示例仅提供了一个基本的思路和示例代码,你可能需要根据实际需求进行适当的调整和扩展。 希望以上内容对你有所帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

壹氿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值