【OpenFeign】@FeignClient 注入过程源码分析

news/发布时间2024/5/16 9:02:56

1  前言

微服务之间的调用,OpenFeign 是一种选择,并且还提供了很多功能,比如我们有多个节点,它能负载均衡,当服务发生异常时,它还能提供熔断机制。所以它是怎么实现的,因为我们平时只需要写 @FeignClient 是个接口,所以它势必会走代理,所以是不是要从我们的 @FeignClient  下手。那么这节我们就先简单看下OpenFeign 中 @FeignClient 代理的创建过程,当代理的创建过程知道了,然后我们就可以深入它的增强逻辑,继而能看到它的负载均衡、熔断又是如何做的对吧。

如果你之前看过我的 【Mybatis】【二】源码分析-Mapper 接口都是怎么注入到 Spring容器中的?  以及【Mybatis】【三】源码分析- MapperFactoryBean 的创建过程以及 Mapper 接口代理的生成过程详解 那么这节你就能很好的理解,因为他俩的注入过程太像了,基本都差不多。大体都是先扫描包、然后注入FactoryBean 形式的 BeanDefinition 来实现的。

那么废话不多说,我们直接看。

2  源码分析

2.1  入口 EnableFeignClients 

起点在哪里,是不是我们的启动类上,是不是会配置基本的扫描包,@EnableFeignClients 它就是入口。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {

可以看到它引入了 FeignClientsRegistrar,从名称上就可以看出,它就是 FeignClient 的注册器。

2.2  注册器 FeignClientsRegistrar 

那我们看看注册器里都干了什么:

// 可以看到它实现了 ImportBeanDefinitionRegistrar
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {// 注册 BeanDefinitionpublic void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {// 注册默认配置  @FeignClient 注解有个属性是 defaultConfiguration 自定义全局默认配置比如可以配置你自己的重试机制、编码器、解码器等也就是控制 @FeignClient 的行为的(通常不需要配)this.registerDefaultConfiguration(metadata, registry);// 注册 @FeignClientthis.registerFeignClients(metadata, registry);}
}

如上可以看到大概两部分,一部分是默认的全局配置、一部分是注册我们的 @FeignClient。

2.2.1  全局默认配置类 registerDefaultConfiguration 

这个我们就先草草略过:

// @see FeignClientsRegistrar#registerDefaultConfiguration
private void registerDefaultConfiguration(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {Map<String, Object> defaultAttrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName(), true);if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {String name;if (metadata.hasEnclosingClass()) {name = "default." + metadata.getEnclosingClassName();} else {name = "default." + metadata.getClassName();}this.registerClientConfiguration(registry, name, defaultAttrs.get("defaultConfiguration"));}
}
// @see FeignClientsRegistrar#registerClientConfiguration
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, Object configuration) {// FeignClientSpecificationBeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(FeignClientSpecification.class);builder.addConstructorArgValue(name);builder.addConstructorArgValue(configuration);registry.registerBeanDefinition(name + "." + FeignClientSpecification.class.getSimpleName(), builder.getBeanDefinition());
}

2.2.2  注册 registerFeignClients

我们瞅瞅具体都做了哪些内容:

// 开始注册 FeignClient
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {// 存放候选的 FeignClient 的 BeanDefinitionLinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet();// 获取注解信息Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());// @EnableFeignClients 可以直接配置 Client 我们平时基本不配这个,因为当存在大量的 Client 的话那不是费劲么,每新加一个还要配一个多麻烦 所以我们平时配一个或者多个包名    Class<?>[] clients = attrs == null ? null : (Class[])((Class[])attrs.get("clients"));// 当配置了 Client 属性值的话,用 Annotated 的 BeanDefinition 包起来if (clients != null && clients.length != 0) {Class[] var12 = clients;int var14 = clients.length;for(int var16 = 0; var16 < var14; ++var16) {Class<?> clazz = var12[var16];candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));}} else {// 扫描器ClassPathScanningCandidateComponentProvider scanner = this.getScanner();scanner.setResourceLoader(this.resourceLoader);// 指定扫描的筛子 也就是过滤出 @FeignClient 注解的类scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));// 获取注解上配的包名Set<String> basePackages = this.getBasePackages(metadata);// 逐个循环扫描包下的类Iterator var8 = basePackages.iterator();while(var8.hasNext()) {String basePackage = (String)var8.next();// 都加入到集合中
            candidateComponents.addAll(scanner.findCandidateComponents(basePackage));}}// 对集合中的每个类进行注入Iterator var13 = candidateComponents.iterator();while(var13.hasNext()) {BeanDefinition candidateComponent = (BeanDefinition)var13.next();// 对每一个进行基础检查 然后进行注入if (candidateComponent instanceof AnnotatedBeanDefinition) {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 = this.getClientName(attributes);// 每个 @FeignClient 也可以设置自己的配置this.registerClientConfiguration(registry, name, attributes.get("configuration"));// 注册 FeignClientthis.registerFeignClient(registry, annotationMetadata, attributes);}}
}private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, Object configuration) {BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(FeignClientSpecification.class);builder.addConstructorArgValue(name);builder.addConstructorArgValue(configuration);registry.registerBeanDefinition(name + "." + FeignClientSpecification.class.getSimpleName(), builder.getBeanDefinition());
}

