SpringCloud 源码系列(15)— 服务调用Feign 之 结合Ribbon进行负载均衡请求

1,421 阅读7分钟

专栏系列文章:SpringCloud系列专栏

系列文章:

SpringCloud 源码系列(1)— 注册中心Eureka 之 启动初始化

SpringCloud 源码系列(2)— 注册中心Eureka 之 服务注册、续约

SpringCloud 源码系列(3)— 注册中心Eureka 之 抓取注册表

SpringCloud 源码系列(4)— 注册中心Eureka 之 服务下线、故障、自我保护机制

SpringCloud 源码系列(5)— 注册中心Eureka 之 EurekaServer集群

SpringCloud 源码系列(6)— 注册中心Eureka 之 总结篇

SpringCloud 源码系列(7)— 负载均衡Ribbon 之 RestTemplate

SpringCloud 源码系列(8)— 负载均衡Ribbon 之 核心原理

SpringCloud 源码系列(9)— 负载均衡Ribbon 之 核心组件与配置

SpringCloud 源码系列(10)— 负载均衡Ribbon 之 HTTP客户端组件

SpringCloud 源码系列(11)— 负载均衡Ribbon 之 重试与总结篇

SpringCloud 源码系列(12)— 服务调用Feign 之 基础使用篇

SpringCloud 源码系列(13)— 服务调用Feign 之 扫描@FeignClient注解接口

SpringCloud 源码系列(14)— 服务调用Feign 之 构建@FeignClient接口动态代理

前一篇文章已经分析出,最终在 Feign.Builderbuild() 方法构建了 ReflectiveFeign,然后利用 ReflectiveFeign 的 newInstance 方法创建了动态代理。这个动态代理的代理对象是 ReflectiveFeign.FeignInvocationHandler。最终来说肯定就会利用 Client 进行负载均衡的请求。这节就来看看 Feign 如果利用动态代理发起HTTP请求的。

FeignClient 动态代理请求

使用 FeignClient 接口时,注入的其实是动态代理对象,调用接口方法时就会进入执行器 ReflectiveFeign.FeignInvocationHandler,从 FeignInvocationHandler 的 invoke 方法可以看出,就是根据 method 获取要执行的方法处理器 MethodHandler,然后执行方法。MethodHandler 的实际类型就是 SynchronousMethodHandler

static class FeignInvocationHandler implements InvocationHandler {
    private final Target target;
    private final Map<Method, MethodHandler> dispatch;

    FeignInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) {
      this.target = checkNotNull(target, "target");
      this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //...
        // 根据 method 获取 MethodHandler,然后执行方法
        return dispatch.get(method).invoke(args);
    }
}

接着看 SynchronousMethodHandler 的 invoke 方法,核心逻辑就两步:

  • 先根据请求参数构建请求模板 RequestTemplate,就是处理 URI 模板、参数,比如替换掉 uri 中的占位符、拼接参数等。
  • 然后调用了 executeAndDecode 执行请求,并将相应结果解码返回。
public Object invoke(Object[] argv) throws Throwable {
    // 构建请求模板,例如有 url 参数,请求参数之类的
    RequestTemplate template = buildTemplateFromArgs.create(argv);
    Options options = findOptions(argv);
    Retryer retryer = this.retryer.clone();
    while (true) {
      try {
        // 执行并解码
        return executeAndDecode(template, options);
      } catch (RetryableException e) {
        // 重试,默认是从不重试
        try {
          retryer.continueOrPropagate(e);
        } catch (RetryableException th) {
          Throwable cause = th.getCause();
          if (propagationPolicy == UNWRAP && cause != null) {
            throw cause;
          } else {
            throw th;
          }
        }
        continue;
      }
    }
}

可以看到,经过处理后,URI 上的占位符就被参数替换了,并且拼接了请求参数。

执行请求和解码

