spring-cloud-starter-openfeign 源码详细讲解
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 源码详细讲解的更多相关文章
- Spring Cloud 学习 之 Spring Cloud Eureka(源码分析)
Spring Cloud 学习 之 Spring Cloud Eureka(源码分析) Spring Boot版本:2.1.4.RELEASE Spring Cloud版本:Greenwich.SR1 ...
- Spring Cloud Netflix Eureka源码导读与原理分析
Spring Cloud Netflix技术栈中,Eureka作为服务注册中心对整个微服务架构起着最核心的整合作用,因此对Eureka还是有很大的必要进行深入研究. 本文主要分为四部分,一是对项目构建 ...
- spring cloud集成 consul源码分析
1.简介 1.1 Consul is a tool for service discovery and configuration. Consul is distributed, highly ava ...
- 微服务分布式 spring cloud springboot 框架源码 activiti工作流 前后分离
1.代码生成器: [正反双向](单表.主表.明细表.树形表,快速开发利器)freemaker模版技术 ,0个代码不用写,生成完整的一个模块,带页面.建表sql脚本.处理类.service等完整模块2. ...
- spring cloud springboot 框架源码 activiti工作流 前后分离 集成代码生成器
1.代码生成器: [正反双向](单表.主表.明细表.树形表,快速开发利器)freemaker模版技术 ,0个代码不用写,生成完整的一个模块,带页面.建表sql脚本.处理类.service等完整模块2. ...
- Feign 系列(05)Spring Cloud OpenFeign 源码解析
Feign 系列(05)Spring Cloud OpenFeign 源码解析 [TOC] Spring Cloud 系列目录(https://www.cnblogs.com/binarylei/p/ ...
- spring boot 2.0 源码分析(四)
在上一章的源码分析里,我们知道了spring boot 2.0中的环境是如何区分普通环境和web环境的,以及如何准备运行时环境和应用上下文的,今天我们继续分析一下run函数接下来又做了那些事情.先把r ...
- SpringMVC+Maven开发项目源码详细介绍
代码地址如下:http://www.demodashi.com/demo/11638.html Spring MVC概述 Spring MVC框架是一个开源的Java平台,为开发强大的基于Java的W ...
- AQS源码详细解读
AQS源码详细解读 目录 AQS源码详细解读 基础 CAS相关知识 通过标识位进行线程挂起的并发编程范式 MPSC队列的实现技巧 代码讲解 独占模式 独占模式下请求资源 独占模式下的释放资源 共享模式 ...
随机推荐
- python爬虫数据提取之bs4的使用方法
Beautiful Soup的使用 1.下载 pip install bs4 pip install lxml # 解析器 官方推荐 2.引用方法 from bs4 import BeautifulS ...
- 归纳从文件中读取数据的六种方法-JAVA IO基础总结第2篇
在上一篇文章中,我为大家介绍了<5种创建文件并写入文件数据的方法>,本节我们为大家来介绍6种从文件中读取数据的方法. 另外为了方便大家理解,我为这一篇文章录制了对应的视频:总结java从文 ...
- 单表千万行数据库 LIKE 搜索优化手记
我们经常在数据库中使用 LIKE 操作符来完成对数据的模糊搜索,LIKE 操作符用于在 WHERE 子句中搜索列中的指定模式. 如果需要查找客户表中所有姓氏是“张”的数据,可以使用下面的 SQL 语句 ...
- Centos7安装Oracle12c教程
12c数据库 创建oracle的系统用户和用户组 [root@localhost /]# groupadd oinstall [root@localhost /]# groupadd dba [roo ...
- 腾讯大牛半年心血高级编程PDF,帮你轻松构建企业级Web应用
毫无疑问,Java 是这些年来最流行的编程语言之一.它无处不在一计算机. 手机.网站以及各种嵌入式设备中都存在着大量的Java 应用程序,而其中应用最为广泛的应该就是Java EE Web应用程序(以 ...
- Tesselation学习
Tesselation的作用:给低片面数模型镶嵌更多片面,让低模变高模. 和法线贴图不同,法线本质是通过改变低模表面的颜色来模拟高模,比如在一个片面上普通diffuse是均匀的颜色分布(因为光照颜色一 ...
- 喵的Unity游戏开发之路 - 互动环境(有影响的运动)
如图片.视频或代码格式等显示异常,请查看原文: https://mp.weixin.qq.com/s/Sv0FOxZCAHHUQPjT8rUeNw 很多童鞋没有系统的Unity3D游戏开发基础,也不知 ...
- 【jmespath】—1. 基础用法
一.jsonpath 之前我写接口自动化测试时候,对于复杂的json返回,会使用jsonpath这个第三方库,就像写xpath一样,方便的查询json元素. 因为之前写WEB自动化时候,总用xpath ...
- C#通过Com串口进行Barcode Printer
前言 工作中有遇到Barcode打印的需求,最开始是通过打印机型号找到对应的打印机,再进行操作,但是需要匹配的打印机型号太多,而且不定,所以处理起来太过麻烦. 后面通过找到通过串口找到打印机,直接传输 ...
- element封装表格
<template> <div> <el-scrollbar class="table-wrap"> <el-table v-loadin ...