1.测试环境搭建:

1.1 架构图:

product服务提供一个接口:

order服务通过feign的方式来调用product的接口:

order服务需要引入依赖:

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

order服务对外的controller :

order服务主启动类:

2. 源码分析

          2.1: spring 是如何找到@FeignClient标注的接口?
             我们在order服务启动类中加了一个注解:@EnableFeignClients,点击该注解进入看看

    

由图可知,该注解向容器导入了FeignClientsRegistrar.class类,然后我们跟踪进入该类:

该类实现了ImportBeanDefinitionRegistrar接口,该接口有个向spring容器注册的组件的方法:

debug调试:发现metadata就是主启动类的信息

先跟踪第一个方法:registerDefaultConfiguration(metadata, registry);

上图:通过主启动类获取@EnableFeignClients注解的defaultConfiguration属性,同时创建了一个名叫name的变量,值为:default.+主启动类的全限定类名

继续跟进去:

上图:向sprin容器中注册了一个FeignClientSpecification类,beanName为:default.com.yang.xiao.hui.order.OrderApplication.FeignClientSpecification

@EnableFeignClients注解的defaultConfiguration属性我并没有配置信息,所以FeignClientSpecification的属性值是一个空的数组

第一个方法分析完后,下面分析第二个:

public void registerFeignClients(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
ClassPathScanningCandidateComponentProvider scanner = getScanner(); //创建一个扫描器
scanner.setResourceLoader(this.resourceLoader); Set<String> basePackages; Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName()); //获取EnableFeignClients注解的元素据,因为该注解可以包含你要扫描的路径 AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(FeignClient.class); //注解过滤器,代表要过滤含有FeignClient.class的类
final Class<?>[] clients = attrs == null ? null
: (Class<?>[]) attrs.get("clients");
if (clients == null || clients.length == 0) { //由于我没有配置对应的属性,所以会走这个分支
scanner.addIncludeFilter(annotationTypeFilter); //扫描器将注解过滤器新增进来了,这个是重点了
basePackages = getBasePackages(metadata); //EnableFeignClients注解的value,basePackages,basePackageClasses属性有值,就扫描这些配置的包名,如果没设置就扫描主启动类的包名,此处我没有配置,所以这里得到的是主启动类的报名
}
else {
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)));
}

//遍历所有要扫描的包名
for (String basePackage : basePackages) {
Set<BeanDefinition> candidateComponents = scanner
.findCandidateComponents(basePackage); //通过扫描器,读取包名下所有的类,然后看看哪些是有FeignClient.class注解的,将这些类转成beanDefinition对象
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) { //这些BeanDefinition 都含有FeignClient.class注解
// 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"); Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes( FeignClient.class.getCanonicalName()); //获取FeignClient.class注解元信息 String name = getClientName(attributes);//获取FeignClient.class的name值,我这里配置的是@FeignClient(name ="product" ),所以name就是product
registerClientConfiguration(registry, name, attributes.get("configuration"));//这个跟之前分析的第一个方法逻辑是一样的:向容器注入了一个name=product.feignClientSpecification 类型feignClientSpecification的bean registerFeignClient(registry, annotationMetadata, attributes);//注册所有的带有@FeignClient(name ="product" )注解的bean
}
}
}
}

上面代码总结:主要做了:

1.创建了一个扫描器ClassPathScanningCandidateComponentProvider scanner,并将AnnotationTypeFilter过滤器传入扫描器中(scanner.addIncludeFilter(annotationTypeFilter)),用于过滤扫描到的带有@FeignClient注解的类

2.获取要扫描的包路径:basePackages = getBasePackages(metadata);

3.遍历包名获取候选的带有FeignClient注解的类的信息 Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);

因为扫描器之前加了一个注解过滤器,这里isCandidateComponent(metadataReader)的逻辑是使用注解过滤器来过滤出带有FeignClien注解的类

4.遍历扫描到的BeanDefinition,向ioc容器中注册对应的bean,这里每个带有@FeignClient的类都会注册2个bean

registerClientConfiguration(registry, name,attributes.get("configuration")); 向容器中注册了name=product.feignClientSpecification 类型feignClientSpecification的bean

