作者:陆永剑 职位:后端工程师
在微服务化过程中,客户端可能需要调用多个服务的接口才能完成一个业务需求。网关作为后端微服务的统一入口,利用路由转发可以方便客户端快速接入各个微服务。
Spring Gateway (以下简称gateway)路由配置默认情况下是写在配置文件中的,有一个新的服务接入时,需要修改配置文件,然后重启网关才能生效。然而目前更多的需求是通过提供可视化页面,在前端页面增删改来使网关路由动态生效。
结合实际业务需求,接下来从原理和实践两个方面介绍如何上述需求。
我们分两部分来说,先说动态加载,再说动态刷新。
要想实现动态加载,我们要先弄清楚gateway是如何加载现有的配置信息的。
目前配置路由主要有两种方式,一种是用yml配置文件,一种是写代码里。
# yml配置文件形式
spring:
cloud:
gateway:
routes:
- id: v1
uri: http://xxx/v1
predicates:
- Path=/xxx
// 代码形式
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("v1", r -> r.path("/xxx")
.
uri("http://xxx/v1"))
.build();
}
无论是yml还是代码,这些配置最终都是被封装到RouteDefinition对象中的。所有路由信息在系统启动时就被加载装配好了,并存到了内存里。
参考代码如下:
//GatewayAutoConfiguration
@Bean
@ConditionalOnMissingBean
public PropertiesRouteDefinitionLocator
propertiesRouteDefinitionLocator(
GatewayProperties properties) {
return new PropertiesRouteDefinitionLocator(properties);
}
@Bean
@ConditionalOnMissingBean(RouteDefinitionRepository.class)
public InMemoryRouteDefinitionRepository
inMemoryRouteDefinitionRepository() {
return new InMemoryRouteDefinitionRepository();
}
//PropertiesRouteDefinitionLocator
public class PropertiesRouteDefinitionLocator implements
RouteDefinitionLocator {
private final GatewayProperties properties;
...
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
return Flux.fromIterable(this.properties.getRoutes());
}
}
重点类为 RouteDefinitionLocator,此接口有多个实现类,分别对应不同方式配置的路由方式。其中 RouteDefinitionRepository 是从存储器中(例如:mysql、redis 等)读取 RouteDefinition。可以结合 RouteDefinitionRepository 来实现动态路由加载。其他几种获取路由方式不是本文重点,暂不详说。
这样,我们就可以在网关启动时从一个动态数据源(数据库等,而非代码和配置文件)加载配置数据,并通过 RouteDefinitionRepository 的实现类 InMemoryRouteDefinitionRepository 加载为路由信息了。
通过2.1我们找到了gateway从数据库加载路由的方式,但是还停留在gateway启动时加载数据的方式。接下来就看下如何实现在用户操作数据的时候就能完成gateway路由的刷新。
其实gateway是提供了刷新的事件的,那就是RefreshRoutesEvent。
参考代码如下:
public class RefreshRoutesEvent extends ApplicationEvent {
/**
* Create a new ApplicationEvent.
* @param source the object on which the event initially occurred
(never {@code null})
*/
public RefreshRoutesEvent(Object source) {
super(source);
}
}
我们再来深挖下触发了RefreshRoutesEvent事件后,gateway是如何处理的。
@Override
public void onApplicationEvent(ApplicationEvent event) {
...
else if (event instanceof RefreshRoutesEvent && routeLocator != null)
{
// forces initialization
routeLocator.ifAvailable(locator ->
locator.getRoutes().subscribe());
}
}
所以,我们的思路是,在用户操作路由数据,在数据保存到数据库之后,代码向gateway发送刷新事件就可以了。
通过原理的分析,了解了网关从存储器中加载和动态刷新的机制。我们来整理下思路。
想加载路由数据到gateway内存中,需要调用InMemoryRouteDefinitionRepository 的save()方法。
想触发gateway的动态刷新,需要调用RefreshRoutesEvent事件。
接下来从这两点出发,一起开启实践之旅。
动态加载按照顺序可以分为三步,加载路由数据、组装路由、保存到gateway内存中。
可以通过 feign 调用的方式读取数据,此步骤比较简单,不做详细的描述。
List<GatewayRoute> routeList =
gatewayServiceClient.getApiRouteList().getData();
刚刚也分析到了RouteDefinition 是路由数据的核心。它大体包括三个部分,基本信息、断言(PredicateDefinition)、过滤器(FilterDefinition)。
接下来根据查询到的路由数据,进行gateway路由数据封装和保存。
//路由封装
routeList.forEach(gatewayRoute -> {
//RouteDefinition结构
RouteDefinition definition = new RouteDefinition();
//设置基本信息
definition.setId(gatewayRoute.getRouteName());
definition.setUri(UriComponentsBuilder.fromUriString("lb://" +gatewayRoute.getServiceId()).build().toUri());
//设置断言信息
List<PredicateDefinition> predicates = Lists.newArrayList();
PredicateDefinition predicatePath = new PredicateDefinition();
Map<String, String> predicatePathParams = new HashMap<>(8);
predicatePath.setName("Path");
predicatePathParams.put("name", gatewayRoute.getRouteName());
predicatePathParams.put("pattern", gatewayRoute.getPath());
predicatePathParams.put("pathPattern", gatewayRoute.getPath());
predicatePath.setArgs(predicatePathParams);
predicates.add(predicatePath);
definition.setPredicates(predicates);
//设置过滤器信息
List<FilterDefinition> filters = filters(gatewayRoute);
if (!CollectionUtils.isEmpty(filters)) {
definition.setFilters(filters);
}
//重点,保存到gateway内存,其实是放在了Map<String, RouteDefinition> routes里 this.repository.save(Mono.just(definition)).subscribe();
});
动态刷新分为两步。第一步是提供公共方法,允许业务侧更新数据库之后调用刷新网关事件。第二步在gateway端接收到刷新网关事件后,调用RefreshRoutesEvent事件实现刷新。
参考代码如下
public class OpenRestTemplate extends RestTemplate {
/**
* 刷新网关
*/
public void refreshGateway() {
publisher.publishEvent(
new
RefreshRemoteApplicationEvent(this,busProperties.getId(),null)
);
}
}
gateway接收到业务侧路由刷新事件后,调用 RefreshRoutesEvent 即可完成刷新动作。
参考代码如下
/**
* 接收业务侧刷新事件
*
* @param event
*/
@Override
public void onApplicationEvent(RefreshRemoteApplicationEvent event) {
refresh();
}
/**
* 刷新路由
*
* @return
*/
public Mono<Void> refresh() {
//3.1加载路由的逻辑
this.loadRoutes();
//触发默认路由刷新事件,刷新缓存路由
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return Mono.empty();
结合实际业务需求,从原理和实践两个方面介绍了实现网关路由的动态加载和刷新方式。至此动态加载和刷新的功能就实现了,通过此功能,无论开发、测试、生产环境,都可以通过可视化页面快速的实现路由的动态修改,在配置速度和准确率上都可以得到很大提升。