Spring Cloud组件源码之OpenFeign源码分析

news2024/11/24 17:41:58

" Spring 到底是春天的来临万物复苏,还是春转夏的干燥又炎热呢?"  Spring的来临让JavaEE走向了另一个高度。便捷的开发,完美的生态。物极必反,学习Spring的成本越来越低,导致Java程序员越来越密集,越来越廉价…… 那么又有多少 Springer 耐下性子去研究Spring生态的架构和底层实现的细节呢??

版本信息如下:

Spring:5.3.23
Spring Boot:2.6.13
Spring Cloud:3.1.5
Spring Cloud Openfeign:3.1.5
Feign:11.3.0

正文:

既然是研究Spring Cloud源码,那么没有Spring、Spring Boot的前置知识,只会让你永远迈不出第一步。下面是对于Spring Cloud的架构图。

既然是学习OpenFeign源码,我们第一步是不是应该清楚Openfeign的架构,由上图我们得知Spring Cloud 组件名 承上启下,向下兼容组件,向上实现规范和扩展。那么把Spring Cloud Openfeign带入进去就很好理解了,向下兼容Feign这个组件,向上实现规范和扩展。

反观,我们连Feign这个组件的原理都不知道,就去学习Spring Cloud Openfeign那怎么学得懂呢?下面是Feign这个组件的架构思想图。一言以蔽之:简化Http客户端的操作,兼容众多Http客户端框架,向上暴露出接口+注解+配置的简易操作方式。

先学会使用,再来研究其源码,Spring Cloud Feign的使用很简单,其实单独使用Feign这个组件跟其大同小异。接口+注解的使用方法,底层帮开发者自动去调用服务名为deptmanagecloud-provider其中的/provider/pt接口。

//@Service
@FeignClient(value = "deptmanagecloud-provider")
public interface FeignConsumer {
    @GetMapping("/provider/pt")
    String provider();
}

那么,接下来就是去研究其源码,到底如何帮开发者自动去调用的过程。

项目中要使用Spring Cloud OpenFeign就需要使用@EnableFeignClients注解。 所以这将是我们的入手点。

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

此注解配合@Configuration注解一起使用,一般是配合@SpringBootApplication注解(也是一个@Configuration)使用,而@EnableFeignClients注解又使用了@Import注解。所以需要找到FeignClientsRegistrar类的registerBeanDefinitions回掉方法。

class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        registerDefaultConfiguration(metadata, registry);
        registerFeignClients(metadata, registry);
    }


    public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();
        Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
        final Class<?>[] clients = attrs == null ? null : (Class<?>[]) attrs.get("clients");
        // 扫描当前目录下所有的@FeignClient类。
        if (clients == null || clients.length == 0) {
            ClassPathScanningCandidateComponentProvider scanner = getScanner();
            scanner.setResourceLoader(this.resourceLoader);
            scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
            Set<String> basePackages = getBasePackages(metadata);
            for (String basePackage : basePackages) {
                candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
            }
        }
        else {
            for (Class<?> clazz : clients) {
                candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
            }
        }

        // 遍历当前目录下所有的@FeignClient类生成的BeanDefinition
        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);
                registerClientConfiguration(registry, name, attributes.get("configuration"));
                // 给当前目录下所有的@FeignClient类生成的BeanDefinition添加Feign动态代理后的代理Class
                registerFeignClient(registry, annotationMetadata, attributes);
            }
        }
    }

}

Spring Cloud Openfein的核心原理大致就在这里了,所以对以上做个总结:

  1. 使用@EnableFeignClients注解,Spring底层会解析@Import注解,最终会回掉registerBeanDefinitions方法,此方法中调用registerFeignClients
  2. registerFeignClients方法中会去扫描当前路径下所有的类,如果类上带有@FeignClient注解就会生成BeanDefition。
  3. 遍历所有带有@FeignClient注解的类,反射拿到@FeignClient注解的元数据信息,然后调用registerFeignClient方法
  4. registerFeignClient方法会解析带有@FeignClient注解元数据信息当作燃料往Feign里面填充,然后Feign会去动态代理创建出带有@FeignClient注解的类的动态代理类。

