“不积跬步,无以至千里。”
上文写到,ribbon通过其内置的IRule组件,使用指定的负载均衡算法(默认轮询)从ILoadBalancer组件的server list中会拿到一个真正要发送请求的server地址,那么接下来,就会调用网络通信组件发起http请求了。
@Override
public <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;
}
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);
}
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);
};
}
这个lambda表达式就是apply方法,也就是说前面说的 request.apply(serviceInstance) 最终就会调用到这个lambda里来
HttpRequest serviceRequest = new ServiceRequestWrapper(request, instance, loadBalancer);
我告诉你,这行代码,是一个核心逻辑,它主要的工作是干嘛?
重写获取URI逻辑。
@Override
public URI getURI() {
URI uri = this.loadBalancer.reconstructURI(
this.instance, getRequest().getURI());
return uri;
}
因为后面会调用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的方法
@Override
public 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);
}
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。