美文网首页我的微服务
Feign源码解析二

Feign源码解析二

作者: 0爱上1 | 来源:发表于2019-06-11 16:39 被阅读77次

    前言

    上文中,我们搭建了两个服务,一个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类图
    UML.png

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

    • 重要属性(property
    // 资源加载器,可通过该资源加载器加载classpath下的所有文件
    private ResourceLoader resourceLoader;
    
    // 上下文环境,可通过该环境获取当前应用配置属性等
    private Environment environment;
    

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

    • 重要方法(Method
    // 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的方法时会发起远程调用

    相关文章

      网友评论

        本文标题:Feign源码解析二

        本文链接:https://www.haomeiwen.com/subject/ibgqfctx.html