Feign源码学习

Feign在Spring Cloud中主要用于封装Http请求细节,让微服务之间的调用同服务内调用一样便捷。

Open Feign的源码实现过程主要可以概括为以下几点

  1. 通过@EnableFeignClients引入FeignClientsRegistrar。
  2. FeignClientsRegistrar实现了ImportBeanDefinition接口,扫描对应路径下被@EnableFeign注解修饰的接口(必须是接口),生成对应的FeignClientSpecifition。
  3. FeignAutoConfiguration注入所有的FeignClientSpecification的实例,注入到FeignContext中。
  4. 当接口调用时,通过CGLIB动态代理的形式,与已有的RestTemplate整合请求参数,生成最后的RestTemplate。
  5. RestTemplate被转换为Request,使用了Ribbon还会被转换为RibbonRequest
  6. 通过Ribbon进行负载均衡,将url中的应用名转化为ip。
  7. 调用Request内的Client执行真正的Http请求,client可以是ApacheHttpClient等实例。
  8. 返回结果response,使用了Ribbon还会转化为RibbonResponse。
  9. 根据返回结果的状态码使用Decoder或者ErrorDecoder进行解析。Decoder等组件都维护在FeignContext上下文中。

由此得到Feign的启用和请求调用的大体步骤,这些步骤又可以分为三个阶段:BeanDefinition的注册实例的初始化函数的调用和网络请求三个部分,分别以FeignClientRegistrar、FeignClientFactoryBean、SynchronousMethodHandler为链路起点。

1、BeanDefinition的注册

1.1、@EnableFeignClients注解启用Feign

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class) // 引入FeignClientsRegistrar,用来处理@FeignClient
public @interface EnableFeignClients {   
   /**
    * 用来指定自动扫描的包
    */
   String[] value() default {};
   String[] basePackages() default {};
   Class<?>[] basePackageClasses() default {};

   /**
    * 自定义配置
    */
   Class<?>[] defaultConfiguration() default {};

   /**
    * 指定FeignClient注解修饰的类,如果不为空,将禁用FeignClient自动扫描
    */
   Class<?>[] clients() default {};
}

1.2、FeignClientsRegistrar注册

@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
      BeanDefinitionRegistry registry) {
  // 处理@EnableFeignClients注解,从配置参数来构建Feign的自定义Configuration来注册
   registerDefaultConfiguration(metadata, registry);
  // 扫描@FeignClient注解修饰的类,注册BeanDefinition到BeanDefinitionRegistrar中
   registerFeignClients(metadata, registry);
}

1.2.1、处理@EnableFeignClients注解,注册配置

@EnableFeignClients的自定义配置类是被@Configuration注解修饰的配置类,它会提供一系列组装FeignClient的各类组件实例。这些组件包括:Client、Targeter、Decoder、Encoder和Contract等。

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();
      }
      // 注册BeanDefinition到Spring容器中
      registerClientConfiguration(registry, name,
            defaultAttrs.get("defaultConfiguration"));
   }
}

1.2.2、处理@FeignClient注解,注册FeignClient

