再学一下Feign的原理

news2024/9/28 1:21:37

简介

Feign是Spring Cloud Netflix组件中的一个轻量级Restful的HTTP服务客户端,它简化了服务间调用的方式。
Feign是一个声明式的web service客户端.它的出现使开发web service客户端变得更简单.使用Feign只需要创建一个接口加上对应的注解, 比如@FeignClient注解。
Feign是一种声明式、模板化的HTTP客户端。在Spring Cloud中使用Fegin,可以做到使用HTTP请求访问远程服务,就像调用本地方法一样,开发者完全无感知,更感知不到HTTP请求。
Feign的特性:

  • 可插拔的注解支持,包含Feign注解
  • 支持可插拔的HTTP编码器和解码器
  • 支持Hystrix和它的Fallback
  • 支持Ribbion的负载均衡
  • 支持HTTP请求和响应的压缩

快速入门

引入jar

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

编码

@FeignClient(
    url = "/test",
    name = "test-api"
)
public interface TestClient {
    @PostMapping(
        value = {"/create"}
    )
    void create(@RequestBody User user);
  }

启动注解扫描

@Configuration
@EnableFeignClients(basePackages = {"com.test"})

添加注解,启动FeignClient注解扫描,跟我们平时使用ComponentScan的道理都是相似的。

使用

    @Resource
    private TestClient testClient;
    
    public void create() {
        User user = new User();
        user.setName("test");
        testClient.create(user);
    }

完成这些操作之后,我们就可以正常的调用其他服务了,是不是很简单,很容易上手?
那么它到底是如何实现的呢?它都帮我们做了什么事情?接下来的内容才是我们需要了解和掌握的内容。

注册

在我们的业务代码中,我们只是定义了一个添加@FeignClient注解的接口,它是通过JDK的动态代理帮我们创建了接口的代理类,通过代理类完成HTTP的远程调用操作。
在上面的demo中,我们说了,想要调用其他服务,必须添加@EnableFeignClients注解,这个注解就是要扫描我们的代码所有标注了@FeignClient的接口,然后添加到容器当中。

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

	String[] value() default {};

	String[] basePackages() default {};

	Class<?>[] basePackageClasses() default {};

	Class<?>[] defaultConfiguration() default {};

	Class<?>[] clients() default {};

}

我们可以自己定义我们要扫描的包路径,加载所有的@FeignClient标识的所有接口。
在这个注解的上面还有一个注解,@Import,这个我想大家应该都知道,是为了导入其他的bean到容器当中,那么Feign的主要加载、扫描、添加注册信息等操作,都是在FeignClientsRegistrar中完成的。

FeignClientsRegistrar

spring框架已经成为了我们日常开发的企业级框架,或者说已经离不开spring的支持了。
FeignClientsRegistrar的处理逻辑就是在调用ApplicationContext的refresh方法的时候,需要初始化一些BeanPostProcessor,在这个过程中,需要处理一些配置类,比如添加了@Configuration注解的类,那么我们使用的Feign开启的时候,就需要这个配置注解。
FeignClientsRegistrar实现了ImportBeanDefinitionRegistrar接口,那么在容器启动时候就会调用它的registerBeanDefinitions方法,做一些前置处理,通常是把一些bean定义信息注册到容器当中,便于后面初始化这个bean。
现在我们主要看一下FeignClientsRegistrar的registerBeanDefinitions方法

registerFeignClients

public void registerFeignClients(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {
		Set<String> basePackages;

		Map<String, Object> attrs = metadata
				.getAnnotationAttributes(EnableFeignClients.class.getName());
		AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
				FeignClient.class);
		。。。省略 //主要是为了获取扫描的包路径,也就是basePackages

		for (String basePackage : basePackages) {
			//查找添加了@FeignClient注解的接口
			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);
					//添加FeignClientSpecification的bean定义信息
					registerClientConfiguration(registry, name,
							attributes.get("configuration"));
					//添加FeignClientFactoryBean的bean定义信息
					//注册的是FactoryBean信息,那么在初始化这个bean的时候,就会调用到getObject方法,代理的逻辑也是在这里面处理的
					registerFeignClient(registry, annotationMetadata, attributes);
				}
			}
		}
	}