看到这里,能力比较强的读者就能明白,Spring Cloud Openfeign作为中间层,向上实现规范和扩展接口,向下兼容组件本身。拿这里举例子,Spring Cloud Openfein使用Spring的扩展点@Import来解析@FeignClient注解,把@FeignClient注解元数据信息作为燃料,填充给Feign组件本身,Feign组件获取到燃料后动态代理带有@FeignClient注解的类。在后续的依赖注入和方法调用都是走代理对象,此时我们只需要把Feign组件中动态代理的流程和代理回掉弄明白即可。

那么,接着看到registerFeignClient方法的实现。

private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata,
    Map<String, Object> attributes) {
    String className = annotationMetadata.getClassName();
    Class clazz = ClassUtils.resolveClassName(className, null);
    ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory
    ? (ConfigurableBeanFactory) registry : null;
    String contextId = getContextId(beanFactory, attributes);
    String name = getName(attributes);
    FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();
    factoryBean.setBeanFactory(beanFactory);
    factoryBean.setName(name);
    factoryBean.setContextId(contextId);
    factoryBean.setType(clazz);
    factoryBean.setRefreshableClient(isClientRefreshEnabled());
    // 这里是Spring的一个扩展点,最终创建Bean对象的时候,会回掉此处。
    BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> {
        factoryBean.setUrl(getUrl(beanFactory, attributes));
        factoryBean.setPath(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(), null));
        }
        Object fallbackFactory = attributes.get("fallbackFactory");
        if (fallbackFactory != null) {
            factoryBean.setFallbackFactory(fallbackFactory instanceof Class ? (Class<?>) fallbackFactory
                : ClassUtils.resolveClassName(fallbackFactory.toString(), null));
        }
        return factoryBean.getObject();
    });

    BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers);
    BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);

}

这里很简单,拿到了@FeginClient注解的元数据信息,创建了FeignClientFactortyBean对象,把数据全部注入到FeignClientFactortyBean对象中,并且这里使用了Spring的instanceSupplier扩展点,也即最终创建Bean对象的时候,会把创建的权利交给开发者自定义。所以最终创建的对象就是FeignClientFactortyBean对象的getObject方法。

public class FeignClientFactoryBean
implements FactoryBean<Object>, InitializingBean, ApplicationContextAware, BeanFactoryAware {


    @Override
    public Object getObject() {
        return getTarget();
    }

    <T> T getTarget() {
        // 从工厂中拿到FeignContext对象。
        FeignContext context = beanFactory != null ? beanFactory.getBean(FeignContext.class)
        : applicationContext.getBean(FeignContext.class);

        // 从上下文中创建出Feign.Builder对象,
        // 这里需要补充一下前置知识,Feign.Builder用于产出Feign对象,而Feign对象就是请求的上下文对象。
        Feign.Builder builder = feign(context);

        if (!StringUtils.hasText(url)) {
            …………
            // 使用Feign创建出动态代理的类
            return (T) loadBalance(builder, context, new HardCodedTarget<>(type, name, url));
        }

        …………

        // 使用Feign创建出动态代理的类
        Targeter targeter = get(context, Targeter.class);
        return (T) targeter.target(this, builder, context, new HardCodedTarget<>(type, name, url));
    }

    protected Feign.Builder feign(FeignContext context) {
        FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
        Logger logger = loggerFactory.create(type);

        // 从Spring工厂中拿到Feign组件中一个个可自定义配置的组件
        // 而我们啥时候配置过这些自定义的组件呢?
        // 没错,就是Spring boot帮我们自动配置的。
        // 所以只需要找到Spring boot帮我们自动配置的组件即可
        // 并且也能够明白,我们可以通过@Configuration+@Bean的方式自定义Feign的组件
        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;
    }
}