public void registerFeignClients(AnnotationMetadata metadata,
      BeanDefinitionRegistry registry) {
   ClassPathScanningCandidateComponentProvider scanner = getScanner();
   scanner.setResourceLoader(this.resourceLoader);

   Set<String> basePackages;
   Map<String, Object> attrs = metadata
         .getAnnotationAttributes(EnableFeignClients.class.getName());
   AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
         FeignClient.class);
   final Class<?>[] clients = attrs == null ? null
         : (Class<?>[]) attrs.get("clients");
     // 配置扫描器,加入过滤条件
   if (clients == null || clients.length == 0) {
      scanner.addIncludeFilter(annotationTypeFilter);
      basePackages = getBasePackages(metadata);
   }
   else {
      final Set<String> clientClasses = new HashSet<>();
      basePackages = new HashSet<>();
      for (Class<?> clazz : clients) {
         basePackages.add(ClassUtils.getPackageName(clazz));
         clientClasses.add(clazz.getCanonicalName());
      }
      AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
         @Override
         protected boolean match(ClassMetadata metadata) {
            String cleaned = metadata.getClassName().replaceAll("\\$", ".");
            return clientClasses.contains(cleaned);
         }
      };
      scanner.addIncludeFilter(
            new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
   }

  // 通过scanner从扫描路径下扫描候选FeignClient
   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");

            Map<String, Object> attributes = annotationMetadata
                  .getAnnotationAttributes(
                        FeignClient.class.getCanonicalName());

            String name = getClientName(attributes);
            registerClientConfiguration(registry, name,
                  attributes.get("configuration"));

            // 注册BeanDefinition到Spring容器中
            registerFeignClient(registry, annotationMetadata, attributes);
         }
      }
   }
}

2.3、FeignAutoConfiguration,将配置的实例放到FeignContext上下文

public class FeignAutoConfiguration {

  // 自动注入所有的FeignClientSpecification配置Bean
   @Autowired(required = false)
   private List<FeignClientSpecification> configurations = new ArrayList<>();

   @Bean
   public FeignContext feignContext() {
      FeignContext context = new FeignContext();
      // 把这些配置Bean放到上下文中,创建FeignContext到Spring容器
      context.setConfigurations(this.configurations);
      return context;
   }
  
 //... 
}

2、实例的初始化

前一步有FeignClientRegistrar完成BeanDefinition的注册后,FeignClientFactoryBean接过使命,进行Bean的初始化。

2.1、FeignClientFactoryBean

FeignClientFactoryBean实现了FactoryBean接口,作为工厂类,Spring容器通过调用它的getObject方法来获取对应的Bean实例。通过判断@FeignClient注解是否有url配置,决定生成的是LoadBalancerFeignClient还是普通的FeignClient,因为前者更常见,这里着重介绍LoadBalancerFeignClient。LoadBalancerFeignClient的负载均衡由Ribbon支持,在这里不做详细介绍。

@Override
public Object getObject() throws Exception {
   return getTarget();
}

// 获取FeignClient实例
<T> T getTarget() {
   FeignContext context = this.applicationContext.getBean(FeignContext.class);
    // 从上下文中获取Encode,Contract等各种组件放到Feign.Builder中
   Feign.Builder builder = feign(context);

    // 判断对应FeignClient是否有url
    // 有url会生成LoadBalancerFeignClient
    // 否则返回普通的FeignClient
   if (!StringUtils.hasText(this.url)) {
      if (!this.name.startsWith("http")) {
         this.url = "http://" + this.name;
      }
      else {
         this.url = this.name;
      }
      this.url += cleanPath();
        // 生成LoadBalancerFeignClient,带有负载均衡
      return (T) loadBalance(builder, context,
            new HardCodedTarget<>(this.type, this.name, this.url));
   }
  
   // 没有配置url,返回普通的FeignClient
  // ...
}

// 获取LoadBalancerFeignClient
// 这里会从上下文中获取Client和Targeter,如果尚未初始化会触发他们的实例化
// 在FeignClientAutoConfiguration中有Targeter的
protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
            HardCodedTarget<T> target) {
    // 从上下文中获取Client实例,默认为ApacheHttpClient
        Client client = getOptional(context, Client.class);
        if (client != null) {
      // 在Builder中放入Client
            builder.client(client);
      // 获取Client实例
            Targeter targeter = get(context, Targeter.class);
      // target()调用Builder.build()方法,然后调用newInstance方法生成Proxy
            return targeter.target(this, builder, context, target);
        }

        // ... 
    }

2.2、Targeter

Targeter是一个接口,它的target方法生成对应的实例对象。他有两个实现类,分别为DefaultTargeter和HystrixTargeter。OpenFeign使用HystrixTargeter这一层抽象来封装关于Hystrix的实现。