接着看 executeAndDecode,主要有三步:

  • 先调用 targetRequest 方法,主要就是遍历 RequestInterceptor 对请求模板 RequestTemplate 定制化,然后调用 HardCodedTargettarget 方法将 RequestTemplate 转换成 Request 请求对象,Request 封装了请求地址、请求头、body 等信息。
  • 然后使用客户端 client 来执行请求,就是 LoadBalancerFeignClient,这里就进入了负载均衡请求了。
  • 最后用解码器 decoder 来解析响应结果,将结果转换成接口的返回类型。
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
    // 处理RequestTemplate,得到请求对象 Request
    Request request = targetRequest(template);

    Response response;
    try {
      // 调用 client 执行请求,client => LoadBalancerFeignClient
      response = client.execute(request, options);
      // 构建响应 Response
      response = response.toBuilder()
          .request(request)
          .requestTemplate(template)
          .build();
    } catch (IOException e) {
      //...
    }

    if (decoder != null) {
      // 使用解码器解码,将返回数据转换成接口的返回类型
      return decoder.decode(response, metadata.returnType());
    }

    //....
}
// 应用拦截器处理 RequestTemplate,最后使用 target 从 RequestTemplate 中得到 Request
Request targetRequest(RequestTemplate template) {
    for (RequestInterceptor interceptor : requestInterceptors) {
      interceptor.apply(template);
    }
    // target => HardCodedTarget
    return target.apply(template);
}

HardCodedTarget 是硬编码写死的,我们没有办法定制化,看下它的 apply 方法,主要就是处理 RequestTemplate 模板的地址,生成完成的请求地址。最后返回 Request 请求对象。

public Request apply(RequestTemplate input) {
  if (input.url().indexOf("http") != 0) {
    // url() => http://demo-producer
    // input.target 处理请求模板
    input.target(url());
  }
  return input.request();
}

可以看到经过 HardCodedTarget 的 apply 方法之后,就拼接上了 url 前缀了。

LoadBalancerFeignClient 负载均衡

LoadBalancerFeignClient 是 Feign 实现负载均衡核心的组件,是 Feign 网络请求组件 Client 的默认实现,LoadBalancerFeignClient 最后是使用 FeignLoadBalancer 来进行负载均衡的请求。

看 LoadBalancerFeignClient 的 execute 方法,从这里到后面执行负载均衡请求,其实跟分析 Ribbon 源码中 RestTemplate 的负载均衡请求都是类似的了。

  • 可以看到也是先将请求封装到 ClientRequest,实现类是 FeignLoadBalancer.RibbonRequest。注意 RibbonRequest 第一个参数 Client 就是设置的 LoadBalancerFeignClient 的代理对象,启用 apache httpclient 时,就是 ApacheHttpClient
  • 然后获取客户端配置,也就是说 Ribbon 的客户端配置对 Feign 同样生效
  • 最后获取了负载均衡器 FeignLoadBalancer,然后执行负载均衡请求。
public Response execute(Request request, Request.Options options) throws IOException {
    try {
        URI asUri = URI.create(request.url());
        // 客户端名称:demo-producer
        String clientName = asUri.getHost();
        URI uriWithoutHost = cleanUrl(request.url(), clientName);
        // 封装 ClientRequest => FeignLoadBalancer.RibbonRequest
        FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
                this.delegate, request, uriWithoutHost);
        // 客户端负载均衡配置 ribbon.demo-producer.*
        IClientConfig requestConfig = getClientConfig(options, clientName);
        // lbClient => 负载均衡器 FeignLoadBalancer,执行负载均衡请求
        return lbClient(clientName)
                .executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
    }
    catch (ClientException e) {
        //...
    }
}

private FeignLoadBalancer lbClient(String clientName) {
    return this.lbClientFactory.create(clientName);
}

进入 executeWithLoadBalancer 方法,这就跟 Ribbon 源码中分析的是一样的了,最终就验证了 Feign 基于 Ribbon 来做负载均衡请求。

public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
    // 负载均衡器执行命令
    LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig);

    try {
        return command.submit(
            new ServerOperation<T>() {
                @Override
                public Observable<T> call(Server server) {
                    // 用Server的信息重构URI地址
                    URI finalUri = reconstructURIWithServer(server, request.getUri());
                    S requestForServer = (S) request.replaceUri(finalUri);
                    try {
                        // 实际调用 LoadBalancerFeignClient 的 execute 方法
                        return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
                    }
                    catch (Exception e) {
                        return Observable.error(e);
                    }
                }
            })
            .toBlocking()
            .single();
    } catch (Exception e) {
        //....
    }
}