重点是: registerFeignClient(registry, annotationMetadata, attributes);方法

所以,该方法向spring容器注入的bean,名字为:com.yang.xiao.hui.order.controller.ProductService,类型为FeignClientFactoryBean,而FactoryBean都会提供一个getObject方法获取对应的实例

spring如何获取带有@FeignClient注解标准的接口,总结如下图:

3.springboot自动装配机制:springboot启动时会加载类路径下/META-INF/spring.factories中key为org.springframework.boot.autoconfigure.EnableAutoConfiguration的类:

因此有四个相关的配置类会被加载,我们主要关注2个FeignRibbonClientAutoConfiguration 和FeignAutoConfiguration

//再看看DefaultFeignLoadBalancedConfiguration这个类:

之后看看这个bean: FeignAutoConfiguration

总结:自动装配机制注入的bean:

4.获取第三方服务实例

4.1由前面分析,每个带有@FeignClient注解的接口都会被注入到spring容器,名字为接口的权限定类名,类型为FeignClientFactoryBean

4.2我们debug调试,从容器中根据beanName获取对应的实例看看,我这里的beanName=com.yang.xiao.hui.order.controller.ProductService,order服务的controller方法先修改下:

      

跟进该方法:

对于factoryBean,如果beanName前面带上“&” 符号,获取的bean就是原始的factoryBean,如果没带该符号,获取的是factoryBean.getObject()方法返回的bean,我们这次调试是没带上该符合的

至此,我们可以知道,所有带有@FeignClient注解的接口获取实例,最终都是调用FeignClientFactoryBean的getObject方法,该方法才是我们的重点

4.3 FeignClientFactoryBean的getObject()方法的讲解:

分析: Feign.Builder builder = feign(context);

//上面很多bean都是通过get(Context,xxx.class)方法获取,而context就是FeignContext,在springBoot自动装配时注入了spring容器,那么我们跟踪第一个方法看看:

FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);

这里有2个疑问:这个容器是怎么创建的,为何能通过该容器获取对应的bean

由于上面的获取子容器的方法是在FeignContext中的,所以我们要来分析下FeignContext的创建过程:

FeignContext创建时调用了父类的有参构造

此时们再回到,通过服务名称获取子容器的方法,获取不到就创建一个子容器:

由图可知,子容器注入了一个FeignClientsConfiguration配置类,该类是由FeignContext初始化时,调用父类有参构造器传入的,所以分析下该配置类:

@Configuration(proxyBeanMethods = false)
public class FeignClientsConfiguration { @Autowired
private ObjectFactory<HttpMessageConverters> messageConverters; @Autowired(required = false)
private List<AnnotatedParameterProcessor> parameterProcessors = new ArrayList<>(); @Autowired(required = false)
private List<FeignFormatterRegistrar> feignFormatterRegistrars = new ArrayList<>(); @Autowired(required = false)
private Logger logger; @Autowired(required = false)
private SpringDataWebProperties springDataWebProperties; @Bean
@ConditionalOnMissingBean
public Decoder feignDecoder() {
return new OptionalDecoder(
new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)));
} @Bean
@ConditionalOnMissingBean
@ConditionalOnMissingClass("org.springframework.data.domain.Pageable")
public Encoder feignEncoder() {
return new SpringEncoder(this.messageConverters);
} @Bean
@ConditionalOnClass(name = "org.springframework.data.domain.Pageable")
@ConditionalOnMissingBean
public Encoder feignEncoderPageable() {
PageableSpringEncoder encoder = new PageableSpringEncoder(
new SpringEncoder(this.messageConverters));
if (springDataWebProperties != null) {
encoder.setPageParameter(
springDataWebProperties.getPageable().getPageParameter());
encoder.setSizeParameter(
springDataWebProperties.getPageable().getSizeParameter());
encoder.setSortParameter(
springDataWebProperties.getSort().getSortParameter());
}
return encoder;
} @Bean
@ConditionalOnMissingBean
public Contract feignContract(ConversionService feignConversionService) {
return new SpringMvcContract(this.parameterProcessors, feignConversionService);
} @Bean
public FormattingConversionService feignConversionService() {
FormattingConversionService conversionService = new DefaultFormattingConversionService();
for (FeignFormatterRegistrar feignFormatterRegistrar : this.feignFormatterRegistrars) {
feignFormatterRegistrar.registerFormatters(conversionService);
}
return conversionService;
} @Bean
@ConditionalOnMissingBean
public Retryer feignRetryer() {
return Retryer.NEVER_RETRY;
} @Bean
@Scope("prototype")
@ConditionalOnMissingBean
public Feign.Builder feignBuilder(Retryer retryer) {
return Feign.builder().retryer(retryer);
} @Bean
@ConditionalOnMissingBean(FeignLoggerFactory.class)
public FeignLoggerFactory feignLoggerFactory() {
return new DefaultFeignLoggerFactory(this.logger);
} @Bean
@ConditionalOnClass(name = "org.springframework.data.domain.Page")
public Module pageJacksonModule() {
return new PageJacksonModule();
} @Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class })
protected static class HystrixFeignConfiguration { @Bean
@Scope("prototype")
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "feign.hystrix.enabled") //这里可以发现,如果要想让hystrix生效,需要配置一下属性feign.hystrix.enabled
public Feign.Builder feignHystrixBuilder() {
return HystrixFeign.builder();
} } }

