云社区 博客 博客详情

SpringCloud系列:FeignClient源码深度解析

云享专家 发表于 2019-09-05 09:41:22 2019-09-05
0
0

【摘要】 先加载每个服务的配置类,然后加载启动类注解上的配置类,最后加载默认的配置类。这样做有什么好处?spring刷新容器的方法也是对所有的bean进行了缓存,如果已经创建,则不再实例化。所以优先选取每个FeignClient的配置类,最后默认的配置类兜底。所以这也证明了 sleuth的配置一定在 feign的配置类之前加载。至此, FeignBuilder构造流程结束。

概述

springCloud feign主要对netflix feign进行了增强和包装,本篇从源码角度带你过一遍装配流程,揭开feign底层的神秘面纱。主要包括feign整合ribbonhystrixsleuth,以及生成的代理类最终注入到spring容器的过程。篇幅略长,耐心读完,相信你会有所收获。


Feign架构图

一些核心类及大致流程:

1.png

 

大体步骤: 一、注册FeignClient配置类和FeignClient BeanDefinition 二、实例化Feign上下文对象FeignContext 三、创建Feign.builder 对象四、生成负载均衡代理类五、生成默认代理类六、注入到spring容器


源码分析

主要围绕上面6个步骤详细分析。


一、注册FeignClient配置类和FeignClient BeanDefinition

从启动类注解开始,来看下 @EnableFeignClients注解:

1.   @EnableFeignClients

2.   publicclassMyApplication{

3.   }

这是在启动类开启feign装配的注解,跟进该注解,看看做了什么:

1.   @Import(FeignClientsRegistrar.class)

1.   publicclassFeignClientsRegistrarimplementsImportBeanDefinitionRegistrar,

2.           ResourceLoaderAwareBeanClassLoaderAware{

3.

4.       // patterned after Spring Integration IntegrationComponentScanRegistrar

5.       // and RibbonClientsConfigurationRegistgrar

6.       privatefinalLoggerlogger = LoggerFactory.getLogger(FeignClientsRegistrar.class);

7.       privateResourceLoaderresourceLoader;

8.

9.       privateClassLoaderclassLoader;

10.

11.    publicFeignClientsRegistrar() {

12.    }

13.

14.    @Override

15.    publicvoidsetResourceLoader(ResourceLoaderresourceLoader) {

16.        this.resourceLoader = resourceLoader;

17.    }

18.

19.    @Override

20.    publicvoidsetBeanClassLoader(ClassLoaderclassLoader) {

21.        this.classLoader = classLoader;

22.    }

23.

24.    @Override

25.    publicvoidregisterBeanDefinitions(AnnotationMetadatametadata,

26.            BeanDefinitionRegistryregistry) {

27.        //1、先注册默认配置

28.        registerDefaultConfiguration(metadata, registry);

29.        //2、注册所有的feignClient beanDefinition

30.        registerFeignClients(metadata, registry);

31.    }

32.    //...

33.}

我们分别来看一下上面 registerBeanDefinitions中的两个方法: 1注册默认配置方法: registerDefaultConfiguration:

1.       privatevoidregisterDefaultConfiguration(AnnotationMetadatametadata,

2.               BeanDefinitionRegistryregistry) {

3.           Map<StringObject> defaultAttrs = metadata

4.                   .getAnnotationAttributes(EnableFeignClients.class.getName(),true);

5.

6.           if(defaultAttrs != null&& defaultAttrs.containsKey("defaultConfiguration")) {

7.               Stringname;

8.               if(metadata.hasEnclosingClass()) {

9.                   name = "default."+ metadata.getEnclosingClassName();

10.            }

11.            else{

12.                name = "default."+ metadata.getClassName();

13.            }

14.            // name 默认以default 开头,后续会根据名称选择配置

15.            registerClientConfiguration(registry, name,

16.                    defaultAttrs.get("defaultConfiguration"));

17.        }

18.    }

上述方法为读取启动类上面 @EnableFeignClients注解中声明feign相关配置类,默认namedefault,一般情况下无需配置。用默认的 FeignAutoConfiguration即可。上面有个比较重要的方法:注册配置 registerClientConfiguration,启动流程一共有两处读取feign的配置类,这是第一处。根据该方法看一下:

1.       privatevoidregisterClientConfiguration(BeanDefinitionRegistryregistry, Objectname,

2.               Objectconfiguration) {

3.           BeanDefinitionBuilderbuilder = BeanDefinitionBuilder

4.                   .genericBeanDefinition(FeignClientSpecification.class);

5.           builder.addConstructorArgValue(name);

6.           builder.addConstructorArgValue(configuration);

7.           registry.registerBeanDefinition(

8.                   name + "."+FeignClientSpecification.class.getSimpleName(),

9.                   builder.getBeanDefinition());

10.    }

上面将bean配置类包装成 FeignClientSpecification,注入到容器。该对象非常重要,包含FeignClient需要的重试策略,超时策略,日志等配置,如果某个服务没有设置,则读取默认的配置。

2、扫描FeignClient

该方法主要是扫描类路径,对所有的FeignClient生成对应的 BeanDefinition:

1.   publicvoidregisterFeignClients(AnnotationMetadatametadata,

2.               BeanDefinitionRegistryregistry) {

3.

4.           //...

5.           //获取扫描目录下面所有的bean deanDefinition

6.           for(StringbasePackage : basePackages) {

7.               Set<BeanDefinition> candidateComponents = scanner

8.                       .findCandidateComponents(basePackage);

9.               for(BeanDefinitioncandidateComponent : candidateComponents) {

10.                if(candidateComponent instanceofAnnotatedBeanDefinition) {

11.                    // verify annotated class is an interface

12.                    AnnotatedBeanDefinitionbeanDefinition = (AnnotatedBeanDefinition) candidateComponent;

13.                    AnnotationMetadataannotationMetadata = beanDefinition.getMetadata();

14.                    Assert.isTrue(annotationMetadata.isInterface(),

15.                            "@FeignClient can only be specified on an interface");

16.

17.                    Map<StringObject> attributes = annotationMetadata

18.                            .getAnnotationAttributes(

19.                                    FeignClient.class.getCanonicalName());

20.

21.                    Stringname = getClientName(attributes);

22.                    //这里是第二处

23.                    registerClientConfiguration(registry, name,

24.                            attributes.get("configuration"));

25.

26.                    //注册feignClient

27.                    registerFeignClient(registry, annotationMetadata, attributes);

28.                }

29.            }

30.        }

31.    }

可以看到上面又调用了 registerClientConfiguration注册配置的方法,这里是第二处调用。这里主要是将扫描的目录下,每个项目的配置类加载的容器当中。 注册到容器中,什么时候会用到呢?具体又如何使用呢?别着急,后面会有介绍。

我们先会回到继续主流程,继续看注册feignClient的方法,跟进 registerFeignClient

1.   privatevoidregisterFeignClient(BeanDefinitionRegistryregistry,

2.               AnnotationMetadataannotationMetadata, Map<StringObject> attributes) {

3.           StringclassName = annotationMetadata.getClassName();

4.           //声明代理类名称

5.           BeanDefinitionBuilderdefinition = BeanDefinitionBuilder

6.                   .genericBeanDefinition(FeignClientFactoryBean.class);

7.           //logger.info("TEX do some replacement");

8.               //attributes.put("value", ((String)attributes.get("value")).replace('_','-'));

9.           validate(attributes);

10.        definition.addPropertyValue("url", getUrl(attributes));

11.        definition.addPropertyValue("path", getPath(attributes));

12.        Stringname = getName(attributes);

13.        definition.addPropertyValue("name", name);

14.        definition.addPropertyValue("type", className);

15.        definition.addPropertyValue("decode404", attributes.get("decode404"));

16.        definition.addPropertyValue("fallback", attributes.get("fallback"));

17.        definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);

18.

19.        Stringalias= name + "FeignClient";

20.        AbstractBeanDefinitionbeanDefinition = definition.getBeanDefinition();

21.        beanDefinition.setPrimary(true);

