我的微服务

Feign源码解析二

2019-06-11  本文已影响77人  0爱上1

前言

上文中,我们搭建了两个服务,一个user-service,一个order-service,来模拟利用FeignCliet发起远程调用的实现

本文会基于Feign源码,看看Feign到底是怎么实现远程调用

源码

@EnableFeignClients注解

上文中,我们的user-service服务需要调用远程的order-service服务完成一定的业务逻辑,而基本实现是order-service提供一个spi的jar包给user-service依赖,并且在user-service的启动类上添加了一个注解

这个注解就是@EnableFeignClients,接下来我们就从这个注解入手,一步一步解开Feign的神秘面纱

/**
 * Scans for interfaces that declare they are feign clients (via
 * {@link org.springframework.cloud.openfeign.FeignClient} <code>@FeignClient</code>).
 * Configures component scanning directives for use with
 * {@link org.springframework.context.annotation.Configuration}
 * <code>@Configuration</code> classes.
 *
 * @author Spencer Gibb
 * @author Dave Syer
 * @since 1.0
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
    ...
}

该注解类上的注释大概的意思就是:
扫描那些被声明为Feign Clients(只要有org.springframework.cloud.openfeign.FeignClient注解修饰的接口都是Feign Clients接口)的接口

  1. basePackages
    值类型:String[]
    规定了扫描的基础包位置
  2. clients
    值类型:Class<?>[]规定了扫描带有@FeignClient注解的指定类全路径Class的集合

下面我们继续追踪源码,看看到底什么地方用到了这个注解
利用IDEA的查找调用链快捷键,可以发现在.class类型的文件中只有一个文件用到了这个注解

FeignClientsRegistrar.png

OK,下面主要就是看这个类做了什么


FeignClientsRegistrar.class

UML.png

通过UML图我们发现该类分别实现了ImportBeanDefinitionRegistrarResourceLoaderAware以及EnvironmentAware接口
这三个接口均是spring-framework框架的spring-context模块下的接口,都是和spring上下文相关,具体作用下文会分析

// 资源加载器,可通过该资源加载器加载classpath下的所有文件
private ResourceLoader resourceLoader;

// 上下文环境,可通过该环境获取当前应用配置属性等
private Environment environment;

总结下来就是利用这两个重要属性,一个获取应用配置属性,一个可以加载classpath下的文件,那么FeignClientsRegistrar持有这两个东西之后要做什么呢?

// 1. 初始化当前上下文环境属性
@Override
public void setEnvironment(Environment environment) {
    this.environment = environment;
}

// 2. 初始化资源加载器属性
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
    this.resourceLoader = resourceLoader;
}

// 3. 最重要的一个来了,注册bean定义
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    // 注册默认配置
    registerDefaultConfiguration(metadata,registry);
    // 注册FeignClients
    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拼接开头
                name = "default." + metadata.getClassName();
            }
            registerClientConfiguration(registry, name,
                    defaultAttrs.get("defaultConfiguration"));
        }
    }
  1. 这里的注册默认配置方法,读取启动类上面 @EnableFeignClients注解中的defaultConfiguration,默认name为default,一般情况下无需配置。用默认的FeignAutoConfiguration即可。 上面有个比较重要的方法:注册配置registerClientConfiguration(...),启动流程一共有两处读取feign的配置,这是第一处。根据该方法看一下
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
            Object configuration) {
        BeanDefinitionBuilder builder = BeanDefinitionBuilder
                .genericBeanDefinition(FeignClientSpecification.class);
        builder.addConstructorArgValue(name);
        builder.addConstructorArgValue(configuration);
        registry.registerBeanDefinition(
                name + "." + FeignClientSpecification.class.getSimpleName(),
                builder.getBeanDefinition());
    }

上面将bean配置类包装成FeignClientSpecification,注入到容器。该对象非常重要,包含FeignClient需要的重试策略超时策略日志等配置,如果某个FeignClient服务没有设置独立的配置类,则读取默认的配置,可以将这里注册的bean理解为整个应用中所有feign的默认配置


由于FeignClientsRegistrar实现了ImportBeanDefinitionRegistrar接口,这里简单提下这个接口的作用
我们知道在spring框架中,我们如果想注册一个bean的话主要由两种方式:自动注册/手动注册

  1. 类上增加@Component,@Service,@Controller等注解,由Spring自动帮我们注册成bean

  2. 配置文件(.xml中增加<bean>...</bean>标签)或配置类(@Configuration)中增加@Bean注解的类,也会由Spring帮我们自动注册成bean

  3. 通过实现ImportBeanDefinitionRegistrar接口,并实现其registerBeanDefinitions方法,手动注册bean

知道了ImportBeanDefinitionRegistrar接口的作用,下面就来看下FeignClientsRegistrar类是何时被加载实例化的

通过IDEA工具搜索引用链,发现该类是在注解@EnableFeignClients上被import进来的,文章开始的图片中有

@Import(FeignClientsRegistrar.class)

这里提下@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 {

/**
* {@link Configuration}, {@link ImportSelector}, {@link ImportBeanDefinitionRegistrar}
* or regular component classes to import.
*/
Class<?>[] value();
}

该注解仅有一个属性value,使用该注解表明导入一个或者多个@Configuration类,其作用和.xml文件中的<import>等效,其允许导入@Configuration类,ImportSelector接口/ImportBeanDefinitionRegistrar接口的实现,也同样可以导入一个普通的组件类

注意,如果是XML或非@Configuration的bean定义资源需要被导入的话,需要使用@ImportResource注解代替

这里我们导入的FeignClientsRegistrar类正是一个ImportBeanDefinitionRegistrar接口的实现

FeignClientsRegistrar重写了该接口的registerBeanDefinitions方法,该方法有两个参数注解元数据metadata和bean定义注册表registry

