环境
spring-cloud-gateway 版本
org.springframework.cloud
spring-cloud-starter-gateway
2.1.2.RELEASE
现象
目前设计的是路由存在数据库中,自定义实现了一个RouteDefinitionRepository。
打印日志发现路由每30s就加载一次。
自定义MySqlRouteDefinitionRepository 如下
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.stream.Stream;
@Slf4j
public class MySqlRouteDefinitionRepository implements RouteDefinitionRepository{
private volatile boolean init = false;
@Override
public Flux getRouteDefinitions() {
log.info("getRouteDefinitions");
// 从数据库中加载
Stream routeDefinitionStream = routeMapper.select().stream().map(e->routeDefinitionConverter.convert(routeDefinitionDTO));
return Flux.fromStream(routeDefinitionStream);
}
@Override
public Mono save(Mono route) {
return Mono.empty();
}
@Override
public Mono delete(Mono routeId) {
return Mono.empty();
}
}
由哪里触发的路由加载?
在加载路由的地方打了断点,但是由于是响应式编程,无法得知具体触发的地方。根据org.springframework.cloud.gateway.route.RouteDefinitionLocator#getRouteDefinitions 调用入口找到了
org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#getRoutes,然后又找到了
org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter#onApplicationEvent
具体的调用堆栈为
org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter#onApplicationEvent
org.springframework.cloud.gateway.route.CachingRouteLocator#getRoutes
org.springframework.cloud.gateway.route.CompositeRouteLocator#getRoutes
org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#getRoutes
org.springframework.cloud.gateway.route.CompositeRouteDefinitionLocator#getRouteDefinitions
可以看是由于收到了一个RefreshRoutesEvent事件,导致了路由的刷新。
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof PredicateArgsEvent) {
handle((PredicateArgsEvent) event);
}
else if (event instanceof WeightDefinedEvent) {
addWeightConfig(((WeightDefinedEvent) event).getWeightConfig());
}
else if (event instanceof RefreshRoutesEvent && routeLocator != null) {
routeLocator.ifAvailable(locator -> locator.getRoutes().subscribe()); // forces
// initialization
}
}
在WeightCalculatorWebFilter 打了一个断点,堆栈如下:
可以从nacosServicesWatch:130, NacosWatch (com.alibaba.cloud.nacos.discovery)这行看出是Nacos的心跳触发了路由的刷新
onApplicationEvent:133, WeightCalculatorWebFilter (org.springframework.cloud.gateway.filter)
doInvokeListener:172, SimpleApplicationEventMulticaster (org.springframework.context.event)
invokeListener:165, SimpleApplicationEventMulticaster (org.springframework.context.event)
multicastEvent:139, SimpleApplicationEventMulticaster (org.springframework.context.event)
publishEvent:402, AbstractApplicationContext (org.springframework.context.support)
publishEvent:359, AbstractApplicationContext (org.springframework.context.support)
reset:68, RouteRefreshListener (org.springframework.cloud.gateway.route)
resetIfNeeded:63, RouteRefreshListener (org.springframework.cloud.gateway.route)
onApplicationEvent:57, RouteRefreshListener (org.springframework.cloud.gateway.route)
doInvokeListener:172, SimpleApplicationEventMulticaster (org.springframework.context.event)
invokeListener:165, SimpleApplicationEventMulticaster (org.springframework.context.event)
multicastEvent:139, SimpleApplicationEventMulticaster (org.springframework.context.event)
publishEvent:402, AbstractApplicationContext (org.springframework.context.support)
publishEvent:359, AbstractApplicationContext (org.springframework.context.support)
nacosServicesWatch:130, NacosWatch (com.alibaba.cloud.nacos.discovery)
run:-1, 1684543956 (com.alibaba.cloud.nacos.discovery.NacosWatch$$Lambda$1039)
run:54, DelegatingErrorHandlingRunnable (org.springframework.scheduling.support)
call:511, Executors$RunnableAdapter (java.util.concurrent)
runAndReset$$$capture:308, FutureTask (java.util.concurrent)
runAndReset:-1, FutureTask (java.util.concurrent)
如何解决?
MySqlRouteDefinitionRepository 直接处理心跳事件
需要达到如下几点要求
- 第一次要从数据库中获取路由
- 当主动调用路由刷新接口 /refresh 时,需要从数据库中获取路由。
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
import org.springframework.cloud.gateway.route.RouteRefreshListener;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.event.SmartApplicationListener;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Slf4j
public class MySqlRouteDefinitionRepository implements RouteDefinitionRepository, SmartApplicationListener {
private List routes = new ArrayList<>();
private volatile boolean init = false;
@Override
public Flux getRouteDefinitions() {
log.info("getRouteDefinitions");
return Flux.fromIterable(routes);
}
@Override
public Mono save(Mono route) {
return Mono.empty();
}
@Override
public Mono delete(Mono routeId) {
return Mono.empty();
}
@Override
public boolean supportsEventType(Class extends ApplicationEvent> eventType) {
return RefreshRoutesEvent.class.isAssignableFrom(eventType);
}
@Override
public void onApplicationEvent(ApplicationEvent event) {
RefreshRoutesEvent refreshRoutesEvent = (RefreshRoutesEvent) event;
Object source = refreshRoutesEvent.getSource();
if(source instanceof RouteRefreshListener && init){
log.info("RouteRefreshListener return");
return;
}
init = true;
log.info("load route source:{}",source.getClass().getName());
// 从数据库中加载
Stream routeDefinitionStream = routeMapper.select().stream().map(e->routeDefinitionConverter.convert(routeDefinitionDTO));
routes = routeDefinitionStream.collect(Collectors.toList());
}
}
为什么需要心跳事件时去加载路由呢
Refresh routes on heartbeat event when using discovery client #236