摘要:本文主要分析使用cse提供的RestTemplate的场景,其实cse提供的rpc注解(RpcReference)的方式最后的调用逻辑和RestTemplate是殊途同归的。

本文分享自华为云社区《我是一个请求,我该何去何从(下)》,原文作者:向昊 。

上次我们大概了解到了服务端是怎么处理请求的,那么发送请求又是个什么样的流程了?本文主要分析使用cse提供的RestTemplate的场景,其实cse提供的rpc注解(RpcReference)的方式最后的调用逻辑和RestTemplate是殊途同归的。

使用

使用cse提供的RestTemplate时候,是这样初始化的:

RestTemplate restTemplate = RestTemplateBuilder.create();restTemplate.getForObject("cse://appId:serviceName/xxx", Object.class);

我们可以注意到2个怪异的地方:

  • RestTemplate是通过RestTemplateBuilder.create()来获取的,而不是用的Spring里提供的。
  • 请求路径开头是cse而不是我们常见的http、https且需要加上服务所属的应用ID和服务名称。

解析

根据url匹配RestTemplate

首先看下RestTemplateBuilder.create(),它返回的是org.apache.servicecomb.provider.springmvc.reference.RestTemplateWrapper,是cse提供的一个包装类。

// org.apache.servicecomb.provider.springmvc.reference.RestTemplateWrapper
// 用于同时支持cse调用和非cse调用
class RestTemplateWrapper extends RestTemplate {private final List<AcceptableRestTemplate> acceptableRestTemplates = new ArrayList<>();final RestTemplate defaultRestTemplate = new RestTemplate();RestTemplateWrapper() {acceptableRestTemplates.add(new CseRestTemplate());}RestTemplate getRestTemplate(String url) {for (AcceptableRestTemplate template : acceptableRestTemplates) {if (template.isAcceptable(url)) {return template;}}return defaultRestTemplate;}
}

AcceptableRestTemplate:这个类是一个抽象类,也是继承RestTemplate的,目前其子类就是CseRestTemplate,我们也可以看到在初始化的时候会默认往acceptableRestTemplates中添加一个CseRestTemplate。

回到使用的地方restTemplate.getForObject:这个方法会委托给如下方法:

public <T> T getForObject(String url, Class<T> responseType, Object... urlVariables) throws RestClientException {return getRestTemplate(url).getForObject(url, responseType, urlVariables);
}

可以看到首先会调用getRestTemplate(url),即会调用template.isAcceptable(url),如果匹配到了就返回CseRestTemplate,否则就返回常规的RestTemplate。那么再看下isAcceptable()这个方法:

到这里我们就清楚了路径中的cse://的作用了,就是为了使用CseRestTemplate来发起请求,也理解了为啥RestTemplateWrapper可以同时支持cse调用和非cse调用。

委托调用

从上面可知,我们的cse调用其实都是委托给CseRestTemplate了。在构造CseRestTemplate的时候会初始化几个东西:

public CseRestTemplate() {setMessageConverters(Arrays.asList(new CseHttpMessageConverter()));setRequestFactory(new CseClientHttpRequestFactory());setUriTemplateHandler(new CseUriTemplateHandler());
}

这里需要重点关注new CseClientHttpRequestFactory():

public class CseClientHttpRequestFactory implements ClientHttpRequestFactory {@Overridepublic ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {return new CseClientHttpRequest(uri, httpMethod);}
}

最终委托到了CseClientHttpRequest这个类,这里就是重头戏了!

我们先把注意力拉回到这句话:restTemplate.getForObject("cse://appId:serviceName/xxx", Object.class),从上面我们知道其逻辑是先根据url找到对应的RestTemplate,然后调用getForObject这个方法,最终这个方法会调用到:org.springframework.web.client.RestTemplate#doExecute:

protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback,@Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {ClientHttpResponse response = null;try {ClientHttpRequest request = createRequest(url, method);if (requestCallback != null) {requestCallback.doWithRequest(request);}response = request.execute();handleResponse(url, method, response);return (responseExtractor != null ? responseExtractor.extractData(response) : null);}
}

createRequest(url, method):会调用getRequestFactory().createRequest(url, method),即最终会调用到我们初始化CseClientHttpRequest是塞的RequestFactory,所以这里会返回ClientHttpRequest这个类。

request.execute():这个方法会委托到org.apache.servicecomb.provider.springmvc.reference.CseClientHttpRequest#execute这个方法上。

至此我们知道前面的调用最终会委托到CseClientHttpRequest#execute这个方法上。

