Spring Cloud OpenFeign的原理(六)
通过上篇我们了解OpenFeign他也可以完成远程通信,但是它并不是真正义意上的RPC通信,因为他是通过封装代理来实现的,下面和以前一样,知道了怎么用就来看下他是怎么实现的。
一、思考Feign要做的事情
有了ribbon的铺垫现在看OpenFeign应该很清楚的知道,这玩意就是通过注解拿到服务名,然后通过服务名获取服务列表,进行解析和负载最终拼接出一个URI路径进行代理请求,那么他要完成这一系列动作他就要做下面几件事。
- 参数的解析和装载
- 针对指定的FeignClient,生成动态代理
- 针对FeignClient中的方法描述进行解析
- 组装出一个Request对象,发起请求
二、源码分析
看过我写的ribbon的应该清楚,如果想要找到进入源码的入口那么应该要找的是FeignClient,但是FeignClient是在哪里被解析的呢,在应用篇中我在启动类中加了个@EnableFeignClients注解,这 个注解的作用其实就是开启了一个FeignClient的扫描,那么点击启动类的@EnableFeignClients注解看下他是怎么开启FeignClient的扫描的,进去后发现里面有个@Import(FeignClientsRegistrar.class)这个FeignClientsRegistrar跟Bean的动态装载有关
点击进去有个registerBeanDefinitions方法通过名称可以知道是一个Bean的注入方法
下面我写一个简单的例子来描述他是如何实现动态加载的,学FeignClientsRegistrar类 implements ImportBeanDefinitionRegistrar接口并实现registerBeanDefinitions方法
这一步搞完后,定义一个注解,把@EnableFeignClients注解上的注解都抄过来并把@Import注解里面的类改成我们自己定义的类
然后在启动类上用上自定义的注解,那么在启动类时就可以进行一个Bean的动态装载了
通过这个概念已经很清楚源码中FeignClientsRegistrar类的FeignClientsRegistrar是怎么完成Bean的动态加载了
- registerDefaultConfifiguration 方法内部从 SpringBoot 启动类上检查是否有@EnableFeignClients, 有该注解的话, 则完成 Feign 框架相关的一些配置内容注册
- registerFeignClients 方法内部从 classpath 中, 扫描获得 @FeignClient 修饰的类, 将类的内容解析为 BeanDefifinition , 最终通过调用 Spring 框架中的BeanDefifinitionReaderUtils.resgisterBeanDefifinition 将解析处理过的 FeignClientBeanDeififinition 添加到 spring 容器中.
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
//注册默认配置信息
registerDefaultConfiguration(metadata, registry);
//注册FeignClients(可能有多个)
registerFeignClients(metadata, registry);
}
进入registerFeignClients(metadata, registry);这玩意是干啥的呢,在启动类中的@EnableFeignClients是可以定义多个basePackers的如果定义了多个那就要扫描FeignClients,下面就是扫描处理过程,看过spring源码的人就知道前面是什么注解解析
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());
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
FeignClient.class);
final Class<?>[] clients = attrs == null ? null
: (Class<?>[]) attrs.get("clients");
if (clients == null || clients.length == 0) {
scanner.addIncludeFilter(annotationTypeFilter);
basePackages = getBasePackages(metadata);
}
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)));
}
//bssePackages是解析不同basePackage路径下的FeignClient的声明
for (String basePackage : basePackages) {
Set<BeanDefinition> candidateComponents = scanner
.findCandidateComponents(basePackage);
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) {
// 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()); String name = getClientName(attributes);
//如果每个包路径下都有一个或多个FeignClient的话就要加载 一次动态configuration配置所以这就是为什么前面已经加载过了这里还要加载一次的原因
registerClientConfiguration(registry, name,
attributes.get("configuration"));
//注册Feignclient
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
}
点击registerFeignClient(registry, annotationMetadata, attributes);看下做了啥事,这里面的逻辑其实就干了一件事,就是通过BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);注入一个Bean;这个注入的过程中有个比较重要的代码BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);这是一个构造者,构造一个BeanDefinition,里面把FeignClientFactoryBean.class给传了进去
下面进入.genericBeanDefinition(FeignClientFactoryBean.class);看把 FeignClientFactoryBean.class类传进去干嘛,发现注册的Bean就是参数中自己传进来的beanClass,这个传进去的beanClass是工厂Bean
Spring Cloud FengnClient实际上是利用Spring的代理工厂来生成代理类,所以在这里地方才会把所有的FeignClient的BeanDefifinition设置为FeignClientFactoryBean类型,而FeignClientFactoryBean继承自FactoryBean,它是一个工厂Bean。在Spring中,FactoryBean是一个工厂Bean,用来创建代理Bean。工厂 Bean 是一种特殊的 Bean, 对于 Bean 的消费者来说, 他逻辑上是感知不到这个 Bean 是普通的 Bean 还是工厂 Bean, 只是按照正常的获取 Bean 方式去调用, 但工厂bean 最后返回的实例不是工厂Bean 本身, 而是执行工厂 Bean 的 getObject 逻辑返回的示例。
点击这个工厂Bean的FeignClientFactoryBean类中发现里面有个getObject()方法,这个工厂Bean就是通过这个getTarget();返回一个真正的实例
画下时序图
前面说到了在启动时会通过@EnableFeignClients去扫描所有指定路径下的@FeignClient注解声明的一个接口,然后在扫描到以后要去生成一个动态代理的类,这个动态代理的生成就是在调用getObject()时完成 ,而且getObject()又会调用他方法里面的getTarget()去完成这件事,
<T> T getTarget() {
//FeignContext注册到容器是在FeignAutoConfiguration上完成的
//在初始化FeignContext时,会把configurations在容器中放入FeignContext中。configurations的
//来源就是在前面registerFeignClients方法中将@FeignClient的配置configuration。
FeignContext context = this.applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context);//构建Builder对象
//如果url为空,则走负载均衡,生成有负载均衡功能的代理类
if (!StringUtils.hasText(this.url)) {
if (!this.name.startsWith("http")) {
this.url = "http://" + this.name;
}
else {
this.url = this.name;
}
this.url += cleanPath();
return (T) loadBalance(builder, context,
new HardCodedTarget<>(this.type, this.name, this.url));
}
//如果指定了url,则生成默认的代理类
if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
this.url = "http://" + this.url;
}
String url = this.url + cleanPath();
Client client = getOptional(context, Client.class);
if (client != null) {
if (client instanceof LoadBalancerFeignClient) {
// not load balancing because we have a url,
// but ribbon is on the classpath, so unwrap
client = ((LoadBalancerFeignClient) client).getDelegate();
}
if (client instanceof FeignBlockingLoadBalancerClient) {
// not load balancing because we have a url,
// but Spring Cloud LoadBalancer is on the classpath, so unwrap
client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
}
builder.client(client);
}//生成默认代理类
Targeter targeter = get(context, Targeter.class);
return (T) targeter.target(this, builder, context,
new HardCodedTarget<>(this.type, this.name, url));
}
上面有段代码Feign.Builder builder = feign(context);是构建Builder对象
protected Feign.Builder feign(FeignContext context) {
FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
Logger logger = loggerFactory.create(this.type); // @formatter:off
Feign.Builder builder = get(context, Feign.Builder.class)
// required values
//开启日志级别
.logger(logger)
//编码
.encoder(get(context, Encoder.class))
//解码
.decoder(get(context, Decoder.class))
//连接。这个连接用在解析模板的,后面会提
.contract(get(context, Contract.class));
// @formatter:on configureFeign(context, builder); return builder;
}
上面的builder构造完后继续向下走,配置完Feign.Builder之后,再判断是否需要LoadBalance,如果需要,则通过LoadBalance的方法来设置。实际上他们最终调用的是Target.target()方法。
@Import({ HttpClientFeignLoadBalancedConfiguration.class,
OkHttpFeignLoadBalancedConfiguration.class,
DefaultFeignLoadBalancedConfiguration.class })
protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
HardCodedTarget<T> target) {
//针对某一个服务的client
Client client = getOptional(context, Client.class);
if (client != null) {
//将client设置进去相当于增加了客户端负载均衡解析的机制
builder.client(client);
Targeter targeter = get(context, Targeter.class);
return targeter.target(this, builder, context, target);
} throw new IllegalStateException(
"No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?");
}
点击上图的targeter.target(this, builder, context, target);因为熔断准备在后面讲,所以在选tartget的实现时选择DefaultTarget.target