DefaultTargeter

DefaultTargeter只是调用了Feign.Builder的target方法。

class DefaultTargeter implements Targeter {
   @Override
   public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
         FeignContext context, Target.HardCodedTarget<T> target) {
      return feign.target(target);
   }
}

Feign.Builder#target()

public <T> T target(Target<T> target) {
  // newInstance()调用的是ReflectiveFeign的newInstance()
  return build().newInstance(target);
}

2.3、 重要的ReflectiveFeign

2.3.1、 newInstance()的两件事

newInstance()主要做了两件事

  1. 扫描了该FeignClient接口类中的函数信息,生成对应的MethodHandler。targetToHandlersByName.apply(target);
  2. 使用Proxy生成FeignClient的实例对象。
  • Contract

在生成对应的MethodHandler过程中,BaseContract的parseAndValidateMetadata方法会依次解析接口类的注解,函数注解和函数的参数注解,将这些注解包含的信息封装到MethodMetadata对象中,将@RequestMapping的uri拼接到函数的uri中等操作。

在MethodHandler中包含了SynchronizedMethodHandler,内含RestTemplate.Factory用来生成最后的请求RestTemplate。
@Override
public <T> T newInstance(Target<T> target) {
  // 这里生成所有方法MethodHandler
  // nameToHandler由Contract解析target得到
  // 在MethodHandler中存在对应方法的RestTemplate.Factory和SynchronizedMethodHandler
  Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
  Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
  List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();

  for (Method method : target.type().getMethods()) {
    if (method.getDeclaringClass() == Object.class) {
      continue;
    } else if (Util.isDefault(method)) {
      DefaultMethodHandler handler = new DefaultMethodHandler(method);
      defaultMethodHandlers.add(handler);
      methodToHandler.put(method, handler);
    } else {
      methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
    }
  }
  // 这里把target和MethodHandler的映射关系
  // 创建出一个FeignInvocationHandler
  InvocationHandler handler = factory.create(target, methodToHandler);
  // 使用Proxy生成FeignClient的实例对象。
  T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
      new Class<?>[] {target.type()}, handler);

  // 把defaultMethodHandler绑定到proxy
  for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
    defaultMethodHandler.bindTo(proxy);
  }
  return proxy;
}

2.3.2、第一件事:扫描了该FeignClient接口类中的函数信息

2.3.3、第二件事:使用Proxy生成FeignClient的实例对象

3、函数的调用和网络请求

OpenFeign网络请求流程图

3.1、SynchronousMethodHandler#invoke

调用RestTemplate.Factory的create方法生成RestTemplate方法。

@Override
public Object invoke(Object[] argv) throws Throwable {
  // 调用RestTemplate.Factory的create方法生成RestTemplate实例
  RequestTemplate template = buildTemplateFromArgs.create(argv);
  Retryer retryer = this.retryer.clone();
  while (true) {
    try {
      // Target将template转换为request实例
      // client调用request,返回response
      return executeAndDecode(template);
    } catch (RetryableException e) {
     // ...
    }
  }
}

3.2、RestTemplate.Factory#create

根据实际的调用参数和之前的RestTemplate结合,生成RestTemplate实例。