通过该配置类,可以知道,子容器中注入了Decoder,Encoder,Contract,FormattingConversionService,Retryer,FeignLoggerFactory,Module

此时我们再看看父容器和子容器注入的主要类:

有了上面的分析,我们回到之前的方法继续分析:

我们看到该方法:configureUsingProperties(properties.getConfig().get(this.contextId),builder);,通过一个ContextId也就是服务名,我这里是product,获取该服务的专有配置,说明每一个服务都可以拥有自己的特殊配置;
看看这个FeignClientProperties类:

我们再次回到FactoryBean的getObject()方法进行分析:

上图,我们看到从容器中获取了一个client和targeter,这2个bean是在父容器中获取的,前面有分析过了:

继续跟进:

可见是调用了ReflectiveFeign的newInstance(target);方法

至此,我们可以发现,最终是调用了JDK动态代理机制来生成代理类,下面再来分析上面2个方法:

Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);和InvocationHandler handler = factory.create(target, methodToHandler);
先看第一个方法:targetToHandlersByName.apply(target)

我这里只有一个方法:

所以跟进去:

可见,校验是对RequestMaping注解的校验

我们分析对方法上的处理:

之后我们再分析InvocationHandler handler = factory.create(target, methodToHandler);方法:

对上述所有分析做个总结:

5.我们是如何通过feign调用到第三方服务的,在这里就是我们是如何通过order服务调用到product服务:

5.1:调用任何服务都要知道ip和端口,因此,如何获取ip和端口,如果服务提供者有多个,如何进行负载均衡,构建测试代码如下,在order服务下:

我们看看server是如何获取到的,这里用到ribbon进行负载均衡,点击command.submit

上图是ribbon获取负载均衡获取单个服务实例的过程,可以参考我之前的博客,有详细源码讲解:https://www.cnblogs.com/yangxiaohui227/p/12614343.html

继续跟踪,到达了:

最终是使用HttpURLConnection发起请求:

 

 