重构URI后,实际是调用 FeignLoadBalancer 的 execute 方法来执行最终的HTTP调用的。看下 FeignLoadBalancer 的 execute 方法,最终来说,就是使用代理的HTTP客户端来执行请求。

默认情况下,就是 Client.Default,用 HttpURLConnection 执行HTTP请求;启用了 httpclient 后,就是 ApacheHttpClient;启用了 okhttp,就是 OkHttpClient。

这里有一点需要注意的是,FeignClient 虽然可以配置超时时间,但进入 FeignLoadBalancer 的 execute 方法后,可以看到会用 Ribbon 的超时时间覆盖 Feign 配置的超时时间,最终以 Ribbon 的超时时间为准。

public RibbonResponse execute(RibbonRequest request, IClientConfig configOverride) throws IOException {
    Request.Options options;
    if (configOverride != null) {
        // 用 Ribbon 的超时时间覆盖了feign配置的超时时间
        RibbonProperties override = RibbonProperties.from(configOverride);
        options = new Request.Options(override.connectTimeout(this.connectTimeout),
                override.readTimeout(this.readTimeout));
    }
    else {
        options = new Request.Options(this.connectTimeout, this.readTimeout);
    }
    // request.client() HTTP客户端对象
    Response response = request.client().execute(request.toRequest(), options);
    return new RibbonResponse(request.getUri(), response);
}

一张图总结 Feign 负载均衡请求

关于Ribbon的源码分析请看前面 Ribbon 相关的文章,Ribbon 如何从 eureka 注册中心获取 Server 就不再分析了。

下面这张图总结了 Feign 负载均衡请求的流程:

  • 首先服务启动的时候会扫描解析 @FeignClient 注解的接口,并生成代理类注入到容器中。我们注入 @FeignClient 接口时其实就是注入的这个代理类。
  • 调用接口方法时,会被代理对象拦截,进入 ReflectiveFeign.FeignInvocationHandlerinvoke 方法执行请求。
  • FeignInvocationHandler 会根据调用的接口方法获取已经构建好的方法处理器 SynchronousMethodHandler,然后调用它的 invoke 方法执行请求。
  • 在 SynchronousMethodHandler 的 invoke 方法中,会先根据请求参数构建请求模板 RequestTemplate,这个时候会处理参数中的占位符、拼接请求参数、处理body中的参数等等。
  • 然后将 RequestTemplate 转成 Request,在转换的过程中:
    • 先是用 RequestInterceptor 处理请求模板,因此我们可以自定义拦截器来定制化 RequestTemplate。
    • 之后用 Target(HardCodedTarget)处理请求地址,拼接上服务名前缀。
    • 最后调用 RequestTemplate 的 request 方法获取到 Request 对象。
  • 得到 Request 后,就调用 LoadBalancerFeignClient 的 execute 方法来执行请求并得到请求结果 Response
    • 先构造 ClientRequest,并获取到负载均衡器 FeignLoadBalancer,然后就执行负载均衡请求。
    • 负载均衡请求最终进入到 AbstractLoadBalancerAwareClient,executeWithLoadBalancer 方法中,会先构建一个 LoadBalancerCommand,然后提交一个 ServerOperation。
    • LoadBalancerCommand 会通过 LoadBalancerContext 根据服务名获取一个 Server。
    • 在 ServerOperation 中根据 Server 的信息重构URI,将服务名替换为具体的IP地址,之后就可以发起真正的HTTP调用了。
    • HTTP调用时,底层使用的组件默认是 HttpURLConnection;启用了okhttp,就是 okhttp 的 OkHttpClient;启用了 httpclient,就是 apache 的 HttpClient。
    • 最红用 HTTP 客户端组件执行请求,得到响应结果 Response
  • 得到 Response 后,就使用解码器 Decoder 解析响应结果,返回接口方法定义的返回类型。

负载均衡获取Server的核心组件是 LoadBalancerClient,具体的源码分析可以参考 Ribbon 源码分析的两篇文章。LoadBalancerClient 负载均衡的原理可以看下面这张图。