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 与内存都只是缓存/快照)。 * * */ @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(); } }