Ribbon源码深度刨析-(5)ServiceRequestWrapper

本文解析了Ribbon如何通过ServiceRequestWrapper重写getURI,将服务名转换为实际的IP和端口,实现负载均衡过程中的URI重构。核心在于Spring原生网络组件和Ribbon封装的底层逻辑。
摘要由CSDN通过智能技术生成

“不积跬步,无以至千里。”

上文写到,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

request

spring组件拿到这个地址是没办法处理的,它需要是ip,主机和port端口号

是类似这种:http://localhost:10001/test_1/zhangopop

所以就需要这个ServiceRequestWrapper 包装类,去包装原生的request,来重写getURI()方法,把服务名称,变成实际发送的ip和port

requestWrapper

然后将这个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操作,没什么营养,体力活

context.reconstructURIWithServer

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。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值