该方法会由spring负责调用,继而注册所有标注为@FeignClient注解的bean定义


registerFeignClients(...)

下面看registerBeanDefinitions方法中的第二个方法,在该方法中完成了所有@FeignClient注解接口的扫描工作,以及注册到spring中,注意这里注册bean的类型为FeignClientFactoryBean,下面细说

public void registerFeignClients(AnnotationMetadata metadata,
            BeanDefinitionRegistry registry) {
    // 获取ClassPath扫描器
    ClassPathScanningCandidateComponentProvider scanner = getScanner();
    // 为扫描器设置资源加载器
    scanner.setResourceLoader(this.resourceLoader);

    Set<String> basePackages;
    // 1. 从@EnableFeignClients注解中获取到配置的各个属性值
    Map<String, Object> attrs = metadata
            .getAnnotationAttributes(EnableFeignClients.class.getName());
    // 2. 注解类型过滤器,只过滤@FeignClient   
    AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
            FeignClient.class);
    // 3. 从1. 中的属性值中获取clients属性的值        
    final Class<?>[] clients = attrs == null ? null
            : (Class<?>[]) attrs.get("clients");
    if (clients == null || clients.length == 0) {
        // 扫描器设置过滤器且获取需要扫描的基础包集合
        scanner.addIncludeFilter(annotationTypeFilter);
        basePackages = getBasePackages(metadata);
    }
    else {
        // clients属性值不为null,则将其clazz路径转为包路径
        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)));
    }

    // 3. 扫描基础包,且满足过滤条件下的接口封装成BeanDefinition
    for (String basePackage : basePackages) {
        Set<BeanDefinition> candidateComponents = scanner
                .findCandidateComponents(basePackage);
        // 遍历扫描到的bean定义        
        for (BeanDefinition candidateComponent : candidateComponents) {
            if (candidateComponent instanceof AnnotatedBeanDefinition) {
                // 并校验扫描到的bean定义类是一个接口
                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());

                String name = getClientName(attributes);
                // 可以看到这里也注册了一个FeignClient的配置bean
                registerClientConfiguration(registry, name,
                        attributes.get("configuration"));
                // 注册bean定义到spring中
                registerFeignClient(registry, annotationMetadata, attributes);
            }
        }
    }
}

总结一下该方法,就是扫描@EnableFeignClients注解上指定的basePackage或clients值,获取所有@FeignClient注解标识的接口,然后将这些接口一一调用以下两个重要方法完成注册configuration配置bean和注册FeignClient bean

// ...省略部分代码
Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(FeignClient.class.getCanonicalName());
// 获取@FeignClient注解中指定的name
String name = getClientName(attributes);
// 1. 获取@FeignClient注解中指定的configuration,并以FeignClientSpecification类注册bean
registerClientConfiguration(registry, name, attributes.get("configuration"));
// 2. 注册FeignClient bean,以FeignClientFactoryBean类注册
registerFeignClient(registry, annotationMetadata, attributes);
  1. 可以理解为每个单独的FeignClient接口都会注册一个自己的configuration bean定义到spring的beanDefinitionMap中,key是@FeignClient注解上指定的serviceId/name/value值,value则是一个类型为FeignClientSpecification的BeanDefinition
  1. 中完成了每个FeignClient接口的client bean的注册,beanDefinitionMap中的key是@FeignClient注解的每个接口的全限定名,value则是类型是FeignClientFactoryBean的BeanDefinition
registerFeignClient.png

断点位置相当重要

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

这里是利用了spring的代理工厂来生成代理类,即这里将所有的 feignClient的描述信息BeanDefinition设定为 FeignClientFactoryBean类型,该类继承自FactoryBean,因此这是一个代理类,FactoryBean是一个工厂bean,用作创建代理bean,所以得出结论,feign将所有的feignClient bean定义的类型包装成 FeignClientFactoryBean

最终其实就是存入了BeanFactory的beanDefinitionMap中

DefaultListableBeanFactory.png

那么代理类什么时候会触发生成呢? 在spring刷新容器时,会根据beanDefinition去实例化bean,如果beanDefinition的beanClass类型为代理bean,则会调用其T getObject() throws Exception;方法生成代理bean,而我们实际利用注入进来的FeignClient接口就是这些一个个代理类


总结

这里有坑...

这里有一个需要注意的点,也是开发中会遇到的一个启动报错点
如果我们同时定义了两个不同名称的接口(同一个包下/或依赖方指定全部扫描我们提供的@FeignClient),且这两个@FeignClient接口注解的value/name/serviceId值一样的话,依赖方拿到我们的提供的spi依赖,启动类上@EnableFeignClients注解扫描能同时扫描到这两个接口,就会启动报错

原因就是Feign会为每个@FeignClient注解标识的接口都注册一个以serviceId/name/value为key,FeignClientSpecification类型的bean定义为value去spring注册bean定义,又默认不允许覆盖bean定义,所以报错

报错信息.png

官方提示给出的解决方法要么改个@FeignClient注解的serviceId,name,value属性值,要么就开启spring允许bean定义覆写

spring.main.allow-bean-definition-overriding=true

至此我们知道利用在springboot的启动类上添加的@EnableFeignClients注解,该注解中import进来了一个手动注册bean的FeignClientsRegistrar注册器,该注册器会由spring加载其registerBeanDefinitions方法,由此来扫描所有@EnableFeignClients注解定义的basePackages包路径下的所有标注为@FeignClient注解的接口,并将其注册到spring的bean定义Map中,并实例化bean

下一篇博文中,我会分析为什么我们在调用(@Resource)这些由@FeignClient注解的bean的方法时会发起远程调用

上一篇下一篇

猜你喜欢

热点阅读