Spring Cloud 整合 Feign 的原理

前言

上篇 介绍了 Feign 的核心实现原理,在文末也提到了会再介绍其和 Spring Cloud 的整合原理,Spring 具有很强的扩展性,会把一些常用的解决方案通过 starter 的方式开放给开发者使用,在引入官方提供的 starter 后通常只需要添加一些注解即可使用相关功能(通常是 @EnableXXX)。下面就一起来看看 Spring Cloud 到底是如何整合 Feign 的。

整合原理浅析

在 Spring 中一切都是围绕 Bean 来展开的工作,而所有的 Bean 都是基于 BeanDefinition 来生成的,可以说 BeanDefinition 是整个 Spring 帝国的基石,这个整合的关键也就是要如何生成 Feign 对应的 BeanDefinition。

要分析其整合原理,我们首先要从哪里入手呢?如果你看过 上篇 的话,在介绍结合 Spring Cloud 使用方式的例子时,第二步就是要在项目的 XXXApplication 上加添加 @EnableFeignClients 注解,我们可以从这里作为切入点,一步步深入分析其实现原理(通常相当一部分的 starter 一般都是在启动类中添加了开启相关功能的注解)。

feign-1.png

进入 @EnableFeignClients 注解中,其源码如下:

feign-2.png

从注解的源码可以发现,该注解除了定义几个参数(basePackages、defaultConfiguration、clients 等)外,还通过 @Import 引入了 FeignClientsRegistrar 类,一般 @Import 注解有如下功能(具体功能可见 官方 Java Doc):

  • 声明一个 Bean
  • 导入 @Configuration 注解的配置类
  • 导入 ImportSelector 的实现类
  • 导入 ImportBeanDefinitionRegistrar 的实现类(这里使用这个功能

到这里不难看出,整合实现的主要流程就在 FeignClientsRegistrar 类中了,让我们继续深入到类 FeignClientsRegistrar 的源码,

feign-3.png

通过源码可知 FeignClientsRegistrar 实现 ImportBeanDefinitionRegistrar 接口,该接口从名字也不难看出其主要功能就是将所需要初始化的 BeanDefinition 注入到容器中,接口定义两个方法功能都是用来注入给定的 BeanDefinition 的,一个可自定义 beanName(通过实现 BeanNameGenerator 接口自定义生成 beanName 的逻辑),另一个使用默认的规则生成 beanName(类名首字母小写格式)。接口源码如下所示:

feign-4.png

对 Spring 有一些了解的朋友们都知道,Spring 会在容器启动的过程中根据 BeanDefinition 的属性信息完成对类的初始化,并注入到容器中。所以这里 FeignClientsRegistrar 的终极目标就是将生成的代理类注入到 Spring 容器中。

虽然 FeignClientsRegistrar 这个类的源码看起来比较多,但是从其终结目标来看,我们主要是看如何生成 BeanDefinition 的,通过源码可以发现其实现了 ImportBeanDefinitionRegistrar 接口,并且重写了 registerBeanDefinitions(AnnotationMetadata, BeanDefinitionRegistry) 方法,在这个方法里完成了一些 BeanDefinition 的生成和注册工作。源码如下:

feign-5.png

整个过程主要分为如下两个步骤:

  1. 给 @EnableFeignClients 的全局默认配置(注解的 defaultConfiguration 属性)创建 BeanDefinition 对象并注入到容器中(对应上图中的第 ① 步)
  2. 给标有了 @FeignClient 的类创建 BeanDefinition 对象并注入到容器中(对应上图中的第 ② 步)

下面分别深入方法源码实现来看其具体实现原理,首先来看看第一步的方法 registerDefaultConfiguration(AnnotationMetadata, BeanDefinitionRegistry),源码如下:

feign-6.png

可以看到这里只是获取一下注解 @EnableFeignClients 的默认配置属性 defaultConfiguration 的值,最终的功能实现交给了 registerClientConfiguration(BeanDefinitionRegistry, Object, Object) 方法来完成,继续跟进深入该方法,其源码如下:

feign-7.png

可以看到,全局默认配置的 BeanClazz 都是 FeignClientSpecification,然后这里将全局默认配置 configuration 设置为 BeanDefinition 构造器的输入参数,然后当调用构造器实例化时将这个参数传进去。到这里就已经把 @EnableFeignClients 的全局默认配置(注解的 defaultConfiguration 属性)创建出 BeanDefinition 对象并注入到容器中了,第一步到此完成,整体还是比较简单的。

下面再来看看第二步 给标有了 @FeignClient 的类创建 BeanDefinition 对象并注入到容器中 是如何实现的。深入第二步的方法 registerFeignClients(AnnotationMetadata, BeanDefinitionRegistry) 实现中,由于方法实现代码较多,使用截图会比较分散,所以用贴出源代码并在相关位置添加必要注释的方式进行:

public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    // 最终获取到有 @FeignClient 注解类的集合
    LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();
    // 获取 @EnableFeignClients 注解的属性 map
    Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
    // 获取 @EnableFeignClients 注解的 clients 属性
    final Class<?>[] clients = attrs == null ? null : (Class<?>[]) attrs.get("clients");
    if (clients == null || clients.length == 0) {  
        // 如果 @EnableFeignClients 注解未指定 clients 属性则扫描添加(扫描过滤条件为:标注有 @FeignClient 的类)
        ClassPathScanningCandidateComponentProvider scanner = getScanner();
        scanner.setResourceLoader(this.resourceLoader);
        scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
        Set<String> basePackages = getBasePackages(metadata);
        for (String basePackage : basePackages) {
            candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
        }
    }
    else {
        // 如果 @EnableFeignClients 注解已指定 clients 属性,则直接添加,不再扫描(从这里可以看出,为了加快容器启动速度,建议都指定 clients 属性)
        for (Class<?> clazz : clients) {
            candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
        }
    }

    // 遍历最终获取到的 @FeignClient 注解类的集合
    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 注解的属性值
            Map<String, Object> attributes = annotationMetadata
                    .getAnnotationAttributes(FeignClient.class.getCanonicalName());
            // 获取 clientName 的值,也就是在构造器的参数值(具体获取逻辑可以参见 getClientName(Map<String, Object>) 方法      
            String name = getClientName(attributes);
            // 同上文第一步最后调用的方法,注入 @FeignClient 注解的配置对象到容器中
            registerClientConfiguration(registry, name, attributes.get("configuration"));
            // 注入 @FeignClient 对象,该对象可以在其它类中通过 @Autowired 直接引入(e.g. XXXService)
            registerFeignClient(registry, annotationMetadata, attributes);
        }
    }
}

