微服务系列(六)探究Spring Cloud服务调用

微服务系列(六)探究Spring Cloud服务调用

大纲

  1. 分布式架构的“骨架”
  2. 基于http协议的通讯模式
  3. RestTemplate与Feign
  4. 新的选择WebClient
  5. 从Spring源码中我看到了什么

分布式架构的“骨架”

分布式架构是由一个个组件组装而成,各司其职,对外提供服务。而将他们联系起来的则是网络,组件之间的通讯方式则是这个分布式架构的“骨架”。

如果没有“骨架”,每个组件只有做自己的事,不能联系起来,也就不能互相配合并提供完整的服务。

对于一个大型的分布式系统中,如果能使用一个统一的通讯方式来作为“骨架”,并且这个通讯方式能满足系统的所有需求,那么这个“骨架”就是完美的。

而Spring Cloud选择了HTTP。

我认为主要原因有以下几点:

  1. HTTP是web常用协议,易学,易使用
  2. HTTP很灵活,支持短连接、长连接,几乎能满足大部分需求
  3. HTTP的通讯代价较小,能满足常见系统的性能需求

当然,可能对于某些系统,HTTP是没办法满足它的需求,例如:需要实现推送效果、性能要求高、可靠性高、一致性要求高。

对于这样的系统,就需要选择一个更适合自己的通讯方式,或是用自定义通讯协议来把各个组件串联起来,这样做的代价是很大的,好处也是明显的,某产则是用了这样的方式,并且在外层做了一个协议转换的组件,让系统可以灵活的对接外部不同协议,并在内部用私有自定义协议进行交互。

对于普通需求,我们选择Spring Cloud给我们提供的最简单的方式来做就完事了。

基于http协议的通讯模式

这里列举Java中常见的几个HTTP客户端:

  • OKHttp
  • HttpClient
  • 原生HttpURLConnection

Spring Cloud选择了HTTP,而在Spring就实现了一个屏蔽底层HTTP客户端的Spring http客户端框架,在Spring Cloud体系中,则是用Spring Cloud Feign实现了更高级的功能,不再像以前一样通过自己构造对象、注入、使用api来调用,使用了注解的方式来替代。

RestTemplate与Feign

Spring的RestTemplate和Spring Cloud的Feign是怎样实现的呢

源码分析的部分会屏蔽一些细节的描述,主要了解它的设计思想和值得我们学习的实现

进入org.springframework.web.client.RestTemplate

大致了解下它的API:

  • GET请求:getForObject、getForEntity
  • POST请求:postForLocation、postForObject、postForEntity
  • PUT请求:put
  • DELETE请求:delete
  • OPTIONS请求:optionsForAllow
  • HEAD请求:headForHeaders
  • PATCH请求:patchForObject
  • 通用请求入口:exchange、execute

从代码上看,前7个类型的请求以及exchange最终都是调用了execute,所以我们只需要弄清楚execute是如何实现的。

    // general execution
    
    @Override
    @Nullable
    public <T> T execute(String url, HttpMethod method, @Nullable RequestCallback requestCallback,
    		@Nullable ResponseExtractor<T> responseExtractor, Object... uriVariables) throws RestClientException {
    
    	URI expanded = getUriTemplateHandler().expand(url, uriVariables);
    	return doExecute(expanded, method, requestCallback, responseExtractor);
    }
    
    @Override
    @Nullable
    public <T> T execute(String url, HttpMethod method, @Nullable RequestCallback requestCallback,
    		@Nullable ResponseExtractor<T> responseExtractor, Map<String, ?> uriVariables)
    		throws RestClientException {
    
    	URI expanded = getUriTemplateHandler().expand(url, uriVariables);
    	return doExecute(expanded, method, requestCallback, responseExtractor);
    }
    
    @Override
    @Nullable
    public <T> T execute(URI url, HttpMethod method, @Nullable RequestCallback requestCallback,
    		@Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {
    
    	return doExecute(url, method, requestCallback, responseExtractor);
    }
    
    /**
     * Execute the given method on the provided URI.
     * <p>The {@link ClientHttpRequest} is processed using the {@link RequestCallback};
     * the response with the {@link ResponseExtractor}.
     * @param url the fully-expanded URL to connect to
     * @param method the HTTP method to execute (GET, POST, etc.)
     * @param requestCallback object that prepares the request (can be {@code null})
     * @param responseExtractor object that extracts the return value from the response (can be {@code null})
     * @return an arbitrary object, as returned by the {@link ResponseExtractor}
     */
    @Nullable
    protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback,
    		@Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {
    
    	Assert.notNull(url, "URI is required");
    	Assert.notNull(method, "HttpMethod is required");
    	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);
    	}
    	catch (IOException ex) {
    		String resource = url.toString();
    		String query = url.getRawQuery();
    		resource = (query != null ? resource.substring(0, resource.indexOf('?')) : resource);
    		throw new ResourceAccessException("I/O error on " + method.name() +
    				" request for \"" + resource + "\": " + ex.getMessage(), ex);
    	}
    	finally {
    		if (response != null) {
    			response.close();
    		}
    	}
    }