cse调用

接着上文分析:

public ClientHttpResponse execute() {path = findUriPath(uri);requestMeta = createRequestMeta(method.name(), uri);QueryStringDecoder queryStringDecoder = new QueryStringDecoder(uri.getRawSchemeSpecificPart());queryParams = queryStringDecoder.parameters();Object[] args = this.collectArguments();// 异常流程,直接抛异常出去return this.invoke(args);
}

createRequestMeta(method.name(), uri):这里主要是根据microserviceName去获取调用服务的信息,并会将获取的信息放入到Map中。服务信息如下:

可以看到里面的信息很丰富,例如应用名、服务名、还有接口对应的yaml信息等。

this.collectArguments():这里隐藏了一个校验点,就是会校验传入的参数是否符合对方接口的定义。主要是通过这个方法:org.apache.servicecomb.common.rest.codec.RestCodec#restToArgs,如果不符合真个流程就结束了。

准备invocation

从上面分析可知,获取到接口所需的参数后就会调用这个方法:org.apache.servicecomb.provider.springmvc.reference.CseClientHttpRequest#invoke:

private CseClientHttpResponse invoke(Object[] args) {Invocation invocation = prepareInvocation(args);Response response = doInvoke(invocation);if (response.isSuccessed()) {return new CseClientHttpResponse(response);}throw ExceptionFactory.convertConsumerException(response.getResult());
}

prepareInvocation(args):这个方法会准备好Invocation,这个Invocation在上集已经分析过了,不过上集中的它是为服务端服务的,那么咱们这块当然就得为消费端工作了

protected Invocation prepareInvocation(Object[] args) {Invocation invocation =InvocationFactory.forConsumer(requestMeta.getReferenceConfig(),requestMeta.getOperationMeta(),args);return invocation;
}

从名字也可以看出它是为消费端服务的,其实无论是forProvider还是forConsumer,它们最主要的区别就是加载的Handler不同,这次加载的Handler如下:

  • class org.apache.servicecomb.qps.ConsumerQpsFlowControlHandler(流控)
  • class org.apache.servicecomb.loadbalance.LoadbalanceHandler(负载)
  • class org.apache.servicecomb.bizkeeper.ConsumerBizkeeperHandler(容错)
  • class org.apache.servicecomb.core.handler.impl.TransportClientHandler(调用,默认加载的)

前面3个Handler可以参考下这个微服务治理专栏

doInvoke(invocation):初始化好了invocation后就开始调用了。最终会调用到这个方法上:org.apache.servicecomb.core.provider.consumer.InvokerUtils#innerSyncInvoke

至此,这些动作就是cse中RestTemplate和rpc调用的不同之处。不过可以清楚的看到RestTemplate的方式是只支持同步的,即innerSyncInvoke,但是rpc是可以支持异步的,即reactiveInvoke

public static Response innerSyncInvoke(Invocation invocation) {invocation.next(respExecutor::setResponse);
}

到这里我们知道了,消费端发起请求还是得靠invocation的责任链驱动

启动invocation责任链

好了,咱们的老朋友又出现了:invocation.next,这个方法是个典型的责任链模式,其链条就是上面说的那4个Handler。前面3个就不分析了,直接跳到TransportClientHandler。

// org.apache.servicecomb.core.handler.impl.TransportClientHandler
public void handle(Invocation invocation, AsyncResponse asyncResp) throws Exception {Transport transport = invocation.getTransport();transport.send(invocation, asyncResp);
}

invocation.getTransport():获取请求地址,即最终发送请求的时候还是以ip:port的形式。

transport.send(invocation, asyncResp):调用链为

org.apache.servicecomb.transport.rest.vertx.VertxRestTransport#send

  • ->org.apache.servicecomb.transport.rest.client.RestTransportClient#send(这里会初始化HttpClientWithContext,下面会分析)
  • ->org.apache.servicecomb.transport.rest.client.http.RestClientInvocation#invoke(真正发送请求的地方)
public void invoke(Invocation invocation, AsyncResponse asyncResp) throws Exception {createRequest(ipPort, path);clientRequest.putHeader(org.apache.servicecomb.core.Const.TARGET_MICROSERVICE, invocation.getMicroserviceName());RestClientRequestImpl restClientRequest =new RestClientRequestImpl(clientRequest, httpClientWithContext.context(), asyncResp, throwableHandler);invocation.getHandlerContext().put(RestConst.INVOCATION_HANDLER_REQUESTCLIENT, restClientRequest);Buffer requestBodyBuffer = restClientRequest.getBodyBuffer();HttpServletRequestEx requestEx = new VertxClientRequestToHttpServletRequest(clientRequest, requestBodyBuffer);invocation.getInvocationStageTrace().startClientFiltersRequest();// 触发filter.beforeSendRequest方法for (HttpClientFilter filter : httpClientFilters) {if (filter.enabled()) {filter.beforeSendRequest(invocation, requestEx);}}// 从业务线程转移到网络线程中去发送// httpClientWithContext.runOnContext
}