registerFeignClient

private void registerFeignClient(BeanDefinitionRegistry registry,
			AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
		String className = annotationMetadata.getClassName();
		//feignclient注解的bean是FeignClientFactoryBean,一个FactoryBean
		BeanDefinitionBuilder definition = BeanDefinitionBuilder
				.genericBeanDefinition(FeignClientFactoryBean.class);
		validate(attributes);
		//设置url路径
		definition.addPropertyValue("url", getUrl(attributes));
		//urlpath
		definition.addPropertyValue("path", getPath(attributes));
		String name = getName(attributes);
		//设置服务名
		definition.addPropertyValue("name", name);
		String contextId = getContextId(attributes);
		//beanname的替代名
		definition.addPropertyValue("contextId", contextId);
		definition.addPropertyValue("type", className);
		definition.addPropertyValue("decode404", attributes.get("decode404"));
		//设置失败机制
		definition.addPropertyValue("fallback", attributes.get("fallback"));
		definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
		definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
		//设置bean的别名
		String alias = contextId + "FeignClient";
		//注解bean定义信息
		BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
	}

到这里呢, 所有的添加@FeignClient注解的接口已经把bean信息注册到了容器中,等待的就是bean的初始化动作.

bean初始化

在bean的初始化中,由于bean之间的相互依赖,一定会调用FeignClientFactoryBean的getObject方法.
在这里插入图片描述
继续跟踪创建代理类的代码

Targeter targeter = get(context, Targeter.class);
		return (T) targeter.target(this, builder, context,
				new HardCodedTarget<>(type, name, url));

type就是我们定义的Feign接口, name和url就是注解中定义的属性.
接着就会调用到FeignBuilder的target方法

public <T> T target(Target<T> target) {
      return build().newInstance(target);
    }

调用build方法封装ReflectiveFeign数据,ReflectiveFeign继承了Feign,后面我们调用FeignClient的时候,就是调用它的内部类FeignInvocationHandler的invoke方法

public Feign build() {
      Client client = Capability.enrich(this.client, capabilities);
      Retryer retryer = Capability.enrich(this.retryer, capabilities);
      //Feign调用链路的拦截器
      List<RequestInterceptor> requestInterceptors = this.requestInterceptors.stream()
          .map(ri -> Capability.enrich(ri, capabilities))
          .collect(Collectors.toList());
      //... 省略
	  //设置SynchronousMethodHandler的内部工厂数据
      SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
          new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
              logLevel, decode404, closeAfterDecode, propagationPolicy, forceDecoding);
      ParseHandlersByName handlersByName =
          new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,
              errorDecoder, synchronousMethodHandlerFactory);
      //创建ReflectiveFeign
      return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
    }

接着调用ReflectiveFeign的newInstance方法,Feign接口的代理类的生成就是在这里产生的,每个方法对应的MethodHandler也是在这里注册好的.

public <T> T newInstance(Target<T> target) {
    //这里就会遍历元数据信息,生成每个方法名对应的MethodHandler,这里的MethodHandler就是通过SynchronousMethodHandler.Factory
    //创建的SynchronousMethodHandler
    //MethodHandler是Fegin定义个的方法处理器
    Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
    //method -> MethodHandler
    Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
    List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
	//遍历Feign接口中定义的所有方法
    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)));
      }
    }
    //一个Feign接口创建一个对应的InvocationHandler
    InvocationHandler handler = factory.create(target, methodToHandler);
    //通过jdk动态代理创建代理对象
    T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
        new Class<?>[] {target.type()}, handler);
	//为默认的方法处理器绑定创建的代理对象
    for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
      defaultMethodHandler.bindTo(proxy);
    }
    return proxy;
  }