22.        BeanDefinitionHolderholder = newBeanDefinitionHolder(beanDefinition, className,

23.                newString[] { alias});

24.        //bean definition加入到spring容器

25.        BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);

26.    }

划重点,上面出现了一行相当关键代码:

1.   BeanDefinitionBuilderdefinition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);

springCloud FeignClient其实是利用了spring的代理工厂来生成代理类,所以这里将所有的 feignClient的描述信息 BeanDefinition设定为 FeignClientFactoryBean类型,该类又继承 FactoryBean,很明显,这是一个代理类。spring中, FactoryBean是一个工厂bean,用作创建代理bean,所以得出结论,feign将所有的feignClient bean包装成 FeignClientFactoryBean。扫描方法到此结束。

代理类什么时候会触发生成呢?spring刷新容器时,当实例化我们的业务service时,如果发现注册了FeignClientspring就会去实例化该FeignClient,同时会进行判断是否是代理bean,如果为代理bean,则调用 FeignClientFactoryBean T getObject() throws Exception;方法生成代理bean


先来隆重介绍一下 FeignClientFactoryBean,后面四步都基于此类。

先看一下代理feignClient代理生成入口: getObject方法:

1.   @Override

2.       publicObjectgetObject() throwsException{

3.           //二、实例化Feign上下文对象FeignContext

4.           FeignContextcontext = applicationContext.getBean(FeignContext.class);

5.           //三、生成builder对象,用来生成feign

6.           Feign.Builderbuilder = feign(context);

7.

8.           //判断生成的代理对象类型,如果url为空,则走负载均衡,生成有负载均衡功能的代理类

9.           if(!StringUtils.hasText(this.url)) {

10.            Stringurl;

11.            if(!this.name.startsWith("http")) {

12.                url = "http://"+this.name;

13.            }

14.            else{

15.                url = this.name;

16.            }

17.            url += cleanPath();

18.            //四、生成负载均衡代理类

19.            returnloadBalance(builder, context, newHardCodedTarget<>(this.type,

20.                    this.name, url));

21.        }

22.        //如果指定了url,则生成默认的代理类

23.        if(StringUtils.hasText(this.url) && !this.url.startsWith("http")) {

24.            this.url = "http://"+this.url;

25.        }

26.        Stringurl = this.url + cleanPath();

27.        //五、生成默认代理类

28.        returntargeter.target(this, builder, context, newHardCodedTarget<>(

29.                this.type,this.name, url));

30.    }

getObject()逻辑比较多,每一行都会做一些初始化配置,来逐步分析。

二、实例化Feign上下文对象FeignContext

上述方法中第一行便是实例化 FeignContext

1.   FeignContextcontext = applicationContext.getBean(FeignContext.class);

获取 FeignContext对象,如果没有实例化,则主动实例化,如下:

屏幕快照 2019-09-05 09.35.58.png

可以看到feign的配置类设置到feign的容器当中,而集合中的元素正是上面我们提到的两处调用 registerClientConfiguration方法添加进去的,前后呼应。

然而,当我们引入了 sleuth之后,获取的 feignContext确是 TraceFeignClientAutoConfiguration中配置的实例 sleuthFeignContext:

2.png

 

可以看到上面创建了一个 TraceFeignContext实例,因为该对象继承 FeignContext,同时又加了 @Primary注解,所以在上面第2步中通过类型获取applicationContext.getBean(FeignContext.class);,最终拿到的是 TraceFeignContext


三、构造 FeignBuilder

继续跟进该方法:

屏幕快照 2019-09-05 09.38.29.png屏幕快照 2019-09-05 09.38.43.png屏幕快照 2019-09-05 09.39.02.png

上述代码有两处逻辑,分别来看:

1 Feign.Builder builder = get(context, Feign.Builder.class) ,又会有以下三种情况:

1)单独使用Feign,没有引入 sleuth hystrix通过加载FeignClientsConfiguration的配置创建 Feign的静态内部类: Feign.Builder

1.       @Bean

2.       @Scope("prototype")

3.       @ConditionalOnMissingBean