@Override
public RequestTemplate create(Object[] argv) {
  RequestTemplate mutable = RequestTemplate.from(metadata.template());
  if (metadata.urlIndex() != null) {
    int urlIndex = metadata.urlIndex();
    checkArgument(argv[urlIndex] != null, "URI parameter %s was null", urlIndex);
    mutable.target(String.valueOf(argv[urlIndex]));
  }
  Map<String, Object> varBuilder = new LinkedHashMap<String, Object>();
  // 遍历MethodMetadata中所有关于参数的索引及其对应名称的配置信息。
  for (Entry<Integer, Collection<String>> entry : metadata.indexToName().entrySet()) {
    int i = entry.getKey();
    Object value = argv[entry.getKey()];
    if (value != null) { // Null values are skipped.
      if (indexToExpander.containsKey(i)) {
        value = expandElements(indexToExpander.get(i), value);
      }
      for (String name : entry.getValue()) {
        varBuilder.put(name, value);
      }
    }
  }
    
  RequestTemplate template = resolve(argv, mutable, varBuilder);
  // 设置queryMap参数
  if (metadata.queryMapIndex() != null) {
    // add query map parameters after initial resolve so that they take
    // precedence over any predefined values
    Object value = argv[metadata.queryMapIndex()];
    // 调用Encoder进行入参解析
    Map<String, Object> queryMap = toQueryMap(value);
    template = addQueryMapQueryParameters(queryMap, template);
  }

  // 设置headerMap参数
  if (metadata.headerMapIndex() != null) {
    template =
        addHeaderMapHeaders((Map<String, Object>) argv[metadata.headerMapIndex()], template);
  }

  return template;
}

3.3、SynchronousMethodHandler#executeAndDecode

在这个流程里用到了Encoder、Decoder、ErrorDecoder组件;

Object executeAndDecode(RequestTemplate template) throws Throwable {
  // 1、通过RequestInterceptor机制,
  // 通过拦截器为每个请求增加固定的header信息
  Request request = targetRequest(template);
  Response response;
  long start = System.nanoTime();
  try {
    // 2、调用client的execute方法进行http请求
    response = client.execute(request, options);
  } catch (IOException e) {
    throw errorExecuting(request, e);
  }
  boolean shouldClose = true;
  try {
    if (Response.class == metadata.returnType()) {
      // ... 省略进行一些body校验的代码
      // Ensure the response body is disconnected
      byte[] bodyData = Util.toByteArray(response.body().asInputStream());
      return response.toBuilder().body(bodyData).build();
    }
    // http请求状态码为在[200, 300)代表调用成功
    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()) {
      // 调用异常,且返回类型为Void
      Object result = decode(response);
      shouldClose = closeAfterDecode;
      return result;
    } else {
      // 调用失败,errorDecoder组件解析返回信息
      throw errorDecoder.decode(metadata.configKey(), response);
    }
  } catch (IOException e) {
    throw errorReading(request, response, e);
  } finally {
    if (shouldClose) {
      ensureClosed(response.body());
    }
  }
}

RequestInterceptor

OpenFeign也提供了RequestInterceptor机制,在由RestTemplate生成Request的过程中,会调用所有的RequestInterceptor对RestTemplate进行处理。

Request targetRequest(RequestTemplate template) {
  // 使用请求拦截器为每个请求增加固定的header信息
  for (RequestInterceptor interceptor : requestInterceptors) {
    interceptor.apply(template);
  }
  return target.apply(template);
}

Client

Client用来进行真正的Http请求,前文提到当@FeignClient中有url时,getTarget()中会生成LoadBalancerFeignClient,Ribbon实现了负载均衡决定调用哪台机器。

// LoadBalancerFeignClient#execute
@Override
public Response execute(Request request, Request.Options options) throws IOException {
   try {
      URI asUri = URI.create(request.url());
      String clientName = asUri.getHost();
      URI uriWithoutHost = cleanUrl(request.url(), clientName);
        // 将request封装成了RibbonRequest
      FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
            this.delegate, request, uriWithoutHost);

      IClientConfig requestConfig = getClientConfig(options, clientName);
      // 这里涉及到Ribbon如何实现负载均衡,具体内容将在Ribbon源码中展开。
      return lbClient(clientName)
            .executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
   }
   catch (ClientException e) {
      // ... 不重要
   }
}

真正执行的Client有OkHttpClient和RibbonClient两个子类。OkhttpClient调用OkHttp的相关组件进行网络请求的发送。

参考

[Spring Cloud微服务架构进阶

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容