InvocationHandler 这里返回的对象还是ReflectiveFeign的内部类FeignInvocationHandler

  static class FeignInvocationHandler implements InvocationHandler {

    private final Target target;
    private final Map<Method, MethodHandler> dispatch;

首先它实现了jdk代理的InvocationHandler 接口,包含2个成员变量,一个目标类, 一个是解析后的方法到MethodHandler的映射关系,便于后面调用的时候,通过dispatch查询对应方法的MethodHandler.
到这里Feign接口的相关注册和初始化动作就结束了.

消费

在这里插入图片描述
懒的画图了,从网上复制了一份, 基本上可以完整的概括了FeignClient的调用流程了.

FeignInvocationHandler

了解jdk动态代理的都知道, 想要实现动态代理,首先要实现InvocationHandler接口, 调用的时候就会执行invoke方法,接下来我们仔细看下invoke方法的执行流程.

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      //... 省略
      //通过初始化好的dispatch,根据对应调用的method查找对应的methodHandler, 从上述的分析过程中,我们看到使用的是
      //SynchronousMethodHandler
      return dispatch.get(method).invoke(args);
    }

SynchronousMethodHandler

追踪到对应的handler上看下

public Object invoke(Object[] argv) throws Throwable {
	//封装RequestTemplate 对象, 它包含http相关请求的url,body,method,等等一下参数
    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) {
          //...
        }
        //....
        continue;
      }
    }
  }

接下来的就是使用Http客户端调用远程服务

  Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
    Request request = targetRequest(template);

    if (logLevel != Logger.Level.NONE) {
      logger.logRequest(metadata.configKey(), logLevel, request);
    }

    Response response;
    long start = System.nanoTime();
    try {
      response = client.execute(request, options);
      // ensure the request is set. TODO: remove in Feign 12
      response = response.toBuilder()
          .request(request)
          .requestTemplate(template)
          .build();
    } catch (IOException e) {
      if (logLevel != Logger.Level.NONE) {
        logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
      }
      throw errorExecuting(request, e);
    }
   //...
  }

http客户端也有很多的实现
在这里插入图片描述
在这里插入图片描述
其中Okhttp和httpclient是自动装配的, 我们只需要配置一下,就可以启用对应的http客户端,缺省配置的情况下,默认使用ApacheHttpClient.

OkHttp

public feign.Response execute(feign.Request input, feign.Request.Options options)
      throws IOException {
    okhttp3.OkHttpClient requestScoped;
    if (delegate.connectTimeoutMillis() != options.connectTimeoutMillis()
        || delegate.readTimeoutMillis() != options.readTimeoutMillis()
        || delegate.followRedirects() != options.isFollowRedirects()) {
      requestScoped = delegate.newBuilder()
          .connectTimeout(options.connectTimeoutMillis(), TimeUnit.MILLISECONDS)
          .readTimeout(options.readTimeoutMillis(), TimeUnit.MILLISECONDS)
          .followRedirects(options.isFollowRedirects())
          .build();
    } else {
      requestScoped = delegate;
    }
    Request request = toOkHttpRequest(input);
    Response response = requestScoped.newCall(request).execute();
    return toFeignResponse(response, input).toBuilder().request(input).build();
  }

这里就是OkHttp的远程服务调用代码, 没有太多的逻辑.

重试

重试默认使用的Retryer.Default
我们先看下它的默认参数

public Default() {
      this(100, SECONDS.toMillis(1), 5);
    }

最大尝试次数是5次, 最大时间间隔是1s, 100ms的重试时间间隔, 每次的interval是动态计算下次尝试需要等待的时间

public void continueOrPropagate(RetryableException e) {
      //超过最大重试次数直接抛出异常
      if (attempt++ >= maxAttempts) {
        throw e;
      }

      long interval;
      if (e.retryAfter() != null) {
        interval = e.retryAfter().getTime() - currentTimeMillis();
        if (interval > maxPeriod) {
          interval = maxPeriod;
        }
        if (interval < 0) {
          return;
        }
      } else {
       //根据时间间隔和重试次数计算下次重试需要等待的时间
        interval = nextMaxInterval();
      }
      try {
        Thread.sleep(interval);
      } catch (InterruptedException ignored) {
        Thread.currentThread().interrupt();
        throw e;
      }
      sleptForMillis += interval;
    }

Feign的初始化和调用基本原来,先分享到这里,下一期分享一下和ribbon的结合,实现负载均衡.

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

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

相关文章

Vue实现自动化平台(二)--实现登录页面首页