4.       publicFeign.BuilderfeignBuilder(Retryerretryer) {

5.           returnFeign.builder().retryer(retryer);

6.       }

2)引入了 hystrix,没有引入 sleuth通过加载 FeignClientsConfiguration的配置创建 HystrixFeign的静态内部类: HystrixFeign.Builder

1.       @Configuration

2.       @ConditionalOnClass({HystrixCommand.classHystrixFeign.class})

3.       protectedstaticclassHystrixFeignConfiguration{

4.           @Bean

5.           @Scope("prototype")

6.           @ConditionalOnMissingBean

7.           @ConditionalOnProperty(name = "feign.hystrix.enabled", matchIfMissing = false)

8.           publicFeign.BuilderfeignHystrixBuilder() {

9.               returnHystrixFeign.builder();

10.        }

11.    }

3)同时引入 hystrix  sleuth加载 TraceFeignClientAutoConfiguration的配置创建: HystrixFeign.Builder3.png

注意:

·      TraceFeignClientAutoConfiguration的配置类加载一定是在 FeignClientsConfiguration之前(先加载先生效),而 FeignClientsConfiguration加载是通过 FeignAutoConfiguration完成的,所以上图中引入了条件注解:

1.  @AutoConfigureBefore({FeignAutoConfiguration.class})

·      创建创建的 builder对象和第二种情况一下,只是做了一层包装:

1.    finalclassSleuthFeignBuilder{

2.

3.       privateSleuthFeignBuilder() {}

4.

5.       staticFeign.Builderbuilder(Tracertracer, HttpTraceKeysInjectorkeysInjector) {

6.           returnHystrixFeign.builder()

7.                   //各组件`clientretryerdecoder`进行增强,装饰器模式。

8.                   .client(newTraceFeignClient(tracer, keysInjector))

9.                   .retryer(newTraceFeignRetryer(tracer))

10.                .decoder(newTraceFeignDecoder(tracer))

11.                .errorDecoder(newTraceFeignErrorDecoder(tracer));

12.    }

13.}

2、设置重试策略,log等组件Feign.builder在获取之后又分别指定了重试策略,日志级别,错误代码code等,在上一步中调用 SleuthFeignBuilder.build()时已经设置过默认值了,这里为什么要重复设置呢?

我们跟进去get()方法,一探究竟:

1.       protected<T> T get(FeignContextcontext, Class<T> type) {

2.           //根据name,也就是服务名称来生成builder

3.           T instance = context.getInstance(this.name, type);

4.           if(instance == null) {

5.               thrownewIllegalStateException("No bean found of type "+ type + " for "

6.                       + this.name);

7.           }

8.           returninstance;

9.       }

1.       public<T> T getInstance(Stringname, Class<T> type) {

2.           //这里获取AnnotationConfigApplicationContext容器

3.           AnnotationConfigApplicationContextcontext = getContext(name);

4.           if(BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context,

5.                   type).length > 0) {

6.               returncontext.getBean(type);

7.           }

8.           returnnull;

9.       }

10.

11.    privateMap<StringAnnotationConfigApplicationContext> contexts = newConcurrentHashMap<>();

12.

13.    protectedAnnotationConfigApplicationContextgetContext(Stringname) {

14.        if(!this.contexts.containsKey(name)) {

15.            synchronized(this.contexts) {

16.                if(!this.contexts.containsKey(name)) {

17.                    //这里创建容器createContext(name)

18.                    this.contexts.put(name, createContext(name));

19.                }

20.            }

21.        }

22.        returnthis.contexts.get(name);

23.    }

重点来了,上述代码将FeignContext做了缓存,每个服务对应一个FeignContext,服务名作为key继续跟进 createContext(name)方法:

屏幕快照 2019-09-05 09.37.25.png


可以看到上述AnnotationConfigApplicationContext容器并非spring容器,只是利用了spring刷新容器的方法来实例化配置类,以服务名作为key,配置隔离。

重点来了,上面加载配置的顺序为:先加载每个服务的配置类,然后加载启动类注解上的配置类,最后加载默认的配置类。这样做有什么好处?spring刷新容器的方法也是对所有的bean进行了缓存,如果已经创建,则不再实例化。所以优先选取每个FeignClient的配置类,最后默认的配置类兜底。