分解org.springframework.web.client.RestTemplate#doExecute方法,做了以下几件事:

  1. 构造请求对象
    ClientHttpRequest ClientHttpRequest request = createRequest(url, method);
  2. 处理ClientHttpRequest对象(这里是将参数request对象中的信息筛选并拷贝到ClientHttpRequest中,做好请求的准备)
       if (requestCallback != null) {
       	requestCallback.doWithRequest(request);
       }

ps.这里看起来还可以做很多事情,可以交给开发者来扩展,并通过execute来调用

    public <T> T execute(String url, HttpMethod method, @Nullable RequestCallback requestCallback,
          @Nullable ResponseExtractor<T> responseExtractor, Object... uriVariables){
        ..
    }
  1. 执行请求,并拿到响应对象
    response = request.execute();

  2. 处理响应对象(这里主要是判断是否需要处理异常,如果有异常,则交给ResponseErrorHandler处理)
    handleResponse(url, method, response);

    ps.这里可以发现,errorHandler是可以自定义的,也是一个可扩展的地方

    public void setErrorHandler(ResponseErrorHandler errorHandler) {
       Assert.notNull(errorHandler, "ResponseErrorHandler must not be null");
       this.errorHandler = errorHandler;
    }

大致的处理流程可以看出来了,下面继续思考步骤3.执行请求的逻辑:

这里先讲结果,至于为什么会这样,后面再分析。

默认配置下,这部分的请求最终会执行到org.springframework.http.client.SimpleBufferingClientHttpRequest#executeInternal

内部逻辑就不仔细探究了,简单说就是这部分的逻辑是用原生HttpUrlConnection发起http请求。

所以问题是,它是根据什么来选择发起哪种类型的http请求?

回到步骤2.ClientHttpRequest request = createRequest(url, method);

`ClientHttpRequest request = getRequestFactory().createRequest(url, method);`

而getRequestFactory()方法来自于RestTemplate所继承的抽象父类HttpAccessor

    public ClientHttpRequestFactory getRequestFactory() {
       return this.requestFactory;
    }

默认配置下,会被赋值SimpleClientHttpRequestFactory

    private ClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();

到这里会发现一个有意思的设计,RestTemplate继承自org.springframework.http.client.support.InterceptingHttpAccessor,而InterceptingHttpAccessor继承自org.springframework.http.client.support.HttpAccessor

实际上,它是利用了模板和装饰器的设计模式为http客户端增加了拦截器处理的能力。

    @Override
    public ClientHttpRequestFactory getRequestFactory() {
       List<ClientHttpRequestInterceptor> interceptors = getInterceptors();
       if (!CollectionUtils.isEmpty(interceptors)) {
          ClientHttpRequestFactory factory = this.interceptingRequestFactory;
          if (factory == null) {
             factory = new InterceptingClientHttpRequestFactory(super.getRequestFactory(), interceptors);
             this.interceptingRequestFactory = factory;
          }
          return factory;
       }
       else {
          return super.getRequestFactory();
       }
    }

包装后的execute方法

org.springframework.http.client.InterceptingClientHttpRequest.InterceptingRequestExecution#execute

    @Override
    public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
       if (this.iterator.hasNext()) {
          ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();
          return nextInterceptor.intercept(request, body, this);
       }
       else {
          HttpMethod method = request.getMethod();
          Assert.state(method != null, "No standard HTTP method");
          ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), method);
          request.getHeaders().forEach((key, value) -> delegate.getHeaders().addAll(key, value));
          if (body.length > 0) {
             if (delegate instanceof StreamingHttpOutputMessage) {
                StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) delegate;
                streamingOutputMessage.setBody(outputStream -> StreamUtils.copy(body, outputStream));
             }
             else {
                StreamUtils.copy(body, delegate.getBody());
             }
          }
          return delegate.execute();
       }
    }