可以看到会从我们配置的包下扫描出 @FeignClient 的接口,然后进行注入,并且每个 Client 也都会注入一个自己的 ClientConfiguration,即使没有配置也会注入一个这样的 BeanDefinition 哈。

那我们继续看下 FeignClient 的具体注入:

// annotationMetadata 就是 @FeignClient 注解信息
// attributes 就是每个属性的值
private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {// 我们的 @FeignClient 下的接口名String className = annotationMetadata.getClassName();Class clazz = ClassUtils.resolveClassName(className, (ClassLoader)null);ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory ? (ConfigurableBeanFactory)registry : null;// 我们常设置的 contextId 和 name  contextId没有设置的会拿 name 属性的值  name 属性的值又会先获取 serviceId 的值,没有的话拿 name 再没有的话会拿 value  并且他俩都可以设置表达式 ${}这种写法 会根据环境变量等配置属性解析的String contextId = this.getContextId(beanFactory, attributes);String name = this.getName(attributes);// 嘿嘿, 外壳是用 FeignClientFactoryBean 来包装的FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();factoryBean.setBeanFactory(beanFactory);factoryBean.setName(name);factoryBean.setContextId(contextId);factoryBean.setType(clazz);// 是否开启配置动态刷新这个跟 feign.client.refresh-enabled 有关默认是 falsefactoryBean.setRefreshableClient(this.isClientRefreshEnabled());BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> {factoryBean.setUrl(this.getUrl(beanFactory, attributes));factoryBean.setPath(this.getPath(beanFactory, attributes));factoryBean.setDecode404(Boolean.parseBoolean(String.valueOf(attributes.get("decode404"))));Object fallback = attributes.get("fallback");// 两个异常钩子if (fallback != null) {factoryBean.setFallback(fallback instanceof Class ? (Class)fallback : ClassUtils.resolveClassName(fallback.toString(), (ClassLoader)null));}Object fallbackFactory = attributes.get("fallbackFactory");if (fallbackFactory != null) {factoryBean.setFallbackFactory(fallbackFactory instanceof Class ? (Class)fallbackFactory : ClassUtils.resolveClassName(fallbackFactory.toString(), (ClassLoader)null));}return factoryBean.getObject();});// 装配模式=2 也就是根据类型装配 和 mabatis 里的 MapperFactoryBean 异曲同工definition.setAutowireMode(2);// 延迟加载的 也就是没有人依赖这个 FeignClient 它就不会创建  很合理definition.setLazyInit(true);// 校验属性 主要是检查两个失败回调的设置this.validate(attributes);AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();beanDefinition.setAttribute("factoryBeanObjectType", className);beanDefinition.setAttribute("feignClientsRegistrarFactoryBean", factoryBean);// 哟 还能设置主的boolean primary = (Boolean)attributes.get("primary");beanDefinition.setPrimary(primary);String[] qualifiers = this.getQualifiers(attributes);if (ObjectUtils.isEmpty(qualifiers)) {qualifiers = new String[]{contextId + "FeignClient"};}BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers);BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);// 如果开启了动态配置刷新的话  会更改 BeanDefinition 的 scope 来进行增强this.registerOptionsBeanDefinition(registry, contextId);
}

别看这么二三十行代码,它东西还挺全乎,我们看看它取了哪些重要属性哈:

(1)contextId 

取的就是注解里的 contextId 属性值,没有的话就等于 name 的属性值,并且它配有 BeanFactory 的 resolve,也就是说支持 spring 的表达式 类似我们平时 @Value 或者配置文件里的表达式写法

private String getContextId(ConfigurableBeanFactory beanFactory, Map<String, Object> attributes) {String contextId = (String)attributes.get("contextId");// 没有 contextId 那么 contextId 就等于 name 属性的值if (!StringUtils.hasText(contextId)) {return this.getName(attributes);} else {// 设置了的话,通过 getName 检查一下 检查的方式主要是 加上协议前缀然后 URI.host 一下 这是为什么的  我不得其解contextId = this.resolve(beanFactory, contextId);return getName(contextId);}
}
static String getName(String name) {if (!StringUtils.hasText(name)) {return "";} else {String host = null;try {String url;if (!name.startsWith("http://") && !name.startsWith("https://")) {url = "http://" + name;} else {url = name;}host = (new URI(url)).getHost();} catch (URISyntaxException var3) {}Assert.state(host != null, "Service id not legal hostname (" + name + ")");return name;}
}

(2)name

我们继续看看 name 的获取,可以看到顺序获取的优先级:serviceId > name > value:

String getName(Map<String, Object> attributes) {return this.getName((ConfigurableBeanFactory)null, attributes);
}
// 重载
String getName(ConfigurableBeanFactory beanFactory, Map<String, Object> attributes) {// 先获取 serviceId 的属性值String name = (String)attributes.get("serviceId");// 没有的话 再获取 name的值if (!StringUtils.hasText(name)) {name = (String)attributes.get("name");}// 还没有的话 获取 value的值if (!StringUtils.hasText(name)) {name = (String)attributes.get("value");}// 表达式解析 虽然beanFactory为空,它有 environment 进行解析name = this.resolve(beanFactory, name);// 一样还是要检查一下 URI.host  还是不得其解return getName(name);
}
static String getName(String name) {if (!StringUtils.hasText(name)) {return "";} else {String host = null;try {String url;if (!name.startsWith("http://") && !name.startsWith("https://")) {url = "http://" + name;} else {url = name;}host = (new URI(url)).getHost();} catch (URISyntaxException var3) {}Assert.state(host != null, "Service id not legal hostname (" + name + ")");return name;}
}

(3)url 的获取

private String getUrl(ConfigurableBeanFactory beanFactory, Map<String, Object> attributes) {// 一样解析表达式String url = this.resolve(beanFactory, (String)attributes.get("url"));// url 通过 URL 解析 这个我能理解 上边那俩我就不太理解为什么要用 URL.hostreturn getUrl(url);
}
static String getUrl(String url) {if (StringUtils.hasText(url) && (!url.startsWith("#{") || !url.contains("}"))) {// 会补 http://
        if (!url.contains("://")) {url = "http://" + url;}try {new URL(url);} catch (MalformedURLException var2) {throw new IllegalArgumentException(url + " is malformed", var2);}}return url;
}

(4)path 路径的获取

private String getPath(ConfigurableBeanFactory beanFactory, Map<String, Object> attributes) {String path = this.resolve(beanFactory, (String)attributes.get("path"));return getPath(path);
}
static String getPath(String path) {if (StringUtils.hasText(path)) {// 去除多余空格path = path.trim();// 贴心自动补 / 前缀if (!path.startsWith("/")) {path = "/" + path;}// 去掉最后的 /if (path.endsWith("/")) {path = path.substring(0, path.length() - 1);}}return path;
}

BeanDefinition 是 FactoryBean 方式的,类型为FeignClientFactoryBean,还有 lazyInit 为 true 表示懒加载开启,装配模式=2表示根据类型自动装配,当多个冲突的时候还可以设置 Primary,最后还有一个重要的就是配置热加载的,这个就涉及到 spring 中Scope 的原理哈。这里就不详细解释了,后序单独讲这个 Scope。

那么最后我们画个图小小的捋一下:

大家看源码一定要画图,真的,不画图的话你后续再回忆的时候,又要一点点再看起,这样当你看完画个思路图,下次看的时候直接看图就大概能想起来怎么个过程,是不是。

3  小结

好啦,本节就看到这里,下节我们看看代理的创建以及执行过程,有理解不对的地方欢迎指正哈。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ulsteruni.cn/article/07650824.html

如若内容造成侵权/违法违规/事实不符,请联系编程大学网进行投诉反馈email:xxxxxxxx@qq.com,一经查实,立即删除!

相关文章

C程序引申到编译器的过程

C程序引申到编译器的过程 MLIR与编译 主要内容: MLIR 控制流图(CFG) 静态单一分配(SSA) 数据流分析 汇编 mruby是用C编写的,因此每个操作码背后的逻辑都是用C实现的。为了从字节码编译Ruby程序,可以使用mruby C API的等价C程序。 某些操作码具有直接的API对应项,例如,…

UVM - 19 (callback)

内容改变UVM组件的功能行为pre_send - 用于注入error post_send - 用于收集coverage编写代码实现简单的回调操作定义一个new_driver extends driver,重写其中的pre_send和post_send方法UVM_callbacks不需要创建复杂的oop结构factory进行组件的覆盖 callbacks不是组件覆盖只是数…

UVM - 18(Review and Lab4)

testMakefileMakefile中定义变量,在运行Makefile的时候可以传入参数make verbosity=UVM_HIGHpacket继承自uvm_sequence_item 定义随机变量并创建约束test_baseVirtual interfaceinterface不能在class中进行例化,需要使用virtual interface virtual interface需要和实际的interf…

html5文本标签

标题文本 h1、h2、h3、h4、h5、h6 其中 h1、h2、h3是比较常用的。h3、h4、h5、h6相对来说用的会少一点,除非结构层次比较深才会使用。 段落文本 p <p>这是一个段落<p/> 强调文本 strong 和 em 尽量避免使用b代替strong,使用i代替em。它们表示的含义不一样 <s…

PyTorch 深度学习(GPT 重译)(六)

十四、端到端结节分析,以及接下来的步骤 本章内容包括连接分割和分类模型为新任务微调网络将直方图和其他指标类型添加到 TensorBoard从过拟合到泛化在过去的几章中,我们已经构建了许多对我们的项目至关重要的系统。我们开始加载数据,构建和改进结节候选的分类器,训练分割模…

Rasterization

三角面片 为什么是三角面片?三角形是最基本的多边形 保证在同一个面上 非常好的定义内外 非常容易定义插值光栅化 重要问题之怎么判断像素的中心点在三角形内? 叉积的结果应该同号 AliasingJaggies【锯齿】 Moire【摩尔纹】 Wagon Wheel Illusion 。。。。原因 最根本的原因是…