所以这也证明了 sleuth的配置一定在 feign的配置类之前加载。至此, FeignBuilder构造流程结束。


四、生成负载均衡代理类

再贴一下生成代理类的入口:

1.           //判断url是否为空

2.           if(!StringUtils.hasText(this.url)) {

3.             //......

4.               returnloadBalance(builder, context, newHardCodedTarget<>(this.type,

5.                       this.name, url));

6.           }

7.           //......

8.           returntargeter.target(this, builder, context, newHardCodedTarget<>(

9.                   this.type,this.name, url));

这里有个重要判断:判断FeignClient声明的url是否为空,来判断具体要生成的代理类。如下:这么做有什么意义?1)如果为空,则默认走Ribbon代理,也就是这个入口,会有加载ribbon的处理。 @FeignClient("MyFeignClient") 2)如果不为空,指定url,则走默认生成代理类的方式,也就是所谓的硬编码。 @FeignClient(value = "MyFeignClient",url = "http://localhost:8081") 这样处理方便开发人员进行测试,无需关注注册中心,直接http调用,是个不错的开发小技巧。

生产环境也可以用上述第二种方式,指定域名的方式。

我们跟进 loadBalance方法:


1.       protected<T> T loadBalance(Feign.Builderbuilder, FeignContextcontext,

2.               HardCodedTarget<T> target) {

3.           //获得FeignClient

4.           Clientclient = getOptional(context, Client.class);

5.           if(client != null) {

6.               builder.client(client);

7.               returntargeter.target(this, builder, context, target);

8.           }

9.           thrownewIllegalStateException(

10.                "No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-ribbon?");

11.    }

Client client = getOptional(context, Client.class);这里会从 FeignContext上下文中获取 Client对象,该对象有三种实例,具体是哪个实现呢? spacer.gif

4.png

 

这里又会有三种情况:1)没有整合 ribbon sleuth获取默认的 Client Default实例。

2)整合了 ribbon,没有整合 sleuth获取 LoadBalanceFeignClient实例。

5.png

3)整合了 ribbon  sleuth会获取 TraceFeignClient实例,该实例是对 LoadBalanceFeignClient的一种包装,实现方式通过 BeanPostProcessor实现: FeignBeanPostProcessor中定义了包装逻辑:

1.       @Override

2.       publicObjectpostProcessBeforeInitialization(Objectbean, StringbeanName)

3.               throwsBeansException{

4.           returnthis.traceFeignObjectWrapper.wrap(bean);

5.       }

通过 wrap方法最终返回 TraceFeignClient实例。

继续回到主流程,先来看下 Targeter接口:

1.   interfaceTargeter{

2.           <T> T target(FeignClientFactoryBeanfactory, Feign.Builderfeign, FeignContextcontext,

3.                   HardCodedTarget<T> target);

4.       }

该对象定义在 FeignClientFactoryBean静静态代码块中:

1.       privatestaticfinalTargetertargeter;

2.

3.       static{

4.           TargetertargeterToUse;

5.           //判断类路径是否引入了hystrixFeign

6.           if(ClassUtils.isPresent("feign.hystrix.HystrixFeign",

7.                   FeignClientFactoryBean.class.getClassLoader())) {

8.               targeterToUse = newHystrixTargeter();

9.           }

10.        else{

11.            targeterToUse = newDefaultTargeter();

12.        }

13.        targeter = targeterToUse;

14.    }

这里会初始化 Targeter,该类是生成feign代理类的工具类,有两种实现,正是上面的 HystrixTargeterDefaultTargeter因为我们引入了 hystrix,所以 Targeter实现类为 HystrixTargeter。我们继续跟进 targeter.target方法:

1.     public<T> T target(Target<T> target) {

2.         returnbuild().newInstance(target);

3.       }

上面通过 build()方法获取生成代理类的工具类 ReflectiveFeign,再通过 newInstance正式创建代理类。继续跟进:

1.       publicFeignbuild() {

2.         SynchronousMethodHandler.FactorysynchronousMethodHandlerFactory =

3.             newSynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,

4.                                                  logLevel, decode404);

5.         ParseHandlersByNamehandlersByName =

6.             newParseHandlersByName(contract, options, encoder, decoder,

7.                                     errorDecoder, synchronousMethodHandlerFactory);

8.         returnnewReflectiveFeign(handlersByName, invocationHandlerFactory);

9.       }

这里会创建Feign的方法工厂 synchronousMethodHandlerFactoryFeign通过该工厂为每个方法创建一个 methodHandler,每个 methodHandler中包含Feign对应的配置: retryer requestInterceptors等。

继续跟进 newInstance方法:

1.    public<T> T newInstance(Target<T> target) {

2.        //创建所有的MethodHandler

3.       Map<StringMethodHandler> nameToHandler = targetToHandlersByName.apply(target);

4.       Map<MethodMethodHandler> methodToHandler = newLinkedHashMap<MethodMethodHandler>();

5.       List<DefaultMethodHandler> defaultMethodHandlers = newLinkedList<DefaultMethodHandler>();

6.

7.       for(Methodmethod : target.type().getMethods()) {

8.         if(method.getDeclaringClass() == Object.class) {

9.           continue;

10.       //判断是否启用默认handler

11.      } elseif(Util.isDefault(method)) {

12.        DefaultMethodHandlerhandler = newDefaultMethodHandler(method);

13.        defaultMethodHandlers.add(handler);

14.        methodToHandler.put(method, handler);

15.      } else{

16.        methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));

17.      }

18.    }

19.    //创建InvocationHandler,接收请求,转发到methodHandler

20.    InvocationHandlerhandler = factory.create(target, methodToHandler);

21.    //生成代理类

22.    T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),newClass<?>[]{target.type()}, handler);

23.

24.   //将默认方法绑定到代理类

25.    for(DefaultMethodHandlerdefaultMethodHandler : defaultMethodHandlers) {

26.      defaultMethodHandler.bindTo(proxy);

27.    }

28.    returnproxy;

29.  }

InvocationHandler最终创建的实例为 HystrixInvocationHandler,核心方法如下:

1.    HystrixCommand<Object> hystrixCommand = newHystrixCommand<Object>(setter) {

2.         @Override

3.         protectedObjectrun() throwsException{

4.           try{

5.             returnHystrixInvocationHandler.this.dispatch.get(method).invoke(args);

6.           } catch(Exceptione) {

7.             throwe;

8.           } catch(Throwablet) {

9.             throw(Error) t;

10.        }

11.      }

12.

13.      @Override

14.      protectedObjectgetFallback() {

15.      //......

16.      }

17.    };

整个流程:Feign调用方发起请求,发送至hystrixHystrixInvocationHandler,通过服务名称,找到对应方法的methodHandlermethodHandler中封装了loadBalanceClientretryerRequestInterceptor等组件,如果引入了sleuth,这几个组件均是sleuth的包装类。然后通过以上组件构造 http请求完成整个过程。


五、生成默认代理类

理解了第四步的逻辑,生成默认代理类就很容易理解了,唯一不同点就是 client的实现类为 loadBalanceClient

注意:不管是哪种代理类,最终发起请求还是由 Feign.Default中的 execute方法完成,默认使用 HttpUrlConnection实现。


六、注入spring容器

总结:通过 spring refresh()方法,触发 FeignClientFactoryBean.getObject()方法获得了代理类,然后完成注入 spring容器的过程。该实现方式同 Dubbo的实现方式类似,有兴趣的可以自行研究噢。


作者:阿康


登录后可下载附件,请登录或者注册

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件至:huaweicloud.bbs@huawei.com进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容。
评论文章 //点赞 收藏 0
点赞
分享文章到微博
分享文章到朋友圈

上一篇:SpringCloud系列:拜托!面试请不要再问我Spring Cloud底层原理

下一篇:SpringCloud系列:Spring Cloud微服务版本灰度发布新神器秒杀架构实践

评论 (0)


登录后可评论,请 登录注册

评论