再谈openfeign,聊聊它的源代码

本文涉及的产品
传统型负载均衡 CLB,每月750个小时 15LCU
应用型负载均衡 ALB,每月750个小时 15LCU
网络型负载均衡 NLB,每月750个小时 15LCU
简介: 再谈openfeign,聊聊它的源代码

上篇文章我讲了openfeign的超时和重试。首先我想发2个勘误


1.下面的2个配置对单个接口超时并没有起作用,作为eureka客户端使用时,起作用的其实是默认超时时间,作为普通http客户端时,起作用的其实也是默认超时时间。

hystrix.command.FeignAsHttpCient#feignReadTimeout().execution.isolation.thread.timeoutInMilliseconds=13000
hystrix.command.FeignAsEurekaClient#feignReadTimeout().execution.isolation.thread.timeoutInMilliseconds=23000

2.openfeign作为普通客户端,其实是可以重试的。


看了本文的源码解读,就可以搞明白上面的2个问题了。


Feignclient注册


服务启动时,feignclient需要注册为spring的bean,具体实现代码在FeignClientsRegistrar,这个类实现了ImportBeanDefinitionRegistrar,spring初始化容器的时候会扫描实现这个接口的方法,进行bean注册。


接口定义的方法是registerBeanDefinitions,FeignClientsRegistrar的实现如下:

public void registerBeanDefinitions(AnnotationMetadata metadata,
    BeanDefinitionRegistry registry) {
  registerDefaultConfiguration(metadata, registry);
  registerFeignClients(metadata, registry);
}
private void registerDefaultConfiguration(AnnotationMetadata metadata,
    BeanDefinitionRegistry registry) {
  Map<String, Object> defaultAttrs = metadata
      .getAnnotationAttributes(EnableFeignClients.class.getName(), true);
  if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
    String name;
    if (metadata.hasEnclosingClass()) {
      name = "default." + metadata.getEnclosingClassName();
    }
    else {
      name = "default." + metadata.getClassName();//name="default.boot.Application"
    }
    registerClientConfiguration(registry, name,
        defaultAttrs.get("defaultConfiguration"));
  }
}

下面这个方法看过spring代码的就熟悉了,一个bean的注册:

private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
    Object configuration) {
  BeanDefinitionBuilder builder = BeanDefinitionBuilder
      .genericBeanDefinition(FeignClientSpecification.class);
  builder.addConstructorArgValue(name);
  builder.addConstructorArgValue(configuration);
  registry.registerBeanDefinition(
  //这里name="default.boot.Application.FeignClientSpecification",
  //bean="org.springframework.cloud.openfeign.FeignClientSpecification"
      name + "." + FeignClientSpecification.class.getSimpleName(),
      builder.getBeanDefinition());
}

下面的代码是注册Feign客户端:

public void registerFeignClients(AnnotationMetadata metadata,
    BeanDefinitionRegistry registry) {
  ClassPathScanningCandidateComponentProvider scanner = getScanner();
  scanner.setResourceLoader(this.resourceLoader);
  //配置要扫描的basePackage,这里是"boot"(@SpringBootApplication(scanBasePackages = {"boot"}))
  for (String basePackage : basePackages) {
    Set<BeanDefinition> candidateComponents = scanner
        .findCandidateComponents(basePackage);
    for (BeanDefinition candidateComponent : candidateComponents) {
      if (candidateComponent instanceof AnnotatedBeanDefinition) {
        // verify annotated class is an interface
        AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
        AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
        Assert.isTrue(annotationMetadata.isInterface(),
            "@FeignClient can only be specified on an interface");
                //找出注解是FeignClient的attributes,注册到spring容器
        Map<String, Object> attributes = annotationMetadata
            .getAnnotationAttributes(
                FeignClient.class.getCanonicalName());
        String name = getClientName(attributes);//这里的name是springboot-mybatis
        registerClientConfiguration(registry, name,
            attributes.get("configuration"));
                //这个方法就不讲了,封装BeanDefinition,注册到spring容器
        registerFeignClient(registry, annotationMetadata, attributes);
      }
    }
  }
}

