gateway配置自定义转发
需求背景:
项目上想要在线查看日志,但是又不想搭建elk服务组件。咋解决呢? 所有的项目都引入一个日志模块,能拉取当前项目下的日志文件目录和提供在线查看和下载的功能。
对外暴露出来的服务只有一个,那么就需要根据标识信息进行转发了,我们又两个路由转发负载均衡的组件,一个是gateway,一个是nginx。 本文重点讲一下gateway。
核心思路就是header中携带机器标识,当负载均衡进行转发时,如果能匹配到对应的header就转发到当前服务器。
功能验证
项目搭建:
第一步肯定是搭建一个demo项目,验证方案的可行性。
关键包引入:
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <!-- 确保引入 Spring Cloud LoadBalancer --> <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency>
找到路由转发的处理类:
第一步定位当前服务谁在进行服务列表的筛选,我们去更改这个类做增强。
我们的项目是gateway项目,肯定首先去看GlobalFilter,gateway通过它的拦截器来实现地址转换,lb://xxx转换成http://xxxx.
查询GlobalFilter的实现类,发现一个带有关键字:ReactiveLoadBalancerClientFilter(loadBalancer=负载均衡),断点打在这儿,去看一下业务处理。里面有lb的转换,就是它了。
找到进行节点选择的处理类:
翻译一下源码或者顺着断点多点几次,发现一个choose方法,入参是serviceId返回是ServiceInstance, serviceInstance里面盛放的需要转发到的ip端口等关键信息。这个核心的功能是谁在实现呢?看到注入的类是ReactorLoadBalancer,看看里面有两个实现一个是随机数,一个是轮询。通常默认的是轮询。
自己写负载均衡类:
copy轮询的类(RoundRobinLoadBalancer)出来改一下名字,当做我们自己的类。里面逻辑很简单,通过AtomicInteger+1来趋于节点数进行获取。
我们更改为根据header的关键信息进行获取。核心代码如下。
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances, Request<RequestDataContext> request) {if (instances.isEmpty()) {if (log.isWarnEnabled()) {log.warn("No servers available for service: " + serviceId);}return new EmptyResponse();}if (instances.size() == 1) {return new DefaultResponse(instances.get(0));}// 新增根据header 进行路由选择--startList<String> targetServiceList = request.getContext().getClientRequest().getHeaders().get("X-Target-Server");if(targetServiceList!=null && targetServiceList.size()>0){Optional<ServiceInstance> first = instances.stream().filter(action -> targetServiceList.contains(String.format("%s:%s", action.getHost(), action.getPort()))).findFirst();if(first.isPresent()){return new DefaultResponse(first.get());}}// 新增根据header 进行路由选择--endint pos = this.position.incrementAndGet() & Integer.MAX_VALUE;ServiceInstance instance = instances.get(pos % instances.size());return new DefaultResponse(instance);}你说它的入参列表只有instances,没有header?这个是我们自己创建的类这些内部方法随便改出入参,从choose里面延续的传递下来就行。
我以为我把核心功能完成了,剩下的注入到容器让他生效就行了,没想到我还是太年轻了。
----------------------------------------------------华丽的分割线-----------------------------------------------------------
让自己写的负载均衡类生效
看一下框架里面的轮询类怎么生效的?哦,通过类(LoadBalancerClientConfiguration),注入进来的。而且他写的是@ConditionalOnMissingBean,见下图:

我们仿照它这段写一个,去掉@ConditionalOnMissingBean注解,写@Bean创建我们自己的类,让我们的类优先加载。
问题分析:
报错:空指针
ClientFactoryObjectProvider 对应的name为空,这个name从哪儿来的呢?我们自己写的类初始化的时候传递的。上面截图的name. name从哪儿来的呢?从环境变量里面获取的字段:loadbalancer.client.name,
先在配置文件写上这个字段验证一下,嗯,写的我测试的这个serviceId能正常访问。这个东西不应该是在配置文件写死的啊?我可能路由不通的项目,写死一个固定的servcieId肯定不行。试试别的项目呢?
嗯, 不出意外的果然不行,访问的时候找不到服务列表,servcieId传递的值错误了。一直传递我配置文件的servcieId。这样肯定是不行的,删除配置文件,回到了原点。
额,遇到难题了,我尝试网上搜索一下。照着这个样例写的也是不生效,待会再说异常原因。
以下截图是踩坑的错误例子,不要这样写,正确写法,看最后结论。

呼,平心静气看一下代码逻辑。
逻辑二次梳理
要实现的功能是轮询的负载均衡,根据什么进行轮询?根据服务标识(servcieId)进行轮询,类里面的全局变量定义了轮询的位置信息,所以每个servcieId应该是对应的是一个类。这样当这个servcieId进行下次进行访问的时候,才能在上次访问的基础上去访问下一个机器。所以我把自己写的策略类,直接扔到spring容器中是不对的。
线上验证, 去掉我的实现,走默认实现,查看spring容器中是否有注入默认的实现类(RoundRobinLoadBalancer)
断点分析
断点打到RoundRobinLoadBalancer的构造方法,看看是哪个地方触发的类创建?项目启动的时候没有进入断点,访问时进入断点,我们看看怎么触发过来的。找找为什么会调用这个类(LoadBalancerClientConfiguration)来创建对象(RoundRobinLoadBalancer),我们着重关心关键词LoadBalancerClientConfiguration,因为这个是构建这个bean的关键类。我们会看到factoryBeanName就是这个名称,用这个类当做工厂来创建轮询负载均衡的类。入口类是LoadBalancerClientFactory
分析一下工厂类
LoadBalancerClientFactory类在进行getInstances方法的时候触发的类加载,contexts很熟悉的上下文,还是一个本地的map,看一下这个属性所在的类(NamedContextFactory)。根据名称来创建的上下文工厂。和我们刚才的想法是一致的,根据不同的servcieId创建这个servcieId对应的上下文。LoadBalancerClientFactory被spring管理,我们找一下它的构建类。有了新的发现。

构建工厂类,根据配置项构建,断点打上看看配置项有没有对应的关键词:LoadBalancerClientConfiguration
下面一个setConfigurations信息,看看里面有没有关键词:LoadBalancerClientConfiguration
这个configurations就是我们要找的类。这个地方注入了类:LoadBalancerClientSpecification声明了这个工厂类加载的时候根据哪些类进行加载。
找一下类LoadBalancerClientSpecification加载
找啊找找到了我们之前错误截图的注解@LoadBalancerClients
这个能决定要根据那个配置进行加载。它能创建LoadBalancerClientSpecification。
我们指向谁?指向刚才写的配置加载类,但是配置加载类被spring管理,导致我们自定义的负载类被加载产生异常,
正确结论
拆分成为两个类。 一个被spring管理的类,添加注解@LoadBalancerClients指向另外一个普通类。
普通类内注入我们自定义的负载类,见下图。