spring-cloud-starter-openfeign 源码详细讲解的更多相关文章

  1. Spring Cloud 学习 之 Spring Cloud Eureka(源码分析)

    Spring Cloud 学习 之 Spring Cloud Eureka(源码分析) Spring Boot版本:2.1.4.RELEASE Spring Cloud版本:Greenwich.SR1 ...

  2. Spring Cloud Netflix Eureka源码导读与原理分析

    Spring Cloud Netflix技术栈中,Eureka作为服务注册中心对整个微服务架构起着最核心的整合作用,因此对Eureka还是有很大的必要进行深入研究. 本文主要分为四部分,一是对项目构建 ...

  3. spring cloud集成 consul源码分析

    1.简介 1.1 Consul is a tool for service discovery and configuration. Consul is distributed, highly ava ...

  4. 微服务分布式 spring cloud springboot 框架源码 activiti工作流 前后分离

    1.代码生成器: [正反双向](单表.主表.明细表.树形表,快速开发利器)freemaker模版技术 ,0个代码不用写,生成完整的一个模块,带页面.建表sql脚本.处理类.service等完整模块2. ...

  5. spring cloud springboot 框架源码 activiti工作流 前后分离 集成代码生成器

    1.代码生成器: [正反双向](单表.主表.明细表.树形表,快速开发利器)freemaker模版技术 ,0个代码不用写,生成完整的一个模块,带页面.建表sql脚本.处理类.service等完整模块2. ...

  6. Feign 系列(05)Spring Cloud OpenFeign 源码解析

    Feign 系列(05)Spring Cloud OpenFeign 源码解析 [TOC] Spring Cloud 系列目录(https://www.cnblogs.com/binarylei/p/ ...

  7. spring boot 2.0 源码分析(四)

    在上一章的源码分析里,我们知道了spring boot 2.0中的环境是如何区分普通环境和web环境的,以及如何准备运行时环境和应用上下文的,今天我们继续分析一下run函数接下来又做了那些事情.先把r ...

  8. SpringMVC+Maven开发项目源码详细介绍

    代码地址如下:http://www.demodashi.com/demo/11638.html Spring MVC概述 Spring MVC框架是一个开源的Java平台,为开发强大的基于Java的W ...

  9. AQS源码详细解读

    AQS源码详细解读 目录 AQS源码详细解读 基础 CAS相关知识 通过标识位进行线程挂起的并发编程范式 MPSC队列的实现技巧 代码讲解 独占模式 独占模式下请求资源 独占模式下的释放资源 共享模式 ...

随机推荐

  1. 区块链入门到实战(25)之以太坊(Ethereum) – 以太币单位

    以太币的主要单位是以太/Ether,即一个以太币,以太币的最小单位是wei. 以太币最小单位 wei 是以虚拟币先驱人物:戴伟 Wei Dai 命名,戴伟 W Dai 是一位兴趣广泛的密码学专家,他在 ...

  2. C++字符串与指针

    字符串初始化 在C++中基本数据类型并不包括string,string类型其实是一种类类型,通过STL函数库中的模板类basic_string 实例化得到. int main () { // stri ...

  3. WebApis中DOM操作的基本案例

    1.1. 排他操作 1.1.1 排他思想 如果有同一组元素,我们想要某一个元素实现某种样式, 需要用到循环的排他思想算法: 所有元素全部清除样式(干掉其他人) 给当前元素设置样式 (留下我自己) 注意 ...

  4. 人到中年的程序员,请提前准备好 Plan B

    中年程序员的生存现状已经是老生常谈的话题了,有多老呢?十年前,就有一位名叫"johnfx"的程序员谈过这个话题,并且专门为此写了一篇文章.随着中年程序员生存现状的话题再次成为热点, ...

  5. 小程序商城系统CRMEB Pro v1.1全新重构,新增DIY功能

    CRMEB ProV1.1全新升级发布,真正实现了后台可自由拖拽组合实现首页布局的DIY功能,这一功能的实现,将告别过去千篇一律的同质化界面布局,真正实现个性化.高自由的随心组合.本次发布的版本中我们 ...

  6. 使用rabbitmq过程中遇到的问题及解决方法记录。

    OS: Linux ---Centos7RabbitMQ版本:RabbitMQ version: 3.8.1erlang版本:Erlang configuration: Erlang/OTP 22 [ ...

  7. Left Mouse Button (bfs)

    Mine sweeper is a very popular small game in Windows operating system. The object of the game is to ...

  8. 再试Count(*) 与Count(*) 列

    试问,如果有一张表有两个字段,均可为空,插入两条首个字段为空的记录,再插入两条第二字段为空的记录,问count(*)和count(列)结果如何? 答案:count(*)是正常的四条,而count(列) ...

  9. C#的TextBox的四种禁止编辑方法

    前言 一般而言,Textbox中有两个属性可以对其进行防止编辑的设定,这是最基础的知识,也是我要提出的前两种方法.而后两种方法实际为一种,但可以应用于不同环境中. 一.ReadOnly属性 这样设置, ...

  10. js中数组扁平化处理