• springcloud-gateway 路由加载流程


    问题

    Spring Cloud Gateway版本是2.2.9.RELEASE,原本项目中依赖服务自动发现来自动配置路由到微服务的,但是发现将spring.cloud.gateway.discovery.locator.enabled=false 启动之后Gateway依然会将所有微服务自动注册到路由中,百思不得其解,遂调试代码观察期启动过曾,记录此文以供参考,官方文档
    在这里插入图片描述
    可以看到此处明确表示,可以通过设置该值来讲微服务中服务自动添加到配置中。按照这个线索,找到了
    org.springframework.cloud.gateway.discovery.DiscoveryClientRouteDefinitionLocator#getRouteDefinitions
    这个类在之前的文章includeExpression中有提及过,在构造函数中对成员变量

    public class DiscoveryClientRouteDefinitionLocator implements RouteDefinitionLocator {
    
    	private final DiscoveryLocatorProperties properties;
    	...
    	private Flux<List<ServiceInstance>> serviceInstances;
    
    	/**
    	 * Kept for backwards compatibility. You should use the reactive discovery client.
    	 * @param discoveryClient the blocking discovery client
    	 * @param properties the configuration properties
    	 * @deprecated kept for backwards compatibility
    	 */
    	@Deprecated
    	public DiscoveryClientRouteDefinitionLocator(DiscoveryClient discoveryClient,
    			DiscoveryLocatorProperties properties) {
    		this(discoveryClient.getClass().getSimpleName(), properties);
    		serviceInstances = Flux
    				.defer(() -> Flux.fromIterable(discoveryClient.getServices()))
    				.map(discoveryClient::getInstances)
    				.subscribeOn(Schedulers.boundedElastic());
    	}
    
    	public DiscoveryClientRouteDefinitionLocator(ReactiveDiscoveryClient discoveryClient,
    			DiscoveryLocatorProperties properties) {
    		this(discoveryClient.getClass().getSimpleName(), properties);
    		serviceInstances = discoveryClient.getServices()
    				.flatMap(service -> discoveryClient.getInstances(service).collectList());
    	}
    	...
    

    这里会对serviceInstances进行初始化,从注册中心获取所有已注册的微服务实例进行填充,serviceInstance的获取结束了,在DiscoveryClientRouteDefinitionLocator还有一个重要的方法

    #DiscoveryClientRouteDefinitionLocator
    	@Override
    	public Flux<RouteDefinition> getRouteDefinitions() {
    
    		SpelExpressionParser parser = new SpelExpressionParser();
    		Expression includeExpr = parser
    				.parseExpression(properties.getIncludeExpression());
    		Expression urlExpr = parser.parseExpression(properties.getUrlExpression());
    
    		Predicate<ServiceInstance> includePredicate;
    		if (properties.getIncludeExpression() == null
    				|| "true".equalsIgnoreCase(properties.getIncludeExpression())) {
    			includePredicate = instance -> true;
    		}
    		else {
    			includePredicate = instance -> {
    				Boolean include = includeExpr.getValue(evalCtxt, instance, Boolean.class);
    				if (include == null) {
    					return false;
    				}
    				return include;
    			};
    		}
    
    		return serviceInstances.filter(instances -> !instances.isEmpty())
    				.map(instances -> instances.get(0)).filter(includePredicate)
    				.map(instance -> {
    					RouteDefinition routeDefinition = buildRouteDefinition(urlExpr,
    							instance);
    
    					final ServiceInstance instanceForEval = new DelegatingServiceInstance(
    							instance, properties);
    
    					for (PredicateDefinition original : this.properties.getPredicates()) {
    						PredicateDefinition predicate = new PredicateDefinition();
    						predicate.setName(original.getName());
    						for (Map.Entry<String, String> entry : original.getArgs()
    								.entrySet()) {
    							String value = getValueFromExpr(evalCtxt, parser,
    									instanceForEval, entry);
    							predicate.addArg(entry.getKey(), value);
    						}
    						routeDefinition.getPredicates().add(predicate);
    					}
    
    					for (FilterDefinition original : this.properties.getFilters()) {
    						FilterDefinition filter = new FilterDefinition();
    						filter.setName(original.getName());
    						for (Map.Entry<String, String> entry : original.getArgs()
    								.entrySet()) {
    							String value = getValueFromExpr(evalCtxt, parser,
    									instanceForEval, entry);
    							filter.addArg(entry.getKey(), value);
    						}
    						routeDefinition.getFilters().add(filter);
    					}
    
    					return routeDefinition;
    				});
    	}
    
    

    根据includeExpr 配置的sl表达式判断当前的serviceInstance是否服务转发要求。填充好predicate,filter之后返回routeDefinition,urlExpr的默认值是"lb://"+serverId,predicate匹配的path就等于serverId,有配置可以转换为小写spring.cloud.gateway.discovery.locator.lower-case-service-id=true因为eureka默认会uppercase serverId,filter中会StripPrefix=1,将路径中的serverId移除再转发给下游。路由定义的部分就这里了,他的刷新逻辑,依靠SpringRefreshContext 事件,具体的定义逻辑在org.springframework.cloud.gateway.config.GatewayAutoConfiguration

    	@Bean
    	@ConditionalOnClass(
    			name = "org.springframework.cloud.client.discovery.event.HeartbeatMonitor")
    	public RouteRefreshListener routeRefreshListener(
    			ApplicationEventPublisher publisher) {
    		return new RouteRefreshListener(publisher);
    	}
    
    

    RouteRefreshListener 实现了ApplicationListener,监控以下几个事件,并按照条件进行逻辑执行

    • ContextRefreshEvent 启动容器,或者beans重新加载
    • RefreshScopeRefreshdEvent 是 Spring Cloud 中的一个事件,当 @RefreshScope 注解的 beans 被重新加载时发布。这个事件主要用于 Spring Cloud 的配置刷新机制,当配置中心的配置信息发生变化时,会触发该事件以刷新 beans。
    • ParentHeartbeatEvent 是 Spring Cloud Bus 中的一个事件,用于在父上下文(通常是 Spring Cloud Config Server)发生心跳时发布。这个事件用于通知子上下文(如微服务)关于父上下文的健康状态。
    • HeartbeatEvent 是 Spring Cloud Bus 中的另一个事件,用于在服务实例之间广播心跳消息,以检测和维持各个服务的健康状态。
    	public void onApplicationEvent(ApplicationEvent event) {
    		if (event instanceof ContextRefreshedEvent) {
    			ContextRefreshedEvent refreshedEvent = (ContextRefreshedEvent) event;
    			if (!WebServerApplicationContext.hasServerNamespace(
    					refreshedEvent.getApplicationContext(), "management")) {
    				reset();
    			}
    		}
    		else if (event instanceof RefreshScopeRefreshedEvent
    				|| event instanceof InstanceRegisteredEvent) {
    			reset();
    		}
    		else if (event instanceof ParentHeartbeatEvent) {
    			ParentHeartbeatEvent e = (ParentHeartbeatEvent) event;
    			resetIfNeeded(e.getValue());
    		}
    		else if (event instanceof HeartbeatEvent) {
    			HeartbeatEvent e = (HeartbeatEvent) event;
    			resetIfNeeded(e.getValue());
    		}
    	}
    

    容器启动完成之后,该监听器接收到ContextRefreshedEvent ,会执行reset方法,reset方法主要功能是发送一个RefreshRoutesEvent 事件,定义如下

    public class RefreshRoutesEvent extends ApplicationEvent {
    	public RefreshRoutesEvent(Object source) {
    		super(source);
    	}
    }
    

    同样在AutoConfig中声明的CachingRouteLocator接收该参数,

    tip:另外还有个CachingRouteDefinitionLocator也生命监听 RefreshRoutesEvent事件,但是默认配置下并不会产生任何逻辑

    	@Bean
    	@Primary
    	@ConditionalOnMissingBean(name = "cachedCompositeRouteLocator")
    	// TODO: property to disable composite?
    	public RouteLocator cachedCompositeRouteLocator(List<RouteLocator> routeLocators) {
    		return new CachingRouteLocator(
    				new CompositeRouteLocator(Flux.fromIterable(routeLocators)));
    	}
    

    event invoke方法如下

    	public void onApplicationEvent(RefreshRoutesEvent event) {
    		try {
    			fetch().collect(Collectors.toList()).subscribe(list -> Flux.fromIterable(list)
    					.materialize().collect(Collectors.toList()).subscribe(signals -> {
    						applicationEventPublisher
    								.publishEvent(new RefreshRoutesResultEvent(this));
    						cache.put(CACHE_KEY, signals);
    					}, throwable -> handleRefreshError(throwable)));
    		}
    		catch (Throwable e) {
    			handleRefreshError(e);
    		}
    	}
    

    CompositeRouteLocator的目的是混合 多个来源的Routers定义比如配置文件,接口,注册中心,通过@Bean形式定义的路由。可以理解成mixed route locator。
    fetch方法就是获取所有RouteLocator的路由定义
    private Flux fetch() { return this.delegate.getRoutes().sort(AnnotationAwareOrderComparator.INSTANCE); }
    这里其实就是指定,之前org.springframework.cloud.gateway.discovery.DiscoveryClientRouteDefinitionLocator#getRouteDefinitions的方法也就是获取具体的路由定义,获取成功之后放入缓存,Fire RefreshRoutesResultEvent事件就解决了,开发人员可以通过 监听RefreshRoutesResultEvent 主动感知当前的路由变化。路由加载流程到此结束,

    后语

    我的问题是通过设置spring.cloud.gateway.discovery.locator.enabled依然没有办法让微服务注册失效,在整个流程中并没有看到使用该数值的地方,难道这是一个bug?转念一想DiscoveryClientRouteDefinitionLocator只需要阻止该Bean的注册即可取消掉从eureka获取服务实例的能力,通过引用关系可知

    • org.springframework.cloud.gateway.discovery.GatewayDiscoveryClientAutoConfiguration.ReactiveDiscoveryClientRouteDefinitionLocatorConfiguration
    • org.springframework.cloud.gateway.discovery.GatewayDiscoveryClientAutoConfiguration.BlockingDiscoveryClientRouteDefinitionLocatorConfiguration

    这里两处找到了其定义,一个是为reactive非阻塞io准备的,一个是为阻塞io准备的,根据源代码可知,具体执行提供那个Locator取决于spring.cloud.discovery.reactive.enabled,我的项目中是通过VM Options参数的形式传入的-Dspring.cloud.config.discovery.enabled=true,所以我在这两个方法处都打了断点。

    		@Configuration(proxyBeanMethods = false)
    	@ConditionalOnProperty(value = "spring.cloud.discovery.reactive.enabled",
    			matchIfMissing = true)
    	public static class ReactiveDiscoveryClientRouteDefinitionLocatorConfiguration {
    
    		@Bean
    		@ConditionalOnProperty(name = "spring.cloud.gateway.discovery.locator.enabled")
    		public DiscoveryClientRouteDefinitionLocator discoveryClientRouteDefinitionLocator(
    				ReactiveDiscoveryClient discoveryClient,
    				DiscoveryLocatorProperties properties) {
    			return new DiscoveryClientRouteDefinitionLocator(discoveryClient, properties);
    		}
    
    	}
    
    	@Configuration(proxyBeanMethods = false)
    	@Deprecated
    	@ConditionalOnProperty(value = "spring.cloud.discovery.reactive.enabled",
    			havingValue = "false")
    	public static class BlockingDiscoveryClientRouteDefinitionLocatorConfiguration {
    
    		@Bean
    		@ConditionalOnProperty(name = "spring.cloud.gateway.discovery.locator.enabled")
    		public DiscoveryClientRouteDefinitionLocator discoveryClientRouteDefinitionLocator(
    				DiscoveryClient discoveryClient, DiscoveryLocatorProperties properties) {
    			return new DiscoveryClientRouteDefinitionLocator(discoveryClient, properties);
    		}
    
    	}
    

    当我准备要执行调试模式想看看具体是哪个Bean被初始化的时候,恍然大悟看到了DiscoveryClientRouteDefinitionLocator的依赖,我自己在GatewayApplication 中手动注册了Bean

        @Bean
        public DiscoveryClientRouteDefinitionLocator discoveryClientRouteLocator(ReactiveDiscoveryClient discoveryClient,
                DiscoveryLocatorProperties discoveryLocatorProperties) {
            return new DiscoveryClientRouteDefinitionLocator(discoveryClient, discoveryLocatorProperties);
        }
    

    真的是笑死,出这个问题的原因是因为最开始的代码使用的出c4o老师生成的,一开始并不明确该代码的作用,后面查看文档才知道用于微服务发现。已经不在关注这里了。

  • 相关阅读:
    UWB定位系统中为何要加入陀螺仪
    已解决java.nio.charset.CoderMalfunctionError: 编码器故障错误的正确解决方法,亲测有效!!!
    Java设计模式之抽象工厂模式
    动态代理(CGlib和jdk)
    java计算机毕业设计西安财经大学校园一卡通管理系统源码+系统+数据库+lw文档+mybatis+运行部署
    Redis(主从复制、哨兵模式、集群)概述及部署
    mysql文档--架构体系--《Mysql底层探索》-文档架构首页
    驳"一切不谈考核的管理都是扯淡"
    Servlet的注册和生命周期
    WebKit Inside: CSS 样式表解码字符集
  • 原文地址:https://blog.csdn.net/topc2000/article/details/139956976