这里非常的简单,创建出Feign.Builder对象,此对象用于构建出Feign对象,而Feign对象作为请求的整个上下文。而我们需要把之前@FeignClient注解的燃料交给Feign,还有Spring boot帮我们自动配置的燃料也一同交给Feign,所以这里feign()方法从Spring的工厂中拿到Spring boot自动配置的组件。下面图来自Spring Cloud Openfeign中spring.factory自动配置文件自动配置的类。所以再再再再再一次证明了Spring Cloud Openfeign是承上启下。向上对接规范和扩展接口,向下对接组件本身

 

接下来,我们直接看向重点,如何动态代理和方法拦截的即可。

public abstract class Feign {
    // build方法构建出ReflectiveFeign类
    // 然后调用ReflectiveFeign类的newInstance方法
    public <T> T target(Target<T> target) {
        return build().newInstance(target);
    } 

    public Feign build() {
        super.enrich();

        SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
        new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors,
            responseInterceptor, logger, logLevel, dismiss404, closeAfterDecode,
            propagationPolicy, forceDecoding);
        ParseHandlersByName handlersByName =
        new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,
            errorDecoder, synchronousMethodHandlerFactory);
        return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
    }
}


 这里把所有Feign中组件准备好,构建出ReflectiveFeign类,然后调用newInstance方法。

@Override
public <T> T newInstance(Target<T> target) {
    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)));
    }
}
// 创建出JDK动态代理中的拦截器对象
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;
}

这里可以看到创建出JDK动态代理中所需的拦截器对象InvocationHandler,然后调用Proxy.newProxyInstance方法创建出代理类的实例对象。所以接下来看到如何拦截并且处理的就完美的闭环了。

@Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      if ("equals".equals(method.getName())) {
        try {
          Object otherHandler =
              args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
          return equals(otherHandler);
        } catch (IllegalArgumentException e) {
          return false;
        }
      } else if ("hashCode".equals(method.getName())) {
        return hashCode();
      } else if ("toString".equals(method.getName())) {
        return toString();
      }

      return dispatch.get(method).invoke(args);
    }


@Override
  public Object invoke(Object[] argv) throws Throwable {
    RequestTemplate template = buildTemplateFromArgs.create(argv);
    Options options = findOptions(argv);
    Retryer retryer = this.retryer.clone();
    while (true) {
      try {
        return executeAndDecode(template, options);
      } catch (RetryableException e) {
        try {
          retryer.continueOrPropagate(e);
        } catch (RetryableException th) {
          Throwable cause = th.getCause();
          if (propagationPolicy == UNWRAP && cause != null) {
            throw cause;
          } else {
            throw th;
          }
        }
        if (logLevel != Logger.Level.NONE) {
          logger.logRetry(metadata.configKey(), logLevel);
        }
        continue;
      }
    }
  }

// 执行,也即调用某个Http客户端的http请求
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
    Request request = targetRequest(template);
        …………
    response = client.execute(request, options);
        …………
  }

最终回掉到SynchronousMethodHandler的invoke方法中,可以很清楚的看到内部就开始使用某个Http客户端,发送Http请求了。

总结:

能够理解,框架的定位,框架向上兼容什么,向下兼容什么,这对于理解源码是非常重要的一个部分。

最后,如果本帖对您有一定的帮助,希望能点赞+关注+收藏!您的支持是给我最大的动力,后续会一直更新各种框架的使用和框架的源码解读~!

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

实习不对口,还去吗?

作者&#xff1a;阿秀校招八股文学习网站&#xff1a;https://interviewguide.cn这是阿秀的第「255」篇原创小伙伴们大家好&#xff0c;我是阿秀。欢迎今年参加秋招的小伙伴加入我的学习圈&#xff0c;目前已经超过 2200 小伙伴加入&#xff01;去年认真准备和走下来的基本都拿…

LBS计算附近的对象:MySQL 空间索引方式

