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 一般都是在启动类中添加了开启相关功能的注解)。

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

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

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

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

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

Spring 有一些了解的朋友们都知道, Spring 会在容器启动的过程中根据 BeanDefinition 的属性信息完成对类的初始化,并注入到容器中。所以这里 FeignClientsRegistrar 的终极目标就是 将生成的代理类注入到 Spring 容器中。
虽然 FeignClientsRegistrar 这个类的源码看起来比较多,但是从其终结目标来看,我们主要是看如何生成 BeanDefinition 的,通过源码可以发现其实现了 ImportBeanDefinitionRegistrar 接口,并且重写了 registerBeanDefinitions(AnnotationMetadata, BeanDefinitionRegistry) 方法,在这个方法里完成了一些 BeanDefinition 的生成和注册工作。源码如下:

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

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

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

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

可以看到,全局默认配置的 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 对象,继续深入该方法,源码如下:

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

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

可以看到这个方法是直接调用的类中的另一个方法 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 CloudFeign 整合原理的核心实现介绍完毕。

总结

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


原文:Spring Cloud 整合 Feign 的原理 | mghio's Blog
作者: mghio