FeignClient初始化


Feign客户端的初始化在FeignClientFactoryBean类,这个类实现了FactoryBean接口,在getObject,这里的uml类图如下:

微信图片_20221212163608.png

getObject方法的代码如下:

<T> T getTarget() {
  FeignContext context = applicationContext.getBean(FeignContext.class);
  Feign.Builder builder = feign(context);
    //如果feignClient没有指定url,就走这个分支,这里会通过ribbon走负载均衡
  if (!StringUtils.hasText(this.url)) {
    if (!this.name.startsWith("http")) {
      url = "http://" + this.name;
    }
    else {
      url = this.name;
    }
    //http://springboot-mybatis
    url += cleanPath();
    return (T) loadBalance(builder, context, new HardCodedTarget<>(this.type,
        this.name, url));
  }
  //feignClient指定了url,走到这儿
  if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
    this.url = "http://" + this.url;
  }
  //url = "http://localhost:8083"
  String url = this.url + cleanPath();
  //LoadBalancerFeignClient
  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
      //OKHttpClient
      client = ((LoadBalancerFeignClient)client).getDelegate();
    }
    builder.client(client);
  }
  //这里是HystrixTargeter,不知道为什么总是不用DefaultTargeter
  Targeter targeter = get(context, Targeter.class);
  return (T) targeter.target(this, builder, context, new HardCodedTarget<>(
      this.type, this.name, url));
}

我们先来看一下FeignClient不指定url的情况,代码如下:

protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
    HardCodedTarget<T> target) {
  //这里的client是LoadBalancerFeignClient
  Client client = getOptional(context, Client.class);
  if (client != null) {
    builder.client(client);
    //这里的targeter是HystrixTargeter
    Targeter targeter = get(context, Targeter.class);
    return targeter.target(this, builder, context, target);
  }
  throw new IllegalStateException(
      "No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?");
}

再看看HystrixTargeter中的target

public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
          Target.HardCodedTarget<T> target) {
  if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) {
    return feign.target(target);
  }
  feign.hystrix.HystrixFeign.Builder builder = (feign.hystrix.HystrixFeign.Builder) feign;
  SetterFactory setterFactory = getOptional(factory.getName(), context,
    SetterFactory.class);
  if (setterFactory != null) {
    builder.setterFactory(setterFactory);
  }
  Class<?> fallback = factory.getFallback();
  if (fallback != void.class) {
    return targetWithFallback(factory.getName(), context, target, builder, fallback);
  }
  Class<?> fallbackFactory = factory.getFallbackFactory();
  if (fallbackFactory != void.class) {
    return targetWithFallbackFactory(factory.getName(), context, target, builder, fallbackFactory);
  }
    //这里返回的是一个HardCodedTarget的代理,HardCodedTarget(type=FeignAsEurekaClient, name=springboot-mybatis, url=http://springboot-mybatis)
  //FeignAsEurekaClient就是我demo中的feign客户端类,可以看出,这里是为FeignAsEurekaClient做了一个代理
  return feign.target(target);
}

上面targe返回的对象debug内容如下:

proxy = {$Proxy168@11372} "HardCodedTarget(type=FeignAsEurekaClient, name=springboot-mybatis, url=http://springboot-mybatis)"
h = {HystrixInvocationHandler@11366} "HardCodedTarget(type=FeignAsEurekaClient, name=springboot-mybatis, url=http://springboot-mybatis)"
 target = {Target$HardCodedTarget@11142} "HardCodedTarget(type=FeignAsEurekaClient, name=springboot-mybatis, url=http://springboot-mybatis)"
  type = {Class@9295} "interface boot.feign.FeignAsEurekaClient"
  name = "springboot-mybatis"
  url = "http://springboot-mybatis"
 dispatch = {LinkedHashMap@11346}  size = 5
  {Method@11392} "public abstract java.lang.String boot.feign.FeignAsEurekaClient.getEmployeebyName(java.lang.String)" -> {SynchronousMethodHandler@11431} 
  {Method@11393} "public abstract java.lang.String boot.feign.FeignAsEurekaClient.saveEmployeebyName(boot.feign.Employee)" -> {SynchronousMethodHandler@11432} 
  {Method@11394} "public abstract java.lang.String boot.feign.FeignAsEurekaClient.feignReadTimeout()" -> {SynchronousMethodHandler@11433} 
  {Method@11395} "public abstract java.lang.String boot.feign.FeignAsEurekaClient.uploadFile(org.springframework.web.multipart.MultipartFile)" -> {SynchronousMethodHandler@11434} 
  {Method@11396} "public abstract feign.Response boot.feign.FeignAsEurekaClient.downloadFile(java.lang.String)" -> {SynchronousMethodHandler@11435} 
 fallbackFactory = null
 fallbackMethodMap = {LinkedHashMap@11382}  size = 5
  {Method@11392} "public abstract java.lang.String boot.feign.FeignAsEurekaClient.getEmployeebyName(java.lang.String)" -> {Method@11392} "public abstract java.lang.String boot.feign.FeignAsEurekaClient.getEmployeebyName(java.lang.String)"
  {Method@11393} "public abstract java.lang.String boot.feign.FeignAsEurekaClient.saveEmployeebyName(boot.feign.Employee)" -> {Method@11393} "public abstract java.lang.String boot.feign.FeignAsEurekaClient.saveEmployeebyName(boot.feign.Employee)"
  {Method@11394} "public abstract java.lang.String boot.feign.FeignAsEurekaClient.feignReadTimeout()" -> {Method@11394} "public abstract java.lang.String boot.feign.FeignAsEurekaClient.feignReadTimeout()"
  {Method@11395} "public abstract java.lang.String boot.feign.FeignAsEurekaClient.uploadFile(org.springframework.web.multipart.MultipartFile)" -> {Method@11395} "public abstract java.lang.String boot.feign.FeignAsEurekaClient.uploadFile(org.springframework.web.multipart.MultipartFile)"
  {Method@11396} "public abstract feign.Response boot.feign.FeignAsEurekaClient.downloadFile(java.lang.String)" -> {Method@11396} "public abstract feign.Response boot.feign.FeignAsEurekaClient.downloadFile(java.lang.String)"
 setterMethodMap = {LinkedHashMap@11383}  size = 5
  {Method@11392} "public abstract java.lang.String boot.feign.FeignAsEurekaClient.getEmployeebyName(java.lang.String)" -> {HystrixCommand$Setter@11414} 
  {Method@11393} "public abstract java.lang.String boot.feign.FeignAsEurekaClient.saveEmployeebyName(boot.feign.Employee)" -> {HystrixCommand$Setter@11415} 
  {Method@11394} "public abstract java.lang.String boot.feign.FeignAsEurekaClient.feignReadTimeout()" -> {HystrixCommand$Setter@11416} 
  {Method@11395} "public abstract java.lang.String boot.feign.FeignAsEurekaClient.uploadFile(org.springframework.web.multipart.MultipartFile)" -> {HystrixCommand$Setter@11417} 
  {Method@11396} "public abstract feign.Response boot.feign.FeignAsEurekaClient.downloadFile(java.lang.String)" -> {HystrixCommand$Setter@11418}

我们再来看一下FeignClient指定url的情况,这种情况跟不指定url类似,只是代理的类中有url值。debug发现使用的代理也是HardCodedTarget,代码如下:

proxy = {$Proxy161@11205} "HardCodedTarget(type=FeignAsHttpCient, name=feign, url=http://localhost:8083)"
 h = {ReflectiveFeign$FeignInvocationHandler@11201} "HardCodedTarget(type=FeignAsHttpCient, name=feign, url=http://localhost:8083)"
  target = {Target$HardCodedTarget@11192} "HardCodedTarget(type=FeignAsHttpCient, name=feign, url=http://localhost:8083)"
   type = {Class@9298} "interface boot.feign.FeignAsHttpCient"
   name = "feign"
   url = "http://localhost:8083"
  dispatch = {LinkedHashMap@11194}  size = 1
   {Method@11221} "public abstract java.lang.String boot.feign.FeignAsHttpCient.feignReadTimeout()" -> {SynchronousMethodHandler@11222}