所以整理一下request.execute()的处理逻辑:

由于request是由requestFactory生产的对象

而requestFactory是一个包裹起来的对象

外层是InterceptingClientHttpRequestFactory

里层是SimpleClientHttpRequestFactory

最终生产的request对象是InterceptingClientHttpRequest(SimpleClientHttpRequest())

Note.当然这里是以默认配置为例,Spring还提供了mock功能,也可以将SimpleClientHttpRequest切换成OKHttp、NettyClient、HttpComponentsClient等其他HTTP客户端请求的方式。

到这里RestTemplate的基本设计模式都看完了,最后简单介绍下其内部的几个重要的对象:

    private final List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
    
    private ResponseErrorHandler errorHandler = new DefaultResponseErrorHandler();
    
    private UriTemplateHandler uriTemplateHandler = new DefaultUriBuilderFactory();
    
    private final ResponseExtractor<HttpHeaders> headersExtractor = new HeadersExtractor();

HttpMessageConverter:上层接口,用于读取流中的数据并转化成相应的对象或将相应的对象写入到流中,Spring提供了丰富的实现,包括gson/jackson/xml等解析

ResponseErrorHandler:上层接口,用于处理HTTP请求拿到的响应异常信息,有默认实现,也可以自定义。

UriTemplateHandler:上层接口,用于restTemplate调用时通过params传参时进行的URI拼接,有默认实现,也可以自定义。

ResponseExtractor:上层接口,用于提取响应体,通过统一接口屏蔽了head类型的请求类型、T返回体、包装类ResponseEntiry<T.>的解析过程,分别使用了org.springframework.web.client.RestTemplate.HeadersExtractororg.springframework.web.client.HttpMessageConverterExtractororg.springframework.web.client.RestTemplate.ResponseEntityResponseExtractor#ResponseEntityResponseExtractor(org.springframework.web.client.HttpMessageConverterExtractor)

=============== 分界线

可以看到RestTemplate的实现比较简单,最主要是学习它的设计方式,相比之下,feign的实现相对就复杂多了。

首先,我们知道Feign的使用:

  1. @FeignClient/@RequestMapping定义一个提供Feign调用的接口
  2. @EnableFeignClients

原理是@EnableFeignClients import了org.springframework.cloud.openfeign.FeignClientsRegistrar

并由它进行了Feign的beanDefinition注入

    BeanDefinitionBuilder definition = BeanDefinitionBuilder
          .genericBeanDefinition(FeignClientFactoryBean.class);

最终Spring容器中的Feign bean是一个代理类,内部持有一个HardCodedTarget对象。

    @Override
    public Object getObject() throws Exception {
       return getTarget();
    }
    
    /**
     * @param <T> the target type of the Feign client
     * @return a {@link Feign} client created with the specified data and the context information
     */
    <T> T getTarget() {
       FeignContext context = applicationContext.getBean(FeignContext.class);
       Feign.Builder builder = feign(context);
    
       if (!StringUtils.hasText(this.url)) {
          String url;
          if (!this.name.startsWith("http")) {
             url = "http://" + this.name;
          }
          else {
             url = this.name;
          }
          url += cleanPath();
          return (T) loadBalance(builder, context, new HardCodedTarget<>(this.type,
                this.name, url));
       }
       if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
          this.url = "http://" + this.url;
       }
       String url = this.url + cleanPath();
       Client client = getOptional(context, Client.class);
       if (client != null) {
          if (client instanceof LoadBalancerFeignClient) {
             // not load balancing because we have a url,
             // but ribbon is on the classpath, so unwrap
             client = ((LoadBalancerFeignClient)client).getDelegate();
          }
          builder.client(client);
       }
       Targeter targeter = get(context, Targeter.class);
       return (T) targeter.target(this, builder, context, new HardCodedTarget<>(
             this.type, this.name, url));
    }

那么当我们使用@FeignClient接口来进行feign调用时,代码逻辑如下(由于代码结构比较复杂,省略代码分析的部分):

  1. 根据Method找到一个对应的feign.InvocationHandlerFactory.MethodHandler执行

  2. 如果Method不是default方法、object原生方法,则MethodHandler类型为feign.SynchronousMethodHandler#SynchronousMethodHandler

  3. 执行时,根据参数构造执行模板(包括url拼接信息、body信息等),然后交给retryer重试执行

  4. 执行内部逻辑包括增加拦截器处理、日志记录、响应消息的解码、异常处理,而真正的执行逻辑由feign.Client#execute执行

  5. 这里又会有default和loadbalance两种实现,简单说,default则是发起了http的请求并获取了响应对象,loadbalance则是在外层增加了ribbon负载均衡(替换uri),并且增加了多种http客户端的支持(包括OKHttp、HttpComponentsClient等)。

