Springboot Feign整合源码解析

一、简述

图片

从Spring官网的start示例可以看出,feign的使用仅仅是声明一个接口,然后使用spring mvc或者 JAX-RS的方式给方法打上注解,调用方就可以像使用本地Service那样,依赖并调用了。

了解Spring的同学应该都知道,定义一个接口,没有实现类,想要把这个接口直接注入到Spring容器是不可能的,肯定是用了动态代理生成代理类,并且改变了Spring IOC的行为,将Bean的实例用动态代理类填充了。

那么接下来,我们就从入口,通过源码一步步来解析Feign和Springboot是怎么整合在一起的。

二、FeignClient是怎么实例化到Spring容器的

1、EnableFeignClients 注解

如果我们要开启Feign接口的扫描,会在我们项目启动类上,加上一个@EnableFeignClients的注解,这个大家都会用,但是为什么加了注解,Spring就会去扫描呢?

EnableFeignClients注解内容:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {}

这个注解还有一个Import注解,我们先看Import注解的说明:

/**
 * Indicates one or more {@link Configuration @Configuration} classes to import.
 *
 * <p>Provides functionality equivalent to the {@code <import/>} element in Spring XML.
 * Allows for importing {@code @Configuration} classes, {@link ImportSelector} and
 * {@link ImportBeanDefinitionRegistrar} implementations, as well as regular component
 * classes (as of 4.2; analogous to {@link AnnotationConfigApplicationContext#register}).
 *
 * <p>{@code @Bean} definitions declared in imported {@code @Configuration} classes should be
 * accessed by using {@link org.springframework.beans.factory.annotation.Autowired @Autowired}
 * injection. Either the bean itself can be autowired, or the configuration class instance
 * declaring the bean can be autowired. The latter approach allows for explicit, IDE-friendly
 * navigation between {@code @Configuration} class methods.
 *
 * <p>May be declared at the class level or as a meta-annotation.
 *
 * <p>If XML or other non-{@code @Configuration} bean definition resources need to be
 * imported, use the {@link ImportResource @ImportResource} annotation instead.
 *
 * @author Chris Beams
 * @author Juergen Hoeller
 * @since 3.0
 * @see Configuration
 * @see ImportSelector
 * @see ImportResource
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {

它注释其中有一句:

Allows for importing {@code @Configuration} classes, {@link ImportSelector} and {@link ImportBeanDefinitionRegistrar} implementations, as well as regular component classes (as of 4.2; analogous to {@link AnnotationConfigApplicationContext#register}).

说明可以通过@Import注解里,传入ImportSelector.class和ImportBeanDefinitionRegistrar.class的实现类,实现Bean的注册。
如何证明?
在SpringBoot启动过程中,实际上调用了Spring的核心方法AbstractApplicationContext#refresh()。

image

在refresh方法中,Spring在初始化Bean工厂的时候,会执行ConfigurationClassPostProcessor这个后置处理器,在这个后置处理器中,就会执行ImportSelector或者ImportBeanDefinitionRegistrar实现类的接口实现方法,生成BeanDefinition,后续会被Spring实例化。而且基本我们用到的组件@Enablexxxx,都是应用这个原理,把一个组件的初始化类用@Import来给注解打tag,再将初始化类实现ImportSelector或者ImportBeanDefinitionRegistrar作为value设置到注解中,让Spring自己去调用实例方法,这样就可以用一个自定义注解@Enablexxxx一键开启或者关闭某个组件了,理解了这个,我们应该知道给SpringBoot写一个小组件应该怎么开始了。

这一块的源码不做细讲,因为refresh方法太复杂了,篇幅有限,有兴趣的可以调试 ConfigurationClassPostProcessor这个后置处理器postProcessBeanFactory方法。

2、FeignClientsRegistrar

我们回到ImportBeanDefinitionRegistrar的实现类FeignClientsRegistrar:

@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,BeanDefinitionRegistry registry) {
   registerDefaultConfiguration(metadata, registry);
   registerFeignClients(metadata, registry);
}

在registerFeignClients方法中:
首先确定哪些类需要被扫描

image.png

如果@EnableFeignClients 中的clients属性配置了有值,其它的属性的配置就不会生效。

然后循环这些包路径,找到对应的有@FeignClient标注的类,执行registerFeignClient方法:

image
图片

这里最重要的就是:

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

3、FeignClientFactoryBean

FactoryBean是一个接口,其中有一个getObject方法,在执行AbstractBeanFactory#getBean方法时,如果你的name不加上"&"前缀的话,会得到这个getObject方法返回的对象实例。所以我们如果要找Feign的实例,就要去看FeignClientFactoryBean 这个类的getObject方法:

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

在getTarget()方法中:

image

会判断@FeignClient注解的url属性有没有值,我们一般是写服务名,这样就会最终生成”http://service-name“的url,然后再执行loadBalance方法。

protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
      HardCodedTarget<T> target) {
   Client client = getOptional(context, Client.class);
   if (client != null) {
      builder.client(client);
      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?");
}

这里有两个比较看不懂的变量,Client client和Targeter targeter,client变量主要用于设置请求客户端对象,其中客户端的负载均衡就是在这里初始化的,这个留在下一节单独讲。target是用于生成代理对象,如果这个Feign有配置熔断器,也会在这里初始化熔断器相关信息。

4、Targeter,Springboot自动装配

但是Targeter接口有两个实现类,那么在这里,会返回哪一个呢?


图片

这个就要回到我们Springboot的自动装配知识点了。

图片

我们知道,在Springboot启动的过程中,会调用AbstractApplicationContext#refresh()方法,也知道@Import直接的作用,在我们Springboot启动类中,会定义一个注解: @SpringBootApplication,这个注解上面再有@EnableAutoConfiguration注解,@EnableAutoConfiguration注解上面有一个@Import(AutoConfigurationImportSelector.class),

@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
   if (!isEnabled(annotationMetadata)) {
      return NO_IMPORTS;
   }
   AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
         .loadMetadata(this.beanClassLoader);
   AnnotationAttributes attributes =getAttributes(annotationMetadata);
   //在这里扫描classpath下所有 META-INF/spring.factories 中的
  //org.springframework.boot.autoconfigure.EnableAutoConfiguration 配置,
  //并让Spring实例化其配置的所有类
   List<String> configurations = getCandidateConfigurations(annotationMetadata,
         attributes);
   configurations = removeDuplicates(configurations);
   Set<String> exclusions = getExclusions(annotationMetadata, attributes);
   checkExcludedClasses(configurations, exclusions);
   configurations.removeAll(exclusions);
   configurations = filter(configurations, autoConfigurationMetadata);
   fireAutoConfigurationImportEvents(configurations, exclusions);
   return StringUtils.toStringArray(configurations);
}

从上面的代码注释可以看出,这个类会扫描classpath下所有 META-INF/spring.factories 中的org.springframework.boot.autoconfigure.EnableAutoConfiguration 配置,并让Spring实例化其配置的所有类。所以org.springframework.cloud.openfeign.FeignAutoConfiguration在Spring容器启动的时候就会被加载到容器并实例化。

image

使用哪个Targeter,取决于feign.hystrix.HystrixFeign这个类在不在classpath中。

我的工程是引入了这个jar:io.github.openfeign,所以就会使用HystrixTargeter。

进入HystrixTargeter#target方法:

图片

在这个方法里,首先判断@FeignClient注解有没有配置fallback属性,再判断fallbackFactory属性,如果都没有配置,就直接调用feign.target(target)。这里我们假设都没有配置,进入Feign.target()方法:

public <T> T target(Target<T> target) {
  return build().newInstance(target);
}

public Feign build() {
  SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
      new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,logLevel, decode404, closeAfterDecode);
  ParseHandlersByName handlersByName =
new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,errorDecoder, synchronousMethodHandlerFactory);
  return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
}

@Override
public <T> T newInstance(Target<T> target) {
//
  Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
  Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
  List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
  // 将@FeignClient接口中所有的方法存在methodToHandler这个Map中
  for (Method method : target.type().getMethods()) {
    if (method.getDeclaringClass() == Object.class) {
      continue;
    } else if(Util.isDefault(method)) {
      //Object默认的方法,会用默认的DefaultMethodHandler
      DefaultMethodHandler handler = new DefaultMethodHandler(method);
      defaultMethodHandlers.add(handler);
      methodToHandler.put(method, handler);
    } else {
      //这里通过看上面的build方法,可以知道,value是SynchronousMethodHandler
      methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
    }
  }
  //这里如果没有配置fallBack或者fallBackFactory,会返FeignInvocationHandler
  InvocationHandler handler = factory.create(target, methodToHandler);
  //对被@FeignClient注解的类利用JDK动态代理产生代理对象实例,并使用FeignInvocationHandler对这个对象进行增强
  T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[]{target.type()}, handler);

  for(DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
    defaultMethodHandler.bindTo(proxy);
  }
  return proxy;
}

5、FeignInvocationHandler

上面的代码描述了产生代理类的过程,当一个Feign接口被调用时,接下来我们就要看FeignInvocationHandler的invoke方法了:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  if ("equals".equals(method.getName())) {
    try {
      Object
          otherHandler =
          args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
      return equals(otherHandler);
    } catch (IllegalArgumentException e) {
      return false;
    }
  } else if ("hashCode".equals(method.getName())) {
    return hashCode();
  } else if ("toString".equals(method.getName())) {
    return toString();
  }
  //这里的dispatch就是我们定义的methodToHandler这个map,直接拿出SynchronousMethodHandler执行invoke方法
  return dispatch.get(method).invoke(args);
}

SynchronousMethodHandler#invoke

@Override
public Object invoke(Object[] argv) throws Throwable {
  RequestTemplate template = buildTemplateFromArgs.create(argv);
  Retryer retryer = this.retryer.clone();
  while (true) {
    try {  
      //这里就是真正执行http请求和负载均衡的地方了
      return executeAndDecode(template);
    } catch (RetryableException e) {
      retryer.continueOrPropagate(e);
      if (logLevel != Logger.Level.NONE) {
        logger.logRetry(metadata.configKey(), logLevel);
      }
      continue;
    }
  }
}

到此为止,@FeignClient注解的类,怎么会被实例化,并且注入到Spring容器中,已经完成。

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

推荐阅读更多精彩内容