createRequest(ipPort, path):根据参数初始化HttpClientRequest clientRequest,初始化的时候会传入一个创建一个responseHandler,即对响应的处理。

注意org.apache.servicecomb.common.rest.filter.HttpClientFilter#afterReceiveResponse的调用就是在这里埋下伏笔的,是通过回调org.apache.servicecomb.transport.rest.client.http.RestClientInvocation#processResponseBody这个方法触发的(在创建responseHandler时候创建的)

且org.apache.servicecomb.common.rest.filter.HttpClientFilter#beforeSendRequest:这个方法的触发我们也可以很清楚的看到在发送请求执行的。

requestEx:注意它的类型是HttpServletRequestEx,虽然名字里面带有Servlet,但是打开它的方法可以发现有很多我们在tomcat中那些常用的方法都直接抛出异常了,这也是一个易错点!

httpClientWithContext.runOnContext:用来发送请求的逻辑,不过这里还是有点绕的,下面重点分析下

httpClientWithContext.runOnContext

首先看下HttpClientWithContext的定义:

public class HttpClientWithContext {public interface RunHandler {void run(HttpClient httpClient);}private HttpClient httpClient;private Context context;public HttpClientWithContext(HttpClient httpClient, Context context) {this.httpClient = httpClient;this.context = context;}public void runOnContext(RunHandler handler) {context.runOnContext((v) -> {handler.run(httpClient);});}
}

从上面可知发送请求调用的是这个方法:runOnContext,参数为RunHandler接口,然后是以lambda的方式传入的,lambda的参数为httpClient,这个httpClient又是在HttpClientWithContext的构造函数中初始化的。这个构造函数是在org.apache.servicecomb.transport.rest.client.RestTransportClient#send这个方法中初始化的(调用org.apache.servicecomb.transport.rest.client.RestTransportClient#findHttpClientPool这个方法)。

但是我们观察调用的地方:

// 从业务线程转移到网络线程中去发送
httpClientWithContext.runOnContext(httpClient -> {clientRequest.setTimeout(operationMeta.getConfig().getMsRequestTimeout());processServiceCombHeaders(invocation, operationMeta);try {restClientRequest.end();} catch (Throwable e) {LOGGER.error(invocation.getMarker(),"send http request failed, local:{}, remote: {}.", getLocalAddress(), ipPort, e);fail((ConnectionBase) clientRequest.connection(), e);}
});

其实在这块逻辑中HttpClient是没有被用到的,实际上发送请求的动作是restClientRequest.end()触发的,restClientRequest是cse中的类RestClientRequestImpl,然后它包装了HttpClientRequest(vertx中提供的),即restClientRequest.end()最终还是委托到了HttpClientRequest.end()上了。

那么这个HttpClientRequest是怎么被初始化的了?它是在createRequest(ipPort, path)这个方法中初始化的,即在调用org.apache.servicecomb.transport.rest.client.http.RestClientInvocation#invoke方法入口处。

初始化的逻辑如下:

clientRequest = httpClientWithContext.getHttpClient().request(method,
requestOptions, this::handleResponse)

httpClientWithContext.getHttpClient():这个方法返回的是HttpClient,上面说的HttpClient作用就体现出来了,用来初始化了我们发送请求的关键先生:HttpClientRequest。那么至此我们发送请求的整体逻辑大概就清晰了。

总结

无论是采用RestTemplate的方式还是采用rpc注解的方式来发送请求,其底层逻辑其实是一样的。即首先根据请求信息匹配到对方的服务信息,然后经过一些列的Handler处理,如限流、负载、容错等等(这也是一个很好的扩展机制),最终会走到TransportClientHandler这个Handler,然后根据条件去初始化发送的request,经过HttpClientFilter的处理后就会委托给vertx的HttpClientRequest来真正的发出请求。

点击关注,第一时间了解华为云新鲜技术~

