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队列的实现技巧 代码讲解 独占模式 独占模式下请求资源 独占模式下的释放资源 共享模式 ...
随机推荐
- superslide滚动插件使用记录-产品滚动-图片滚动
在用wordpress制作一个企业网站时,用到了这个superslide的滚动插件,用于案例.证书等滚动效果.该插件网站在这里:http://www.superslide2.com/ 我所使用的wor ...
- js动画和css3动画的区别
JS动画(逐帧动画) 首先,在js动画是逐帧动画,是在时间帧上逐帧绘制帧内容,由于是一帧一帧的话,所以他的可操作性很高,几乎可以完成任何你想要的动画形式.但是由于逐帧动画的帧序列内容不一样,会增加制作 ...
- Python 逆向抓取 APP 数据
今天继续给大伙分享一下 Python 爬虫的教程,这次主要涉及到的是关于某 APP 的逆向分析并抓取数据,关于 APP 的反爬会麻烦一些,比如 Android 端的代码写完一般会进行打包并混淆加密加固 ...
- 3D人物移动控制实现方案
要控制3D人物在3D世界中进行正常的移动.转向,一般有两种情况: 1.使用人物动画控制人物 的移动 转向 2.使用脚本控制人物 的移动.转向 对方案一: Animator 组件勾选上 Apply Ro ...
- java的方法详解和总结
一.什么是方法 在日常生活中,我们所说的方法就是为了解决某件事情,而采取的解决办法 java中的方法可以理解为语句的集合,用来完成解决某件事情或实现某个功能的办法 方法的优点: 程序变得更加简短而清晰 ...
- Queries for Number of Palindromes(区间dp)
You've got a string s = s1s2... s|s| of length |s|, consisting of lowercase English letters. There a ...
- Java是否还能再辉煌十年?
目录 Java是否还能再辉煌十年? 一.前言 二.如今的Java语言 2.1 位居TIOBE榜首 2.2 革命性的语言 三.Java受到的挑战 3.1 后台服务器软件的语言竞争 3.1.1 Pytho ...
- Codeforces1249E By Elevator or Stairs?
题意 给定整数c和数组a,b,\(a_i\)表示通过爬楼梯的方法从第\(i\)层到\(i+1\)层需要的时间,\(b_i\)表示通过坐电梯的方法从第\(i\)层到\(i+1\)层需要的时间,坐电梯前需 ...
- 用Java爬虫爬取凤凰财经提供的沪深A股所有股票代号名称
要爬取的凤凰财经网址:http://app.finance.ifeng.com/list/stock.php?t=hs 本作主要采用的技术是jsoup,相关介绍网页:https://www.jians ...
- Python的链接数上升得太快了!足见Python之火!