从上面的代码分析中,我们看出,这2种方式的主要不同是,如果不指定url,则给Feign传入的是LoadBalancerFeignClient,它是一个装饰器,里面的delegate指定了实际的client,这里是OkHttpClient。而如果指定了url,给Feign传入的就是实际的httpclient,这里是OKHttpClient。


上面使用了代理,这里的UML类图如下:

微信图片_20221212163821.png

通过这张图,我们可以看到代理是怎么最终走到OkHttpClient的。如果使用了熔断,则使用HystrixInvocationHandler,否则使用FeignInvocationHandler,他们的invoke方法最终都调用了SynchronousMethodHandler的invoke,这里最终调用了底层的OkHttpClient。


指定url


上面的类图看出,SynchronousMethodHandler这个类的invoke方法是上面的代理中反射触发的方法,我们来看一下:

public Object invoke(Object[] argv) throws Throwable {
  //RequestTemplate封装RequestTemplate
  RequestTemplate template = buildTemplateFromArgs.create(argv);
  Retryer retryer = this.retryer.clone();
  while (true) {
    try {
      return executeAndDecode(template);
    } catch (RetryableException e) {
    //这里可以看出,无论是不是指定url,都会走重试的逻辑,默认重试是不生效的
      try {
        retryer.continueOrPropagate(e);
      } catch (RetryableException th) {
        Throwable cause = th.getCause();
        if (propagationPolicy == UNWRAP && cause != null) {
          throw cause;
        } else {
          throw th;
        }
      }
      if (logLevel != Logger.Level.NONE) {
        logger.logRetry(metadata.configKey(), logLevel);
      }
      continue;
    }
  }
}
Object executeAndDecode(RequestTemplate template) throws Throwable {
    Request request = targetRequest(template);
    Response response;
    long start = System.nanoTime();
    try {
    //这里调用OkHttpClient,这个并不是原生的那个OkHttpClient,而是Feign封装的,看下面的讲解
      response = client.execute(request, options);
    } catch (IOException e) {
      if (logLevel != Logger.Level.NONE) {
        logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
      }
      throw errorExecuting(request, e);
    }
    long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
    boolean shouldClose = true;
    try {
      //省略部分代码
    //处理响应
      if (response.status() >= 200 && response.status() < 300) {
        if (void.class == metadata.returnType()) {
          return null;
        } else {
          Object result = decode(response);
          shouldClose = closeAfterDecode;
          return result;
        }
      } else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) {
        Object result = decode(response);
        shouldClose = closeAfterDecode;
        return result;
      } else {
        throw errorDecoder.decode(metadata.configKey(), response);
      }
    } catch (IOException e) {
      if (logLevel != Logger.Level.NONE) {
        logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime);
      }
      throw errorReading(request, response, e);
    } finally {
      if (shouldClose) {
        ensureClosed(response.body());
      }
    }
  }

前面我们讲过,如果指定了url,就不走ribbon的LoadBalance了,而是直接用httpclient去发送请求。其实说"直接",也不完全是直接,因为feign封装了一个自己的OkHttpClient,并且有自己的Request,Response。


OkHttpClient这个装饰器类首先包含了一个okhttp3.OkHttpClient的客户端,发送请求的时候,首先把feign.Request转换成okhttp的Request,而接收响应的时候,会把okhttp的Response转换成feign.Response,代码如下:

feign.Request转换成okhttp的Request