目录1. MySQL空间数据类型的基本介绍1.1 什么是MySQL空间数据类型1.2 有哪些空间数据类型1.3 支持空间数据类型的引擎1.4 坐标系类型2. 存储坐标系的示例代码2.1 geomtry和point都可以存储坐标系&#xff0c;有什么区别呢&#xff1f;2.2 创建测试表2.3 新增坐标2.3 计算两地之…

计组2.1——计算机中的数据

问题&#xff1a;数据如何在计算机中表示&#xff1f; 运算器如何进行数字运算和逻辑运算&#xff1f; 计组2.11.进制转化&#xff1a;2. BCD码3.字符和字符串1.ASCII2.汉字编码3.字符串4.奇偶校验码1. 校验原理3. 奇偶校验5.汉明码6.循环冗余校验码1.进制转化&#xff1a; #me…

【C语言】关于文件操作你知多少?

目录 一.引入 二. 什么是文件 2.1 什么是文件 2.2 程序文件 2.3 数据文件 2.4 文件名 三.文件的打开和关闭 3.1 文件指针 3.2 文件的打开和关闭 四.文件的顺序读写 4.1 函数汇总 4.2 printf/fprintf/sprintf 4.3 scanf/fscanf/sscanf 五. 文件的随机读写 5.1 引入 5.2 fsee…

HTML5 <ins> 标签、HTML5 <link> 标签

HTML5 <ins> 标签 实例 一段带有已删除部分和新插入部分的文本&#xff1a; <p>My favorite color is <del>blue</del> <ins>red</ins>!</p> 尝试一下 浏览器支持 所有主流浏览器都支持 <ins> 标签。 标签定义及使用说明 …

Sentry安装使用(最全最细)

