package com.zsjz.gateway.registry;
import com.zsjz.gateway.config.GatewayProperties;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Optional;
/**
* 服务/节点配置的内存快照(事实来源是 PostgreSQL,Redis 与内存都只是缓存/快照)。
*
*
* - 启动时:gateway_service 为空则用 YAML 播种,然后加载快照;
* - 运行中:本实例管理接口写库后立即 {@link #reload()};其他实例每 10 秒对账一次,
* 快照变化时发布 {@link RefreshRoutesEvent} 刷新路由缓存。
*
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class GatewayRegistry {
private final GatewayConfigRepository repository;
private final GatewayProperties properties;
private final ApplicationEventPublisher eventPublisher;
private volatile List services = List.of();
/**
* 首次加载(@PostConstruct 阶段)不能发布 RefreshRoutesEvent:启动期事件监听会
* 拉起路由基础设施(CachingRouteLocator -> 路由定义仓库 -> 本类),形成循环依赖。
* 初始路由定义由 CachingRouteLocator 自己创建时拉取,不需要事件;后续变更才需要。
*/
private volatile boolean initialized = false;
@PostConstruct
void init() {
seedFromYamlIfNeeded();
reload();
}
/** 首次启动播种:表为空时把 gateway.services 写入数据库;之后 YAML 不再覆盖 */
void seedFromYamlIfNeeded() {
if (repository.countServices() > 0) {
return;
}
List defs = properties.getServices();
if (defs == null || defs.isEmpty()) {
log.warn("gateway_service 表为空且 gateway.services 未配置,网关没有任何可转发服务");
return;
}
for (GatewayProperties.ServiceDef def : defs) {
GatewayConfigRepository.validateServiceId(def.getId());
GatewayConfigRepository.validatePathPrefix(def.getPathPrefix());
repository.seedService(def.getId(), def.getName(), def.getPathPrefix(),
def.getLoginPath(), def.getHealthPath(), def.getDefaultNodeId());
if (def.getNodes() != null) {
for (GatewayProperties.NodeDef node : def.getNodes()) {
GatewayConfigRepository.validateNodeId(node.getId());
GatewayConfigRepository.validateBaseUrl(node.getBaseUrl());
repository.seedNode(node.getId(), def.getId(), node.getName(), node.getBaseUrl());
}
}
}
log.info("已按 YAML 播种 {} 个网关服务", defs.size());
}
/**
* 从数据库重载快照;有变化时发布路由刷新事件。
*
* @return true 表示快照发生了变化
*/
public synchronized boolean reload() {
List fresh = repository.loadAll();
boolean changed = !fresh.equals(services);
if (changed) {
services = fresh;
if (initialized) {
eventPublisher.publishEvent(new RefreshRoutesEvent(this));
}
log.info("网关配置已重载:{} 个服务 {}", fresh.size(),
fresh.stream().map(ServiceInfo::id).toList());
}
initialized = true;
return changed;
}
/** 其他网关实例写库后的对账(多实例共享 PG,写侧只刷新自己,读侧靠这里收敛) */
@Scheduled(fixedDelayString = "${gateway.registry-poll-interval:10s}")
void pollForChanges() {
try {
reload();
} catch (Exception ex) {
log.warn("网关配置对账失败,下一轮重试:{}", ex.toString());
}
}
public List getServices() {
return services;
}
public Optional getService(String serviceId) {
return services.stream().filter(s -> s.id().equals(serviceId)).findFirst();
}
/** 按路径前缀找服务(快照已按前缀长度降序,第一个命中的即最长前缀) */
public Optional findServiceByPath(String rawPath) {
return services.stream().filter(s -> s.matchesPath(rawPath)).findFirst();
}
}