static Request toOkHttpRequest(feign.Request input) {
  Request.Builder requestBuilder = new Request.Builder();
  requestBuilder.url(input.url());//封装url
  MediaType mediaType = null;
  boolean hasAcceptHeader = false;
  //封装headers
  for (String field : input.headers().keySet()) {
    if (field.equalsIgnoreCase("Accept")) {
      hasAcceptHeader = true;
    }
    for (String value : input.headers().get(field)) {
      requestBuilder.addHeader(field, value);
      if (field.equalsIgnoreCase("Content-Type")) {
        mediaType = MediaType.parse(value);
        if (input.charset() != null) {
          mediaType.charset(input.charset());
        }
      }
    }
  }
  // Some servers choke on the default accept string.
  if (!hasAcceptHeader) {
    requestBuilder.addHeader("Accept", "*/*");
  }
  byte[] inputBody = input.body();
  boolean isMethodWithBody =
      HttpMethod.POST == input.httpMethod() || HttpMethod.PUT == input.httpMethod()
          || HttpMethod.PATCH == input.httpMethod();
  if (isMethodWithBody) {
    requestBuilder.removeHeader("Content-Type");
    if (inputBody == null) {
      // write an empty BODY to conform with okhttp 2.4.0+
      // http://johnfeng.github.io/blog/2015/06/30/okhttp-updates-post-wouldnt-be-allowed-to-have-null-body/
      inputBody = new byte[0];
    }
  }
  //封装body
  RequestBody body = inputBody != null ? RequestBody.create(mediaType, inputBody) : null;
  requestBuilder.method(input.httpMethod().name(), body);
  return requestBuilder.build();
}

把okhttp的Response转换成feign.Response

private static feign.Response toFeignResponse(Response response, feign.Request request)
    throws IOException {
  return feign.Response.builder()
      .status(response.code())
      .reason(response.message())
      .request(request)
      .headers(toMap(response.headers()))
      .body(toBody(response.body()))
      .build();
}

发送请求的方法

public feign.Response execute(feign.Request input, feign.Request.Options options)
    throws IOException {
  okhttp3.OkHttpClient requestScoped;
  //这里delegate的connectTimeoutMillis默认是2000,delegate的readTimeoutMillis默认是100000
  //从代码可以看到,如果配置了options的超时时间跟不一样,会被替换掉
  /**
   *比如下面的时间设置就会替换掉默认时间
   *feign.client.config.default.connectTimeout=3000
   *feign.client.config.default.readTimeout=13000      
   *
   *网上说的对单个接口设置超时时间,下面这个超时时间是不生效的,从源码中我们也能看到了
   *hystrix.command.FeignAsHttpCient#feignReadTimeout().execution.isolation.thread.timeoutInMilliseconds=13000
   *
   *也可以看出,要想自定义超时,最好的方法就是给Request定制Options
   *
   */
  if (delegate.connectTimeoutMillis() != options.connectTimeoutMillis()
      || delegate.readTimeoutMillis() != options.readTimeoutMillis()) {
    requestScoped = delegate.newBuilder()
        .connectTimeout(options.connectTimeoutMillis(), TimeUnit.MILLISECONDS)
        .readTimeout(options.readTimeoutMillis(), TimeUnit.MILLISECONDS)
        .followRedirects(options.isFollowRedirects())
        .build();
  } else {
    requestScoped = delegate;
  }
  Request request = toOkHttpRequest(input);
  Response response = requestScoped.newCall(request).execute();
  return toFeignResponse(response, input).toBuilder().request(input).build();
}

到这里,我们就讲完了指定url的FeignClient请求流程,相信你对超时和重试也有了一定的认识。


不指定url


上一节的UML类图我们可以看出,无论是否指定url,最终都是要从SynchronousMethodHandler类的executeAndDecode方法调用HttpClient。不指定url的情况下,使用的client是LoadBalancerFeignClient。我们看一下他的execute方法:

public Response execute(Request request, Request.Options options) throws IOException {
  try {
      //asUri="http://springboot-mybatis/feign/feignReadTimeout"
    URI asUri = URI.create(request.url());
    String clientName = asUri.getHost();//springboot-mybatis
    URI uriWithoutHost = cleanUrl(request.url(), clientName);//http:///feign/feignReadTimeout
    //下面封装了OkHttpClient,默认连接超时是2s,读超时是10s
    FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
        this.delegate, request, uriWithoutHost);
        //这里如果配置了feign相关的配置,就是我们配置的,否则就是默认的DEFAULT_OPTIONS
    IClientConfig requestConfig = getClientConfig(options, clientName);
    return lbClient(clientName).executeWithLoadBalancer(ribbonRequest,
        requestConfig).toResponse();
  }
  catch (ClientException e) {
    IOException io = findIOException(e);
    if (io != null) {
      throw io;
    }
    throw new RuntimeException(e);
  }
}

上面的executeWithLoadBalancer调用了AbstractLoadBalancerAwareClient的executeWithLoadBalancer方法,代码如下:

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) {
            //这里的finalUri是http://192.168.0.118:8083/feign/feignReadTimeout
                    URI finalUri = reconstructURIWithServer(server, request.getUri());//这个就是一个拼接url的方法,不细讲了
          //下面的requestForServer是FeignLoadBalancer,看上面的UML类图,是AbstractLoadBalancerAwareClient的子类
                    S requestForServer = (S) request.replaceUri(finalUri);
                    try {
                        return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
                    } 
                    catch (Exception e) {
                        return Observable.error(e);
                    }
                }
            })
            .toBlocking()
            .single();
    } catch (Exception e) {
        Throwable t = e.getCause();
        if (t instanceof ClientException) {
            throw (ClientException) t;
        } else {
            throw new ClientException(e);
        }
    }
}

上面的execute方法执行的是FeignLoadBalancer里面的execute方法,代码如下:

public RibbonResponse execute(RibbonRequest request, IClientConfig configOverride)
    throws IOException {
  Request.Options options;
  if (configOverride != null) {
      /**
     * 下面就是我们配置的超时时间,在这里被替换到了Request的Options中,XXX是default或者是服务名
     * feign.client.config.XXX.connectTimeout=3000
         * feign.client.config.XXX.readTimeout=7000
     */
    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就是OkHttpClient
  Response response = request.client().execute(request.toRequest(), options);
  return new RibbonResponse(request.getUri(), response);
}

后面的逻辑就是feign.okhttp.OkHttpClient的execute方法了,跟上节介绍的一样,这里不再赘述了。


可以看出,不指定url的情况,会使用ribbon做负载均衡,并对feign的Request和Response进行了一层封装,封装类是RibbonRequest和RibbonResponse。


ribbon负载


顺带讲一下ribbon的负载吧。上面的讲解中提到了AbstractLoadBalancerAwareClient的executeWithLoadBalancer方法,我们再贴一次代码:

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) {
            //这里的finalUri是http://192.168.0.118:8083/feign/feignReadTimeout
                    URI finalUri = reconstructURIWithServer(server, request.getUri());//这个就是一个拼接url的方法,不细讲了
          //下面的requestForServer是FeignLoadBalancer,看上面的UML类图,是AbstractLoadBalancerAwareClient的子类
                    S requestForServer = (S) request.replaceUri(finalUri);
                    try {
                        return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
                    } 
                    catch (Exception e) {
                        return Observable.error(e);
                    }
                }
            })
            .toBlocking()
            .single();
    } catch (Exception e) {
        Throwable t = e.getCause();
        if (t instanceof ClientException) {
            throw (ClientException) t;
        } else {
            throw new ClientException(e);
        }
    }
}

我们看一下上面的command.submit方法,这个方法调用了LoadBalancerCommand的submit方法,代码如下:

