首页 > 编程语言 >【OpenFeign】@FeignClient 注入过程源码分析

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

时间:2024-03-21 09:00:12浏览次数:26  
标签:FeignClient name OpenFeign url 源码 registry attributes String

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 {
    // 注册 BeanDefinition
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        // 注册默认配置  @FeignClient 注解有个属性是 defaultConfiguration 自定义全局默认配置比如可以配置你自己的重试机制、编码器、解码器等也就是控制 @FeignClient 的行为的(通常不需要配)
        this.registerDefaultConfiguration(metadata, registry);
        // 注册 @FeignClient
        this.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) {
    // FeignClientSpecification
    BeanDefinitionBuilder 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 的 BeanDefinition
    LinkedHashSet<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"));
            // 注册 FeignClient
            this.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 有关默认是 false
    factoryBean.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.host
    return 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  小结

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

标签:FeignClient,name,OpenFeign,url,源码,registry,attributes,String
From: https://www.cnblogs.com/kukuxjx/p/18086527

相关文章

  • 个人主页HTML5网站源码个性炫酷大气简洁唯美官网上线倒计时引导页HTML5源码
    源码介绍:个性化官宣个人主页,个人主页HTML5网站源码个性炫酷大气简洁唯美官网上线倒计时引导页HTML5源码,超好看的个人官网展示页面。源码下载:个性炫酷大气简洁唯美官网上线倒计时引导页HTML5源码下载界面预览:......
  • 基于springboot实现校园管理系统的设计与实现演示【附项目源码+论文说明】
    基于springboot实现校园管理系统的设计与实现演示摘要随着科学技术的飞速发展,社会的方方面面、各行各业都在努力与现代的先进技术接轨,通过科技手段来提高自身的优势,校园管理系统当然也不能排除在外。校园管理系统是以实际运用为开发背景,运用软件工程原理和开发方法,采用sp......
  • 基于SpringBoot实现旅游网站管理系统项目演示【附项目源码+论文说明】
    基于SpringBoot实现旅游网站管理系统项目演示摘要随着科学技术的飞速发展,各行各业都在努力与现代先进技术接轨,通过科技手段提高自身的优势,旅游网站当然也不能排除在外,随着旅游网站的不断成熟,它彻底改变了过去传统的旅游网站方式,不仅使旅游管理难度变低了,还提升了旅游网站......
  • Java如何修改框架源码(以ZooKeeper框架为例)
    1、缘由:在Zookeeper框架内部源码中,org.apache.zookeeper.ClientCnxn.SendThread#logStartConnect方法会打印客户端与服务器端的连接状态,如果在网络出现波动时会出现连接异常并在日志中打印出INFO级别信息【java.lang.IllegalArgumentException】,而这个关键词会触发运维告警。2......
  • 高性能、可扩展、支持二次开发的企业电子招标采购系统源码
    在数字化时代,企业需要借助先进的数字化技术来提高工程管理效率和质量。招投标管理系统作为企业内部业务项目管理的重要应用平台,涵盖了门户管理、立项管理、采购项目管理、采购公告管理、考核管理、报表管理、评审管理、企业管理、采购管理和系统管理等多个方面。该系统以项目为......
  • 深入了解鸿鹄工程项目管理系统源码:功能清单与项目模块的深度解析
     工程项目管理软件是现代项目管理中不可或缺的工具,它能够帮助项目团队更高效地组织和协调工作。本文将介绍一款功能强大的工程项目管理软件,该软件采用先进的Vue、Uniapp、Layui等技术框架,涵盖了项目策划决策、规划设计、施工建设到竣工交付、总结评估、运维运营等全过程。通过......
  • FUXA源码启动
    一、环境安装安装NVMNVM安装与配置超详细教程_nvmcsdn-CSDN博客利用NVM安装node14.21.3(github文档建议)二、启动项目以管理员运行CMD,在源码目录下进行如下操作:cd./server​npminstall​npmstart三、可能出现的问题Error:CouldnotfindanyVisualStudio......
  • 【附源码】java计算机毕设基于语言的在线电子书阅读系统(源码+开题)
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景随着信息技术的迅猛发展,数字化阅读已成为人们获取知识和信息的重要途径。电子书以其便捷、环保、更新迅速等特点,逐渐替代了传统纸质书籍,成为大众阅读......
  • 【附源码】java计算机毕设基于网上书店的设计与实现(源码+开题)
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景随着互联网技术的迅猛发展,电子商务已成为当今社会经济发展的重要引擎。网上书店作为电子商务的一种形式,以其便捷性、高效性和广泛覆盖性,正逐渐改变着......
  • 【附源码】java计算机毕设基于通识课程管理系统(源码+开题)
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景随着信息技术的飞速发展,高等教育管理逐步迈向数字化、信息化。通识课程作为高等教育的重要组成部分,其管理效率和质量直接关系到学生的学习体验和学校......