RequestRateLimiter的配置装配流程分析

背景

这几天分析 RequestRateLimterGatewayFilter 的源码,一直对 RedisRateLimter 的限流参数是怎么绑定的这个问题困扰。

今天开机后看到周末跟踪代码打开的窗口,又按流程分析了一遍,终于从 Spring 容器的事件监听/发布原理中找到了答案。

本文来分析每个路由的限流参数绑定的细节,关键在于 FilterArgsEvent 事件的触发监听上。

FilterArgsEvent 事件触发

step1,路由定义加载拦截器。

这是由RouteDefinitionRouteLocator 类的 loadGatewayFilters 方法完成的,它遍历路由配置列表,循序执行下列操作:

  1. 根据拦截器工厂名称,获取拦截器工厂实例 GatewayFilterFactory factory = this.gatewayFilterFactories.get(definition.getName())
  2. 用该工厂实例组装一个拦截器配置类对象 Object configuration = this.configurationService.with(factory).xx.xxx.bind()
  3. 用步骤 2 的配置创建一个拦截器对象,并加入到目标列表 GatewayFilter gatewayFilter = factory.apply(configuration)

step2,创建拦截器配置对象的装配过程

Object configuration = this.configurationService.with(factory)
  .name(definition.getName())
  .properties(definition.getArgs())
  .eventFunction((bound, properties) -> new FilterArgsEvent(
  		// TODO: why explicit cast needed or java compile fails
  		RouteDefinitionRouteLocator.this, id, (Map<String, Object>) properties))
  .bind();
复制代码

配置实例 configuration 的过程中,有一个绑定 eventFunction 的操作,它在 bind 操作中被调用。

跟踪源码发现,在 ConfigurationService 的内部类 AbstractBuilderbind 方法中触发了 FilterArgsEvent 事件:

if (this.eventFunction != null && this.service.publisher != null) {
		ApplicationEvent applicationEvent = this.eventFunction.apply(bound, this.normalizedProperties);
		this.service.publisher.publishEvent(applicationEvent);
}
复制代码

step3,事件发布类是什么时候、由谁设置的呢?

疑点在于,这个 this.service.publisher 属性是什么时候、由谁装配的呢?这就回到了 Spring 框架的底层了。

由类声明ConfigurationService implements ApplicationEventPublisherAware 可知,这个装配类实现了 Spring 的 ApplicationEventPublisherAware 接口,那么它的实例被托管到 Spring 容器后,Spring 会自动注入该成员变量

对的,就是 Spring 自动注入的,不需要成员变量上指定或者构造函数指定这个成员变量,Spring 容器会自动调用实现了 ApplicationEventPublisherAware 接口的 Bean 的 setApplicationEventPublisher 方法设置事件发布对象。

step4,工厂工作的时刻

接着用上一步创建的路由工厂配置对象,创建 GatewayFilter 对象:

GatewayFilter gatewayFilter = factory.apply(configuration);
if (gatewayFilter instanceof Ordered) {
		ordered.add(gatewayFilter);
}
复制代码

这就完成了 loadGatewayFilter 的流程,本质就是生成一个个配置对象,并调用工厂的 apply(Config)方法,得到一个个拦截器实例。

FilterArgsEvent 事件监听

有些简单的拦截器不需要额外配置时,可能就不需要监听该事件。对于限流拦截器来说,它监听了 FilterArgsEvent 事件,并从中获取 redis-rate-limiter 前缀的属性值,最后组装成 RedisRateLimiter.Config 配置。

前面分析的创建限流拦截器的方法apply(Config) ,它的入参仅仅决定了工厂类的配置,即使用的 keyResolverrateLimiter 实例,没有涉及每个路由的 RateLimiter 的限流参数 。

关键点在于:限流拦截器有两个配置,一个是拦截器工厂配置,一个是限流算法的配置,这两种配置信息都需要从 args 获取。

Spring 事件监听机制

对于实现了 Spring 事件监听接口 ApplicationListener 的类,它们的实例被托管到 Spring 容器后,会被加入容器的监听器列表。

当Spring 容器触发某事件的时候,会逐一调用事件监听器类的 onApplicationEvent 方法,通知它们。

事件监听机制是 Spring 底层的细节,开发者只需要向容器注入一个实现监听器的类对象,它会自动加入全局列表的。

RedisRateLimiter 类

接着回到 RedisRateLimiter 类,它是一个 ApplicationEventLinster 实现类,因为它的父类 AbstractRateLimiter 当路由装载配置触发 FilterArgsEvent 的时候,它会监听该事件,并执行如下流程:

public abstract class AbstractRateLimiter<C> 
       extends AbstractStatefulConfigurable<C>
	   implements RateLimiter<C>,  ApplicationListener<FilterArgsEvent> {

     @Override
	public void onApplicationEvent(FilterArgsEvent event) {
		Map<String, Object> args = event.getArgs();

		if (args.isEmpty() || !hasRelevantKey(args)) {
			return;
		}

		String routeId = event.getRouteId();

		C routeConfig = newConfig();
		if (this.configurationService != null) {
		this.configurationService.with(routeConfig).name(this.configurationPropertyName).normalizedProperties(args)
					.bind();
		}
		getConfig().put(routeId, routeConfig);
	}
}	   
复制代码

所以,在路由配置信息加载过程中,RedisRateLimter 监听到 FilterArgsEvent,然后获取限流算法需要的参数,并且组织成 RedisRateLimiter.Config 实例,从而完成对每个路由限流参数设置的功能。

RedisRateLimiter 是单例,所有引用该拦截器的路由,都需要保存各自的配置信息,所以它维护了一个配置集合 getConfig(),以 routeIdkey 存储。

至此,就弄明白了 RedisRateLimiter 的限流参数的装配过程。

同一个路由指定不同限流器

一个路由可以配置多个拦截器,如果我们配置多个限流拦截器的话,getConfig().put(routeId, routeConfig),前面的配置会被后面的配置覆盖,因为它是以 routeId 为主键存储配置的。

看这个配置:
在这里插入图片描述

RedisRateLimiteronApplicationEvent 处理流程来看,它按 routId 存储每个路由的限流算法参数。

要想给同一个路由配置多个限流拦截器,默认情况下只有最后一个配置生效,需要改造这个配置存储的过程。

这个问题可以参考《Spring Cloud Gateway 限流适配多规则的解决方案》