“不积跬步,定制化开发无以至千里。”
上文写到,定制化开发通过其内置的IRule组件,定制化开发使用指定的负载均衡算法(默认轮询)从ILoadBalancer组件的server list定制化开发中会拿到一个真正要发送请求的server地址,定制化开发那么接下来,定制化开发就会调用网络通信组件发起http请求了。
@Overridepublic <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException { Server server = null; if(serviceInstance instanceof RibbonServer) { server = ((RibbonServer)serviceInstance).getServer(); } if (server == null) { throw new IllegalStateException("No instances available for " + serviceId); } RibbonLoadBalancerContext context = this.clientFactory .getLoadBalancerContext(serviceId); RibbonStatsRecorder statsRecorder = new RibbonStatsRecorder(context, server); try { T returnVal = request.apply(serviceInstance); statsRecorder.recordStats(returnVal); return returnVal; } // catch IOException and rethrow so RestTemplate behaves correctly catch (IOException ex) { statsRecorder.recordStats(ex); throw ex; } catch (Exception ex) { statsRecorder.recordStats(ex); ReflectionUtils.rethrowRuntimeException(ex); } return null;}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
RibbonLoadBalancerContext context = this.clientFactory.getLoadBalancerContext(serviceId);
定制化开发之前提到过,ribbon定制化开发的设计理念,定制化开发每个下游被调服务,对应一个spring容器(ApplicationContext)
这行代码,定制化开发其实就是首先根据serviceId
定制化开发拿到这个服务对应的spring容器,再从其对应的spring容器中获取一个ribbon的上下文(RibbonLoadBalancerContext )这个bean
protected AnnotationConfigApplicationContext getContext(String name) { if (!this.contexts.containsKey(name)) { synchronized (this.contexts) { if (!this.contexts.containsKey(name)) { this.contexts.put(name, createContext(name)); } } } return this.contexts.get(name);}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>();
一个map中获取一个AnnotationConfigApplicationContext(spring容器),这个name就是服务名称,这个也验证的我们的结论
至于从容器中获取 RibbonLoadBalancerContext 这个bean的细节就不展开了,那个要追到spring的源码细节,不在这个专题的研究范围之内,不过spring作为一个十分优秀的开源框架,后面我会用极其庞大的一个专题来深入刨析一下,也敬请期待。
我们继续看execute的流程
T returnVal = request.apply(serviceInstance);
这是关键的一行代码,调用了一个request的apply方法,直接拿到了返回值,所以这个apply方法应该就是入口了
而这个request是通过参数传递的,LoadBalancerRequest
那么这个requet的方法,源头又是在哪里?
还记得吗?LoadBalancerInterceptor!!!
requestFactory.createRequest(request, body, execution)
就是这行代码创建了一个 LoadBalancerRequest
public LoadBalancerRequest<ClientHttpResponse> createRequest(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) { return instance -> { HttpRequest serviceRequest = new ServiceRequestWrapper(request, instance, loadBalancer); if (transformers != null) { for (LoadBalancerRequestTransformer transformer : transformers) { serviceRequest = transformer.transformRequest(serviceRequest, instance); } } return execution.execute(serviceRequest, body); };}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
这个lambda表达式就是apply方法,也就是说前面说的 request.apply(serviceInstance) 最终就会调用到这个lambda里来
HttpRequest serviceRequest = new ServiceRequestWrapper(request, instance, loadBalancer);
我告诉你,这行代码,是一个核心逻辑,它主要的工作是干嘛?
重写获取URI逻辑。
@Overridepublic URI getURI() { URI uri = this.loadBalancer.reconstructURI( this.instance, getRequest().getURI()); return uri;}
- 1
- 2
- 3
- 4
- 5
- 6
因为后面会调用ClientHttpRequestExecution
去发送http请求,拿到最终的响应,这个就是spring原生的http网络组件,这个组件里面一定会去调用getURI()
拿到一个请求地址
而你传进来的是类似: http://springboot-project-2/test_1/zhangopop
spring组件拿到这个地址是没办法处理的,它需要是ip,主机和port端口号
是类似这种:http://localhost:10001/test_1/zhangopop
所以就需要这个ServiceRequestWrapper 包装类,去包装原生的request,来重写getURI()方法,把服务名称,变成实际发送的ip和port
然后将这个ServiceRequestWrappter交给了ClientHttpRequestExecution
execution.execute(serviceRequest, body);
再往下的话呢,其实就是走到spring-web的源码里去了,其实就是说是spring-web下的负责底层的http请求的组件,从 ServiceRequestWrapper 中获取出来了对应的真正的请求URL地址,然后发起了一次请求
ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), method);
这就是 ClientHttpRequestExecution 的execute最终构建请求的一行代码
所以,奥秘,不在于说spring-web的底层的http的源码
可以来看看,ServiceRequestWrappter是怎么重写了getURI的方法
@Overridepublic URI reconstructURI(ServiceInstance instance, URI original) { Assert.notNull(instance, "instance can not be null"); String serviceId = instance.getServiceId(); RibbonLoadBalancerContext context = this.clientFactory .getLoadBalancerContext(serviceId); URI uri; Server server; if (instance instanceof RibbonServer) { RibbonServer ribbonServer = (RibbonServer) instance; server = ribbonServer.getServer(); uri = updateToSecureConnectionIfNeeded(original, ribbonServer); } else { server = new Server(instance.getScheme(), instance.getHost(), instance.getPort()); IClientConfig clientConfig = clientFactory.getClientConfig(serviceId); ServerIntrospector serverIntrospector = serverIntrospector(serviceId); uri = updateToSecureConnectionIfNeeded(original, clientConfig, serverIntrospector, server); } return context.reconstructURIWithServer(server, uri);}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
ServiceRequestWrapper的getURI()方法里面,调用了RibbonLoadBalancerClient的reconstructURI()
方法,基于选择出来的server的地址,重构了请求URI
这个参数ServiceInstance instance,就是之前负载均衡从server list中选择出来的一个server
context.reconstructURIWithServer(server, uri); 然后再这个方法里,完成host和port的替换
将http://springboot-project-2/test_1/zhangopop
替换为http://localhost:10001/test_1/zhangopop
就不进去细看了,一大坨琐碎的代码,StringBuilder操作,没什么营养,体力活
ok,总结一下,ribbon底层发送http使用的是spring原生的网络组件ClientHttpRequestExecution,只不过对原生的request进行了封装ServiceRequestWrapper,重写了getURI()方法,主要是把http uri中的服务名称替换为实际要发送的ip和port。
到此为止,ribbon这个客户端的负载均衡组件已经把核心源码讲完了,基本都是大白话的细致讲解,你不可能看不懂,把我这套看懂之后,然后自己画画图,出去面试,简历上写上深入研究过核心源码,基本上问题不大,可能有些面试官他自己也没看过几个源码,也问不出个啥,看源码的能力,核心竞争力,需要持续的培养,自己逼着自己看,前期会很痛苦,抓大放小,看不懂,过!把握核心流程,一些细节性的东西,需要用的时候再细看。
本人后续会刨析大量优秀开源框架核心源码,feign、hystrix、byteTcc、jta、spring、redisson等等,甚至大数据领域的hadoop、spark、es、zookeeper、flink、kafka等等,这些我都会在后面把核心的东西呈现出来,用大白话的形式让每个人看懂,源码路上,带你上道,反正我现在是停不下来了。
话不多说,下一个源码专题,让我们进入feign。