上一章&#xff0c;vue项目的创建&#xff1a; Vue脚手架Vue CLI 使用_做测试的喵酱的博客-CSDN博客 github地址&#xff1a;https://github.com/18713341733/vuemiaotest 这个目前只是用来练手的&#xff0c;项目还没成型。等以后我写完了&#xff0c;再更新一下项目链接。…

python基于机器学习的姓名预测性别网页app开发

前言 做这个项目的起因是之前csdn给我推荐了一个问答&#xff1a;基于机器学习的姓名预测性别的手机app开发。我点进去发现已经有人回答了&#xff0c;链接点进去一看&#xff0c;好家伙&#xff0c;这不是查表算概率吗&#xff0c;和机器学习有半毛钱关系。而且我觉得用姓名预…

《Kubernetes部署篇:Ubuntu20.04基于containerd部署kubernetes1.24.12单master集群》

一、架构图 如下图所示&#xff1a; 二、环境信息 主机名K8S版本系统版本内核版本IP地址备注k8s-master-621.24.12Ubuntu 20.04.5 LTS5.15.0-69-generic192.168.1.62master节点k8s-worker-631.24.12Ubuntu 20.04.5 LTS5.15.0-69-generic192.168.1.63worker节点k8s-worker-641…

Fast DDS 介绍

前面已经简要介绍过DDS协议规范了&#xff0c;接下来我们来看一个它的C实现----Fast DDS。 eProsima Fast-DDS是eprosima对于DDS的C实现&#xff0c;这是一个免费开源软件&#xff0c;遵循Apache License 2.0。eProsima Fast DDS在性能&#xff0c;功能和对最新版本RTPS标准&a…

Day17-对象

文章目录一 函数作业讲解二 函数版图书管理系统三 对象一 引入思考二 对象的创建和使用案例1案例2案例3案例4案例5-描述手机案例6-描述一组手机案例7-把对象作为函数的参数一 函数作业讲解 <script>//1编写函数&#xff0c;计算圆的面积和周长&#xff0c;在函数外由用户…

俄罗斯电商平台ozon的崛起,卖家可以使用测评补单方式打造爆款吗?

OZON俗称俄罗斯亚马逊&#xff0c;1998年成立&#xff0c;是俄罗斯唯一的多品类综合B2C电商平台&#xff0c;也是目前欧洲第四大电商市场。 作为俄罗斯互联网公司五强的OZON平台&#xff0c;拥有庞大的消费者群体&#xff0c;从2018年仅为480万人&#xff0c;到2021年就增长到…

【C++进阶】Makefile基础(一)

文章目录1. 环境2. 规则3. 原理4. 伪目标Makefile 其实只是一个指示 make 程序&#xff08;后面简称 make 或有时称之为 make 命令&#xff09;如何为我们工作的命令文件&#xff0c;我们说 Makefile 其实是在说 make&#xff0c;这一点要有很清晰的认识。而对于我们的项目来说…

21-CSS

目录 1.CSS是什么&#xff1f; 2.CSS基本语法 3.CSS类型 3.1.行内样式&#xff08;适用范围最小&#xff09; 3.2.内部样式&#xff08;适用范围适中&#xff09; 3.3.外部样式&#xff08;适用范围最大&#xff09; PS&#xff1a;关于缓存 3.4.多种样式优先级 4.代码…

浅析Linux Socket套接字编程中的 5 个隐患

隐患 1&#xff0e;忽略返回状态 第一个隐患很明显&#xff0c;但它是开发新手最容易犯的一个错误。如果您忽略函数的返回状态&#xff0c;当它们失败或部分成功的时候&#xff0c;您也许会迷失。反过来&#xff0c;这可能传播错误&#xff0c;使定位问题的源头变得困难。 捕…

XTDrone PX4 仿真平台|Ubuntu20.04 环境搭建失败记录

XTDrone PX4 仿真平台|Ubuntu20.04 环境搭建失败记录仿真环境搭建依赖安装ROS安装Gazebo安装MAVROS安装PX4配置仿真环境搭建 依赖安装 在使用apt安装的过程中&#xff08;包括之后ROS的安装&#xff09;&#xff0c;如果出现难以解决的依赖问题&#xff0c;可以使用aptitude …