public Observable<T> submit(final ServerOperation<T> operation) {
    final ExecutionInfoContext context = new ExecutionInfoContext();
    /**
   * 下面的配置就是当前server请求失败后再重试一次,如果还失败,就请求下一个server,如过还了3个server都失败,就返回错误了
   *# 对当前server的重试次数,默认是0
     *ribbon.maxAutoRetries=1
     *# 切换实例的重试次数,默认是0
     *ribbon.maxAutoRetriesNextServer=3
     *# 对所有操作请求都进行重试,这里建议不要设置成true,否则会对所有操作请求都进行重试
     *ribbon.okToRetryOnAllOperations=true
     *# 根据Http响应码进行重试
     *ribbon.retryableStatusCodes=500,404,502
    **/
    final int maxRetrysSame = retryHandler.getMaxRetriesOnSameServer();
    final int maxRetrysNext = retryHandler.getMaxRetriesOnNextServer();
    // Use the load balancer
    Observable<T> o = 
            (server == null ? selectServer() : Observable.just(server))
            .concatMap(new Func1<Server, Observable<T>>() {
                //省略部分代码
            });
    //如果没有获取到,那就重试
    if (maxRetrysNext > 0 && server == null) 
        o = o.retry(retryPolicy(maxRetrysNext, false));
    return o.onErrorResumeNext(new Func1<Throwable, Observable<T>>() {
        //省略部分代码
    });
}

我们看一下selectServer方法:

private Observable<Server> selectServer() {
    return Observable.create(new OnSubscribe<Server>() {
        @Override
        public void call(Subscriber<? super Server> next) {
            try {
                Server server = loadBalancerContext.getServerFromLoadBalancer(loadBalancerURI, loadBalancerKey);   
                next.onNext(server);
                next.onCompleted();
            } catch (Exception e) {
                next.onError(e);
            }
        }
    });
}

继续跟踪,我们来看getServerFromLoadBalancer方法:

public Server getServerFromLoadBalancer(@Nullable URI original, @Nullable Object loadBalancerKey) throws ClientException {
    String host = null;
    int port = -1;
    if (original != null) {
        host = original.getHost();
    }
    if (original != null) {
        Pair<String, Integer> schemeAndPort = deriveSchemeAndPortFromPartialUri(original);        
        port = schemeAndPort.second();
    }
    // Various Supported Cases
    // The loadbalancer to use and the instances it has is based on how it was registered
    // In each of these cases, the client might come in using Full Url or Partial URL
    ILoadBalancer lb = getLoadBalancer();
    if (host == null) {
        // Partial URI or no URI Case
        // well we have to just get the right instances from lb - or we fall back
        if (lb != null){
            Server svc = lb.chooseServer(loadBalancerKey);
            //省略代码
            host = svc.getHost();
            if (host == null){
                throw new ClientException(ClientException.ErrorType.GENERAL,
                        "Invalid Server for :" + svc);
            }
            logger.debug("{} using LB returned Server: {} for request {}", new Object[]{clientName, svc, original});
            return svc;
        } else {//省略代码
        }
    } else {//省略代码
    }
    // end of creating final URL
    if (host == null){
        throw new ClientException(ClientException.ErrorType.GENERAL,"Request contains no HOST to talk to");
    }
    // just verify that at this point we have a full URL
    return new Server(host, port);
}

简单看一下上面这个ILoadBalancer,这里是一个ZoneAwareLoadBalancer,里面保存的服务的server列表和状态:

lb = {ZoneAwareLoadBalancer@14492} "DynamicServerListLoadBalancer:{NFLoadBalancer:name=springboot-mybatis,current list of Servers=[192.168.0.118:8083],Load balancer stats=Zone stats: ]}ServerList:org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList@43db1f5d"
 balancers = {ConcurrentHashMap@17695}  size = 1
  "defaultzone" -> {BaseLoadBalancer@17904} "{NFLoadBalancer:name=springboot-mybatis_defaultzone,current list of Servers=[192.168.0.118:8083],Load balancer stats=Zone stats: {]\n]}"

通过这个负载均衡器,feign就可以获取到一个server地址,然后把请求发送出去。


总结


openfeign作为eureka客户端和普通http客户端,有所不同。作为eureka客户端时,不用指定url,使用ribbon封装了请求和响应,并且通过ribbon作为负载均衡。


openfeign作为eureka客户端和普通http客户端,都是可以重试的。因为都是通过SynchronousMethodHandler这个类invoke来触发的,失败了都会捕获RetryableException。但是要知道,默认配置是不支持重试的。