我是一个请求,我是如何被发送的?相关推荐

  1. 我是一个请求,我该何去何从

    摘要:本文主要分析在cse框架下一个请求是怎么被接受和处理的. 本文分享自华为云社区<我是一个请求,我该何去何从?>,原文作者:向昊. 前置知识 cse的通信是基于vert.x来搞的,所以 ...

  2. 一个请求结束之后再发送另外一个请求,需要连着发很多请求的方法-promise

    之前用vue写了一个表格组件,一个页面最多会循环出来20个表格,每个表格都有会发请求,差不多20个请求一起发出,数据库不干了,所以就想在一个表格请求完再请求下一个表格. 这就用了传说中的promise ...

  3. 我是一个秒杀请求,正在逃离这颗星球...

    作者 | 悟空聊架构 来源 | 悟空聊架构(ID:PassJava666) 星球简介 地点:β-410 星系,A-731电商星球. 时间:新纪元 2036 年. 星球简介: 中文名:A-731电商星球 ...

  4. 我是一个流氓软件线程

    前情回顾: 我是一个explorer的线程 我是一个杀毒软件线程 我是一个IE浏览器线程 Hello, World! 我是一个流氓软件线程,我不像那些病毒和木马,我只是通过人类的电脑赚一点小钱,并不会 ...

  5. 我是一个Dubbo数据包...

    Hollis的新书限时折扣中,一本深入讲解Java基础的干货笔记! 今天给大家带来一篇关于Dubbo IO交互的文章,本文是一位同事所写,用有趣的文字把枯燥的知识点写出来,通俗易懂,非常有意思,所以迫 ...

  6. 线程可以kill吗_我是一个线程(转)

    我是一个线程,我一出生就被编了个号:0x3704,然后被领到一个昏暗的屋子里,在这里我发现了很多和我一模一样的同伴. 我身边的同伴0x6900 待的时间比较长,他带着沧桑的口气对我说:"我们 ...

  7. 我是一个线程 [转]

    我是一个线程,我一出生就被编了个号:0x3704,然后被领到一个昏暗的屋子里,在这里我发现了很多和我一模一样的同伴. 我身边的同伴0x6900 待的时间比较长,他带着沧桑的口气对我说:"我们 ...

  8. 【转载】我是一个线程(修订版)

    原文:我是一个线程(修订版) 第一回 初生牛犊 我是一个线程,我一出生就被编了个号:0x3704,然后被领到一个昏暗的屋子里,在这里我发现了很多和我一模一样的同伴. 我身边的同伴0x6900 待的时间 ...

  9. 我是一个CPU:这个世界慢!死!了!

    女主宣言 最近小编看到一篇十分有意思的文章,多方位.无死角的讲解了CPU关于处理速度的理解,看完之后真是豁然开朗.IOT时代,随着科技的发展CPU芯片的处理能力越来越强,强大的程度已经超乎了我们的想象 ...

最新文章

  1. spring mvc 关键接口 HandlerMapping HandlerAdapter
  2. 怎么解决相位抵消_两种相位抵消法消侧音原理及其电路形式
  3. 【3006】统计数字
  4. oracle易忘函数用法(6)
  5. php redis 投票_高性能Redis服务架构分析与搭建
  6. 记一次mysql故障恢复
  7. FireReport 层次坐标 使用问题A1[A1:-1]=A1
  8. netty 异常 did not read anything but decoded a message
  9. C#生成GS1码制二维码
  10. INT 10H显示字符串的小例子
  11. gtj2018如何生成工程量报表_工程量清单计价规范2018
  12. python中卡方检验_python 使用卡方检验
  13. java:输入小写字母转为大写字母
  14. C++ Primer Plus课后编程练习第6章参考代码
  15. 如何批量将图片转换成jpg格式?
  16. transforms.Compose,transforms.ToTensor(),transforms.Normalize()的含义与原理
  17. 17家IT创业公司的血泪史(2)
  18. PMP笔记-项目干系人管理要点总结
  19. 5-3 FacetGrid与调色盘
  20. php过滤微信昵称的emoji表情符号,emoji表情过滤处理

热门文章

  1. zrender zlevel层叠控制和Group使用笔记
  2. MyBatis 实例
  3. Bootstrap3 带条纹的表格样式
  4. 四元数与复数之间的关系
  5. java ftp 假死_FTPClient下载文件,程序假死问题
  6. T研究:国内云BPM市场规模尚小,预计2018年仅为3.29亿元
  7. 安卓 dex 通用脱壳技术研究(四)
  8. tab栏的切换【DOM点击事件】
  9. C++中字符数组和字符串string
  10. 自定义的plot函数参数date坐标模型[x,y]的使用建议