这其中有一些值得思考的地方:

  1. 为什么要使用代理对象,并且如何根据Method找到合适的handler?
  2. 在构造代理对象时,是通过一个builder串联起来的,这样的好处是什么?
  3. FeignContext是feign特有的上下文,每次build的过程都会从FeignContext取配置类、encoder、client,而不是直接从Spring上下文取,为什么要这样做?
  • 对于问题1.由于@FeignClient标志的接口本身是没有实现类的,所以必须构造代理对象,并且为了适应不同种类的调用方式,就需要使用动态代理技术。另外,为了隔离每个方法,Feign使用map存储Method到handler的映射关系,并且在代理对象中将请求转发到对应的handler处理,这样就达到了方法间完全隔离的效果,对于每个方法的调用要做变更或加强时,只需要针对每个handler做相应的处理即可。
  • 而问题2.中的builder,是Feign bean构造过程中的核心对象,首先从FeignContext获取默认配置并构造builder,然后根据FeignContext中的Client对象、Targeter对象,一步步串联组装起来,client、targeter各司其职,client负责http请求的执行,targeter则是负责代理对象的构造,这样一来,我们要想对feign进行扩展,如,想改变http请求的执行过程,则可以通过实现Client接口;想改变代理对象的构造过程,则可以通过实现targeter接口即可。(feign-hystrix的整合仅实现了Feign.Builder接口以及实现了熔断效果的feign.hystrix.HystrixInvocationHandler)
  • 这里的FeignContext实现,简单说则是包裹了Spring容器上下文,并且在内部将不同的Feign bean的配置隔离了,Feign bean在配置过程中根据@FeignClient(name=)中的name属性隔离不同的配置,用户在使用的过程中,可以通过configuration增加特定的配置。这样就通过一个FeignContext让每个Feign bean拥有独立的上下文配置,允许灵活的控制每个Feign bean,甚至允许同一Spring容器中的不同的Feign bean使用不同的http客户端。

新的选择WebClient?

WebClient是webflux包中的类,是Spring5.0才推出的一个新的HttpClient,在我看来,它的功能完全囊括了RestTemplate并且新增了异步请求功能,api则是完全的链式风格,写起来非常舒服。

Spring 官网是这样介绍它的:

Spring WebFlux includes a reactive, non-blocking WebClient for HTTP requests. The client has a functional, fluent API with reactive types for declarative composition, see Reactive Libraries. WebFlux client and server rely on the same non-blocking codecs to encode and decode request and response content.

Internally WebClient delegates to an HTTP client library. By default, it uses Reactor Netty, there is built-in support for the Jetty reactive HttpClient, and others can be plugged in through a ClientHttpConnector.

它与RestTemplate不同的主要点在于,它是非阻塞、响应式的,且拥有流式api,而它的内置HTTP client默认使用Reactor Netty。

源码的部分不做分析了,下面用几个使用WebClient的小例子,感受一下它和RestTemplate的区别,以及简单对比下与RestTemplate的性能差异。

定义一个服务端接口,用于被调用:

    /**
     * server端接口
     * @param sleep
     * @return
     * @throws InterruptedException
     */
    @GetMapping("/user")
    public ResponseEntity<User> user(@RequestParam("sleep") long sleep) throws InterruptedException {
        /**
         * 模拟请求耗时
         */
        Thread.sleep(sleep);
        return ResponseEntity.ok(new User());
    }