点击feign.target()往下走,走到这里其实就已经到了核心逻辑了,前面不一直说动态代理吗,前面走的都是人生最长的套路,前面自己写的控制层代码通过@Resource注解注入的UserOpenFeign他最终会调用下面的方法返回一个实例,那么下面看下这newInstance()方法做了啥,发现这玩意有两个实现,至于选择哪个就要看build()返回的是什么了,向下看发现build()返回的是ReflectiveFeign,所以选第二个
public <T> T newInstance(Target<T> target) {
//根据接口类和Contract协议解析方式,解析接口类上的方法和注解,转换成内部的MethodHandler处理方式
Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>(); for (Method method : target.type().getMethods()) {
if (method.getDeclaringClass() == Object.class) {
continue;
} else if (Util.isDefault(method)) {
DefaultMethodHandler handler = new DefaultMethodHandler(method);
defaultMethodHandlers.add(handler);
methodToHandler.put(method, handler);
} else {
methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
}
}
// 基于Proxy.newProxyInstance 为接口类创建动态实现,将所有的请求转换给InvocationHandler 处理。
InvocationHandler handler = factory.create(target, methodToHandler);
T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
new Class<?>[] {target.type()}, handler); for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
defaultMethodHandler.bindTo(proxy);
}
return proxy;
}
下面通过debugger验证下,会看到userOpenFeign的返回的是代理类,通过下图可以知道当调用userOpenFeign时他其实是调用ReflectiveFeign中的handler,而通过Debugger发现这个handler是FeginInvocationHandler,
竟然是走了代理那么他一定是走了ReflectiveFeign的代理方法invoke()方法
Spring Cloud OpenFeign的原理(六)的更多相关文章
- spring cloud系列教程第六篇-Eureka集群版
spring cloud系列教程第六篇-Eureka集群版 本文主要内容: 本文来源:本文由凯哥Java(kaigejava)发布在博客园博客的.转载请注明 1:Eureka执行步骤理解 2:集群原理 ...
- 微服务生态组件之Spring Cloud OpenFeign详解和源码分析
Spring Cloud OpenFeign 概述 Spring Cloud OpenFeign 官网地址 https://spring.io/projects/spring-cloud-openfe ...
- 撸一撸Spring Cloud Ribbon的原理-负载均衡器
在上一篇<撸一撸Spring Cloud Ribbon的原理>中整理发现,RestTemplate内部调用负载均衡拦截器,拦截器内最终是调用了负载均衡器来选择服务实例. 接下来撸一撸负载均 ...
- 撸一撸Spring Cloud Ribbon的原理-负载均衡策略
在前两篇<撸一撸Spring Cloud Ribbon的原理>,<撸一撸Spring Cloud Ribbon的原理-负载均衡器>中,整理了Ribbon如何通过负载均衡拦截器植 ...
- Spring Cloud OkHttp设计原理
Spring Cloud 框架最底层核心的组件就是服务调用方式,一般Spring Cloud框架采用的是HTTP的调用框架,本文将在 Spring Cloud应用场景下,介绍组件OkHttp3的设计原 ...
- Eureka 系列(03)Spring Cloud 自动装配原理
Eureka 系列(03)Spring Cloud 自动装配原理 [TOC] 0. Spring Cloud 系列目录 - Eureka 篇 本文主要是分析 Spring Cloud 是如何整合 Eu ...
- Feign 系列(05)Spring Cloud OpenFeign 源码解析
Feign 系列(05)Spring Cloud OpenFeign 源码解析 [TOC] Spring Cloud 系列目录(https://www.cnblogs.com/binarylei/p/ ...
- Spring Cloud OpenFeign使用教程
文章目录 Spring Cloud OpenFeign Demo 怎么配置OpenFeignServer 怎么配置OpenFeignClient 多个参数传递问题 FeignClient的日志问题 多 ...
- SpringCloud升级之路2020.0.x版-29.Spring Cloud OpenFeign 的解析(1)
本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 在使用云原生的很多微服务中,比较小规模的可能直接依靠云服务中的负载均衡器进行内部域名与服务 ...
随机推荐
- 左右声道音频怎么制作,用Vegas就对啦
一款优秀的视频剪辑软件,不仅有高水平的视频制作功能,它的音频编辑功能也是必不可少的.Vegas就是这么一款软件,同时具备视频制作特效制作的同时,还能帮助制作轨道音频效果. 下面,就让小编带大家去学习, ...
- 下载器Folx扩展程序支持哪些浏览器
Folx使用多线程的下载方式大大提升了下载的速度,可以完全替代浏览器自带的下载工具,使下载文件的管理更加简单高效.但是,必须给浏览器安装Folx扩展程序,才能使用Folx下载页面链接. Folx在偏好 ...
- kubernetes-dashboard.yaml
# Copyright 2017 The Kubernetes Authors.## Licensed under the Apache License, Version 2.0 (the " ...
- k8S 搭建集群
k8S 搭建集群1:修改主机名称hostnamectl --static set-hostname masterhostnamectl --static set-hostname node1hostn ...
- canal部署
转载: https://blog.csdn.net/qq_30043755/article/details/83539116 最后的binlog最后被封装为这样的一个对象: com.alibaba.o ...
- kafka入门之broker-集群管理
依赖于zookeeper,broker向zk中注册的信息以json格式保存,其中包括: 1.listener_security_protocol_map:此值指定了该broker与外界通信所用的安全协 ...
- 一道百度java面试题的多种解法
下面是我在2018年10月11日二面百度的时候的一个问题: java程序,主进程需要等待多个子进程结束之后再执行后续的代码,有哪些方案可以实现? 这个需求其实我们在工作中经常会用到,比如用户下单一个产 ...
- mininet + opendaylight环境配置
环境配置 ubuntu18.04 镜像 mininet2.2.2 apt-get install mininet 但这种安装只是TLS版本的mininet,与最新版本在功能上有所差距. 控制器(ope ...
- 20200509_设置笔记本使用有线访问外网同时wifi访问外网
1. 控制面板\所有控制面板项\网络连接 2. wifi的使用的手机热点, dhcp分配的, 不用做配置 3. 笔记本获取到的内网静态地址是192.168.3.11, 网关是192.168.3.254 ...
- eclispe中打点不会提示的解决方法,以及自动补全
Eclipse中打点无提示的解决办法 建了个JAVA工程,然后发现输入代码后,在输入.后面不会弹出来我所要的函数. alt+/ 提示No Default Proposals 自己找了半天, ...