Sentry安装使用(最全最细&#xff0c;包括解决邮箱发送问题&#xff0c;https上传问题&#xff0c;https访问问题&#xff0c;安装此教程配置即可) ##服务器操作系统为2核8G,CentOS7.9 ##安装Docker-ce yum install -y yum-utils \device-mapper-persistent-data \lvm2yum-c…

日撸 Java 三百行day27

文章目录说明day27 Hanoi 塔问题1.思路2.代码3.图示说明 闵老师的文章链接&#xff1a; 日撸 Java 三百行&#xff08;总述&#xff09;_minfanphd的博客-CSDN博客 自己也把手敲的代码放在了github上维护&#xff1a;https://github.com/fulisha-ok/sampledata day27 Hanoi 塔…

部署LAMP架构和论坛

引言 LAMP架构是目前成熟的企业网站应用模式之一&#xff0c;指的是协同工作的一整套系统和相关软件&#xff0c;能够提供动态Web站点服务及其应用开发环境。LAMP是一个缩写词&#xff0c;具体包括Linux操作系统、Apache网站服务器、MySQL数据库服务器、PHP&#xff08;或Perl、…

Win10,详细永久关闭更新方法(附图文)

一、服务设置 1.同时按下键盘 Win R&#xff0c;打开运行对话框&#xff0c;然后输入命令 services.msc &#xff0c;点击下方的“确定”打开服务。 2.找到 Windows Update 这一项&#xff0c;并双击打开。 3.停止该服务&#xff0c;启动类型设置为禁用 4.点击恢复&#…

webrtc入门系列(二)easy_webrtc_server 入门example测试

《webrtc入门系列&#xff08;一&#xff09;easy_webrtc_server 入门环境搭建》 《webrtc入门系列&#xff08;二&#xff09;easy_webrtc_server 入门example测试》 《webrtc入门系列&#xff08;三&#xff09;云服务器coturn环境搭建》 《webrtc入门系列&#xff08;四&…

好用的免费 PDF 密码删除工具有哪些?

被锁定在文档之外可能会令人沮丧&#xff0c;尤其是当唯一挡路的是一个讨厌的弹出窗口要求您输入密码时。 如果您创建了 PDF 文档或有权访问它&#xff0c;您可以尝试一些行之有效的技巧来删除密码保护。 例如&#xff0c;您可以使用网络浏览器或文档阅读器中的打印设置将 PD…

Windows权限提升—令牌窃取、UAC提权、进程注入等提权

Windows权限提升—令牌窃取、UNC提权、进程注入等提权1. 前言2. at本地命令提权2.1. 适用范围2.2. 命令使用2.3. 操作步骤2.3.1. 模拟提权2.3.2. at配合msf提权2.3.2.1. 生成木马文件2.3.2.2. 设置监听2.3.2.3. 设置反弹2.3.2.4. 查看反弹效果3. sc本地命令提权3.1. 适用范围3.…

QT在线换源安装

Win11上Pytorch的安装并在Pycharm上调用PyTorch最新超详细 网上资源越来越多&#xff0c;关于PyTorch的安装教程各式各样&#xff0c;下面我将详细介绍在安装过程中的操作步骤。 经过上述流程图的介绍我们心中对安装过程有了一个大致的轮廓。下面我将对每一步进行细致的说明 步…

深度分析Palantir的投资价值,Palantir2023年将实现强劲反弹?

来源&#xff1a;猛兽财经 作者&#xff1a;猛兽财经 在本文中&#xff0c;猛兽财经将通过对Palantir的股票关键指标、商业模式、盈利能力、影响Palantir2023年股价的关键利好因素等方面&#xff0c;对Palantir进行全面、深度的分析。 Palantir股票的关键指标 自从Palantir(PL…

p74 应急响应-winlinux 分析后门勒索病毒攻击

数据来源 操作系统(windows,linux)应急响应&#xff1a; 常见危害&#xff1a;暴力破解&#xff0c;漏洞利用&#xff0c;流量攻击&#xff0c;木马控制(Webshell,PC 木马等)&#xff0c;病毒感染(挖矿&#xff0c;蠕虫&#xff0c;勒索等)常见分析&#xff1a;计算机账户&…

postman汉化教程

文章目录1. 下载对应版本的postman2.下载对应版本的汉化包2.1. github下载地址 : &#xff08;9.12.2&#xff09;2.2 百度网盘&#xff08;9.12.2&#xff09;3. 打开postman安装位置4. 压缩包解压到/resources目录下5. 重启postman即可汉化成中文了1. 下载对应版本的postman …

Runtime命令参数字符串和数组比较

问题 最近有个问题本地执行 ssh -p 8084 root10.224.122.51 \"ssh -p 22 root192.168.5.157 mkdir -p /opt/dw-release/pdld-admin\"程序执行总是报错&#xff1a; No such file or directory 但是直接在终端执行正常&#xff0c;这就很奇怪。肯定能推出是程序执行…

OSI七层网络模型与TCP/IP四层模型

一、OSI七层网络模型 OSI 七层模型 是国际标准化组织提出一个网络分层模型&#xff0c;其大体结构以及每一层提供的功能如下图所示&#xff1a; 但由于各方面原因&#xff0c;OSI 七层模型并没有被广泛应用&#xff0c;更多的是作为网络分层的一种基础理论模型。 二、TCP/IP…

Oauth2+vue前后端实现单点登录

Oauth2单点登录流程实现 oauth2-demo 介绍 这是一个基于spring cloud oauth2和vue实现的前后端分离的单点登录示例。示例代码:szile/oauth2-demo 后端服务 auth-server 认证服务 ,对应域名:auth.szile.com auth-client 资源服务,对应域名:client.szile.com 角色 浏览器…

肿瘤预测案例中应用自动特征选择

肿瘤预测案例中应用自动特征选择 描述 当特征数量比较多时&#xff0c;模型容易变得更复杂&#xff0c;过拟合的可能性也会增加。这时除了进行降维处理外&#xff0c;还可以通过自动化特征选择选出最重要的部分特征&#xff0c;抛弃对结果影响不大的特征&#xff0c;从而得到…