Revit净高分析的三种方法

关于净高分析&#xff0c;就是在revit绘制模型到一定程度后&#xff0c;需要对构件的空间位置进行分析&#xff0c;如结构层高、管线净高、地下室是否满足人员车辆进出、地上部分是否满足正常的人员设备活动空间、净高是否满足设备进场安装等&#xff0c;它可以让我们提前发现不…

Codepipeline 跨账号访问 Codecommit

背景 大型企业中代码仓库通常存放在各部门开发账户中&#xff0c;而流水线则位于独立 DevOps 账户中。 本文我们将介绍如何创建跨账号访问 Codecommit 代码仓库的 Codepipeline 流水线&#xff0c;即 CodePipeline 调用另一个账号中的 Codecommit 代码仓库。 亚马逊云科技开发…

【Python_Scrapy学习笔记(十二)】基于Scrapy框架实现POST请求爬虫

基于Scrapy框架实现POST请求爬虫 前言 本文中介绍 如何基于 Scrapy 框架实现 POST 请求爬虫&#xff0c;并以抓取指定城市的 KFC 门店信息为例进行展示 正文 1、Scrapy框架处理POST请求方法 Scrapy框架 提供了 FormRequest() 方法来发送 POST 请求&#xff1b; FormReques…

阿里云ECS大测评【免费领阿里云套餐~】

送福利&#xff01;阿里云热门产品免费领&#xff08;含ECS&#xff09;&#xff0c;点击进入&#xff1a;https://click.aliyun.com/m/1000370359/ 本篇博客参与阿里云 ECS实例测评&#xff0c;这里采用阿里的C5.large计算型实例 本次对云服务器ECS产品能力的体验和建议&…

【2023最新】超详细图文保姆级教程:App开发新手入门(4)

之前章节我们已经完成了一个应用项目的导入、代码更新和代码提交和应用打包编译&#xff0c;本章继续讲述一下&#xff0c;如何在开发过程中进行代码的同步联机调试。 7 代码真机调试 7.1 纯静态CSS页面样式查看 代码调试有多种方式&#xff0c;如果是查看纯粹的静态样式&am…

价值迭代求解马尔可夫决策过程

Value Iteration Algorithm 其算法思想是: 在每一个状态s下&#xff0c; 之迭代算法流程如下&#xff1a; 初始化状态价值state value&#xff0c;即对每个状态的价值都赋一个初始值&#xff0c;一般是0 计算每一个状态-动作对的 动作价值函数&#xff0c;通常通过创建一个二维…

项目管理(PMP)考试:易混淆知识点汇总

请点击↑关注、收藏&#xff0c;本博客免费为你获取精彩知识分享&#xff01;有惊喜哟&#xff01;&#xff01; PMP考试中经常出现&#xff0c;但容易混淆的一些内容&#xff0c;包含15个会议、40个分析、6个矩阵、5个清单、5个审计、5个报告、4个分解结构、4个评估、3个日志…

谷歌人机验证Google reCAPTCHA

reCAPTCHA是Google公司推出的一项验证服务&#xff0c;使用十分方便快捷&#xff0c;在国外许多网站上均有使用。它与许多其他的人机验证方式不同&#xff0c;它极少需要用户进行各种识图验证。 它的使用方式如下如所示&#xff0c;只需勾选复选框即可通过人机验证。 虽然简单…

Python项目之中国数据可视化

文章目录关键词一、做什么二、怎么做1、获取数据&&处理数据2、数据库设计&&存储数据3、开发后端接口4、前端页面编写三、效果展示四、总结关键词 PythonDjangoPython网络爬虫echarts可视化阅读者&#xff08;Python基础、Django基础、H5基础&#xff09; 一、…

Linux学习笔记——UDP协议

文章目录UDP协议端格式UDP的特点面向数据报UDP的缓冲区基于UDP的应用层协议UDP使用注意事项16位的UDP校验和的校验原理UDP协议端格式 16位源端口号&#xff1a;数据从哪一个端口发出来的&#xff0c;也就是数据从哪一个进程发送出来的。在编写应用层代码的时候&#xff0c;用ui…