通过源码可以看到最后是通过方法 registerFeignClient(BeanDefinitionRegistry, AnnotationMetadata, Map<String, Object>) 注入的 @FeignClient 对象,继续深入该方法,源码如下:

feign-8.png

方法实现比较长,最终目标是构造出 BeanDefinition 对象,然后通过 BeanDefinitionReaderUtils.registerBeanDefinition(BeanDefinitionHolder, BeanDefinitionRegistry) 注入到容器中。其中关键的一步是从 @FeignClient 注解中获取信息并设置到 BeanDefinitionBuilder 中,BeanDefinitionBuilder 中注册的类是 FeignClientFactoryBean,这个类的功能正如它的名字一样是用来创建出 FeignClient 的 Bean 的,然后 Spring 会根据 FeignClientFactoryBean 生成对象并注入到容器中。

需要明确的一点是,实际上这里最终注入到容器当中的是 FeignClientFactoryBean 这个类,Spring 会在类初始化的时候会根据这个类来生成实例对象,就是调用 FeignClientFactoryBean.getObject() 方法,这个生成的对象就是我们实际使用的代理对象。下面再进入到类 FeignClientFactoryBean 的 getObject() 这个⽅法,源码如下:

feign-9.png

可以看到这个方法是直接调用的类中的另一个方法 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() {
    // 从 Spring 容器中获取 FeignContext Bean
    FeignContext context = beanFactory != null ? beanFactory.getBean(FeignContext.class)
            : applicationContext.getBean(FeignContext.class);
    // 根据获取到的 FeignContext 构建出 Feign.Builder         
    Feign.Builder builder = feign(context);

    // 注解 @FeignClient 未指定 url 属性 
    if (!StringUtils.hasText(url)) {
        // url 属性是固定访问某一个实例地址,如果未指定协议则拼接 http 请求协议
        if (!name.startsWith("http")) {
            url = "http://" + name;
        }
        else {
            url = name;
        }
        // 格式化 url
        url += cleanPath();
        // 生成代理和我们之前的代理一样,注解 @FeignClient 未指定 url 属性则返回一个带有负载均衡功能的客户端对象
        return (T) loadBalance(builder, context, new HardCodedTarget<>(type, name, url));
    }
    // 注解 @FeignClient 已指定 url 属性 
    if (StringUtils.hasText(url) && !url.startsWith("http")) {
        url = "http://" + url;
    }
    String url = this.url + cleanPath();
    // 获取一个 client
    Client client = getOptional(context, Client.class);
    if (client != null) {
        if (client instanceof FeignBlockingLoadBalancerClient) {
            // not load balancing because we have a url,
            // but Spring Cloud LoadBalancer is on the classpath, so unwrap
            // 这里没有负载是因为我们有指定了 url 
            client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
        }
        builder.client(client);
    }
    // 生成代理和我们之前的代理一样,最后被注入到 Spring 容器中
    Targeter targeter = get(context, Targeter.class);
    return (T) targeter.target(this, builder, context, new HardCodedTarget<>(type, name, url));
}

通过源码得知 FeignClientFactoryBean 继承了 FactoryBean,其方法 FactoryBean.getObject 返回的就是 Feign 的代理对象,最后这个代理对象被注入到 Spring 容器中,我们就通过 @Autowired 可以直接注入使用了。同时还可以发现上面的代码分支最终都会走到如下代码:

Targeter targeter = get(context, Targeter.class);
return targeter.target(this, builder, context, target);

点进去深入 targeter.target 的源码,可以看到实际上这里创建的就是一个代理对象,也就是说在容器启动的时候,会为每个 @FeignClient 创建了一个代理对象。至此,Spring Cloud 和 Feign 整合原理的核心实现介绍完毕。

总结

本文主要介绍了 Spring Cloud 整合 Feign 的原理。通过上文介绍,你已经知道 Srpring 会我们的标注的 @FeignClient 的接口创建了一个代理对象,那么有了这个代理对象我们就可以做增强处理(e.g. 前置增强、后置增强),那么你知道是如何实现的吗?感兴趣的朋友可以再翻翻源码寻找答案(温馨提示:增强逻辑在 InvocationHandler 中)。还有 Feign 与 Ribbon 和 Hystrix 等组件的协作,感兴趣的朋友可以自行下载源码学习了解。

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

推荐阅读更多精彩内容