在同一服务编写测试程序:

    private static final String URL = "http://localhost:9090/user";
    private static final String SLEEP_KEY = "sleep";
    private static final long SLEEP = 100;
    private static final String GET_URL = URL + "?" + SLEEP_KEY + "=" + SLEEP;
    
    private static final String ERROR_MESSAGE = "client request failed.";
    private static final RuntimeException ERROR = new RuntimeException(ERROR_MESSAGE);
    
    public static User execute(List<Long> metric , FunctionX<User> fx) throws InterruptedException {
            long start = System.currentTimeMillis();
            User returnVal = fx.apply();
            long end = System.currentTimeMillis();
            metric.add(end - start);
            return returnVal;
    }
    
    @GetMapping("/rt/test")
    public List<Long> test() throws InterruptedException {
        execute(metric, ()->{
            for(int i = 0; i < 10; i++){
                ResponseEntity<User> response = restTemplate.exchange(GET_URL, HttpMethod.GET, null, User.class);
                if(response.getStatusCode() == HttpStatus.OK){
                    //ignore
                }else{
                    throw ERROR;
                }
            }
            return null;
        });
        return metric;
    }
    
    @GetMapping("/rt/get")
    public User get() throws InterruptedException {
                return execute(metric, ()->{
                    ResponseEntity<User> response = restTemplate.exchange(GET_URL, HttpMethod.GET, null, User.class);
                    if(response.getStatusCode() == HttpStatus.OK){
                        return response.getBody();
                    }else{
                        throw ERROR;
                    }
                });
    }

单线程分别发起十次请求(server端请求耗时100ms):

RestTemplate

[343,117,107,112,107,106,116,112,105,108]

WebClient

[896,118,114,111,107,116,107,115,111,110]

可以看到,第一次请求的耗时相对较长,推测是进行了一些初始化的操作,而后面的处理耗时差异很小,WebClient稍差于RestTemplate。

为了对比出异步能带来某些场景下的性能优势,对于每次请求,都让RestTempate循环请求十次,并记录耗时;对于每次请求,让WebClient发起十个异步请求,当异步请求均成功后认为请求完成,并记录耗时。

    @GetMapping("/rt/test")
    public List<Long> test() throws InterruptedException {
        execute(metric, ()->{
            for(int i = 0; i < 10; i++){
                ResponseEntity<User> response = restTemplate.exchange(GET_URL, HttpMethod.GET, null, User.class);
                if(response.getStatusCode() == HttpStatus.OK){
                    //ignore
                }else{
                    throw ERROR;
                }
            }
            return null;
        });
        return metric;
    }
    
    @GetMapping("/wc/test")
    public List<Long> test() throws InterruptedException {
                CountDownLatch latch = new CountDownLatch(10);
                List<User> users = new ArrayList<>();
                execute(metric, ()->{
                    for(int i = 0; i < 10; i++){
                        WebClient.create(GET_URL)
                                .method(HttpMethod.GET)//.get()
                                .retrieve()
                                .bodyToMono(User.class)
                                .toFuture()
                                .whenComplete((u, t) -> {
                                    users.add(u);
                                    latch.countDown();
                                });
                    }
                    latch.await();
                    return null;
                });
                return metric;
    }

测试10次,观察WebClient能否带来较明显的性能优势(ps.为了证明WebClient并不是一无是处的!):

RestTemplate

[1293,1066,1049,1051,1050,1050,1049,1045,1040,1039]

WebClient

[1306,138,133,131,139,146,123,130,118,119]

所以,对于某些场景,由于异步的支持,是可以提升性能的。

当然,我们可以自己增加一个线程池来让RestTemplate异步处理,这样做也能达到相同的性能提升,但增加的代码和复杂度是很明显的。

首先WebClient是webflux包的,这也是很多项目都接触不到它的原因之一,从使用感受上说,WebClient明显写起来更舒服,用简洁代码描述复杂逻辑,最重要的是它支持异步,虽然也因此略微影响到了性能,但实际上对于大部分场景都是不影响的。总的来说,WebClient功能更强大,之所以还没有取代RestTemplate,是因为webflux包还没有被广泛使用。从文档上看,Spring是极力推崇reactive的,事实是,异步虽好,但也难控,用的好是一把利剑,用的不好会造成性能瓶颈。

从Spring源码中我看到了什么

综合前面的几章的分析,列举我从Spring源码中悟到的几个道理或是经验:

  1. 想熟读Spring源码,必须学会这几个关键字wrapper、delegate、factory、proxy、interceptor、builder、template,另外对于handler、converter、extractor、parser、retryer这几个词也要能区分其含义
  2. Spring作为框架,只是粘合各种功能强大的“工具”,所以学习Spring源码,更多的是教会我们如何实现灵活、易扩展、可增强的抽象层,而不是如何提升底层工具的性能
  3. Spring源码看似是把简单问题复杂化了,其实是对高内聚的代码的分解,作为开发人员,我们应该学会这样的思想
  4. Spring非常喜欢reactive(肯定是!至少我从文档上的理解是这样的)
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值