openfeign作为eureka客户端和普通http客户端,对单个接口设置超时时间,都是不生效的,实际上还是使用了默认的超时时间。

相关实践学习
SLB负载均衡实践
本场景通过使用阿里云负载均衡 SLB 以及对负载均衡 SLB 后端服务器 ECS 的权重进行修改,快速解决服务器响应速度慢的问题
负载均衡入门与产品使用指南
负载均衡(Server Load Balancer)是对多台云服务器进行流量分发的负载均衡服务,可以通过流量分发扩展应用系统对外的服务能力,通过消除单点故障提升应用系统的可用性。 本课程主要介绍负载均衡的相关技术以及阿里云负载均衡产品的使用方法。
相关文章
|
7月前
|
负载均衡 Java 物联网
SpringCloud简介和用处
SpringCloud简介和用处
167 0
|
7月前
|
Java Spring 容器
Spring框架讲解笔记:spring框架学习的要点总结
Spring框架讲解笔记:spring框架学习的要点总结
|
XML Java 应用服务中间件
涨薪50%%就因回答对了这题:为什么Spring Boot提倡约定优于配置?
在 Spring Boot 中,通过约定优于配置这个思想,可以让我们少写很多的配置,然后就只需要关注业务代码的编写就行。今天呢,我给大家聊聊为什么SpringBoot提倡约定优于配置。
101 0
|
XML JSON Java
SpingBoot原理
1. 配置优先级:Springboot项目当中属性配置的常见方式以及配置的优先级 2. Bean的管理 3. 剖析Springboot的底层原理
74 0
|
设计模式 Java Spring
【Spring】核心部分之AOP:通过列举代码例子,从底层刨析,深入源码,轻轻松松理解Spring的核心AOP,AOP有这一篇足以
【Spring】核心部分之AOP:通过列举代码例子,从底层刨析,深入源码,轻轻松松理解Spring的核心AOP,AOP有这一篇足以
|
XML 设计模式 缓存
面试必问系列之最强源码分析,带你一步步弄清楚Spring如何解决循环依赖
面试必问系列之最强源码分析,带你一步步弄清楚Spring如何解决循环依赖
29631 6
面试必问系列之最强源码分析,带你一步步弄清楚Spring如何解决循环依赖
|
Java 应用服务中间件 数据库连接
头秃系列,二十三张图带你从源码分析Spring Boot 启动流程~
前言 源码版本 从哪入手? 源码如何切分? 如何创建SpringApplication? 设置应用类型 设置初始化器(Initializer) 设置监听器(Listener) 设置监听器(Listener) 执行run()方法 获取、启动运行过程监听器 环境构建 创建IOC容器 IOC容器的前置处理 刷新容器 IOC容器的后置处理 发出结束执行的事件 执行Runners 总结 总结
|
JSON 前端开发 Java
Spring Cloud 如何统一异常处理?写得太好了!
Spring Cloud 如何统一异常处理?写得太好了!
187 0
Spring Cloud 如何统一异常处理?写得太好了!
|
运维 Dubbo Cloud Native
Dubbo3 源码解读-宋小生-8:Dubbo启动器DubboBootstrap借助双重校验锁的单例模式进行对象的初始化
> Dubbo3 已经全面取代 HSF2 成为阿里的下一代服务框架,2022 双十一基于 Dubbo3 首次实现了关键业务不停推、不降级的全面用户体验提升,从技术上,大幅提高研发与运维效率的同时地址推送等关键资源利用率提升超 40%,基于三位一体的开源中间件体系打造了阿里在云上的单元化最佳实践和统一标准,同时将规模化实践经验与技术创新贡献开源社区,极大的推动了开源技术与标准的发展。 > 本文是
256 0
|
JavaScript Dubbo 小程序
求你别自己瞎写工具类了,Spring自带的这些他不香麽?
求你别自己瞎写工具类了,Spring自带的这些他不香麽?
求你别自己瞎写工具类了,Spring自带的这些他不香麽?