再学一下Feign的原理

article/2025/9/2 10:04: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的道理都是相似的。

使用

    @Resourceprivate 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);。。。省略 //主要是为了获取扫描的包路径,也就是basePackagesfor (String basePackage : basePackages) {//查找添加了@FeignClient注解的接口Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);for (BeanDefinition candidateComponent : candidateComponents) {if (candidateComponent instanceof AnnotatedBeanDefinition) {// verify annotated class is an interfaceAnnotatedBeanDefinition 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,一个FactoryBeanBeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);validate(attributes);//设置url路径definition.addPropertyValue("url", getUrl(attributes));//urlpathdefinition.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);//创建ReflectiveFeignreturn 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 -> MethodHandlerMap<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接口创建一个对应的InvocationHandlerInvocationHandler 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, 从上述的分析过程中,我们看到使用的是//SynchronousMethodHandlerreturn 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 12response = 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://chatgpt.dhexx.cn/article/Qssqq18X.shtml

相关文章

Feign的底层原理

Feign的底层原理 1 EnableFeignClients2 根据接口上的注解创建RequestTemplate3 发送请求 1 EnableFeignClients 这个注解标注在springboot的启动类上,作用是开启feign接口扫描 FeignClientsRegistrar.registerFeignClients()扫描被FeignClient标识的接口生成代理类 public vo…

深入理解Feign之源码解析

转载请标明出处&#xff1a; https://blog.csdn.net/forezp/article/details/73480304 本文出自方志朋的博客 出自方志朋的博客 个人博客纯净版&#xff1a;https://www.fangzhipeng.com/springcloud/2017/08/11/sc-feign-raw.html 什么是Feign Feign是受到Retrofit&#xff…

SpringCloud 中Feign原理(图解)

1 SpringCloud 中Feign原理 1.1 Feign简介 Feign是Netflix公司开源的轻量级rest客户端&#xff0c;使用Feign可以非常方便的实现Http 客户端。Spring Cloud引入Feign并且集成了Ribbon实现客户端负载均衡调用。 1.2 Feign远程调用的基本流程 Feign远程调用&#xff0c;核心就是…

Feign(简介和使用)

1. Feign介绍 通过RestTemplate调用其它服务的API时&#xff0c;所需要的参数须在请求的URL中进行拼接&#xff0c;如果参数少的话或许我们还可以忍受&#xff0c;一旦有多个参数的话&#xff0c;这时拼接请求字符串就会效率低下 Feign是一个声明式的Web Service客户端&#…

什么是Feign?

服务间调用介绍 现有的服务调用方式 利用拼接的方式。 虽然上面有的用不错就很好了 Feign解决了什么问题 Feign的调用方式 Feign体系架构解析-武装到牙齿 上一节我们了解了feign的主要功能&#xff0c;它就像一个自拍杆一样&#xff0c;方便了Eureka的远程调用。可是怎么看…

Feign 的实现原理

Feign 实现原理 Feign是申明式的 HTTP 客户端。代码中创建一个接口并加上FeingClient 注解即可使用。其底层封装了 HTTP 客户端构建并发送的复杂逻辑。同时也可以整合注册中心及 Ribbon 为其提供负载均衡能力&#xff1b;通过整合 Histrix/sentinal 实现熔断限流功能。本期主要…

OpenFeign的实现原理(附Feign和OpenFeign的区别)

目录 问题现象&#xff1a; 问题分析&#xff1a; 拓展&#xff1a; 1、Feign&#xff1a; 2、OpenFeign&#xff1a; 问题现象&#xff1a; 最近在复习SpringCloud的时候看到一个面试题&#xff1a; OpenFeign的实现原理&#xff1f; 问题分析&#xff1a; OpenFeign是Sp…

Feign原理

是一个HTTP请求调用轻量级框架&#xff0c;可以以Java接口注解的方式调用HTTP请求&#xff0c;而不用像Java中通过封装HTTP请求报文的方式直接调用。 Feign解决了什么问题 在服务调用的场景中&#xff0c;我们经常调用基于HTTP协议的服务&#xff0c;而我们经常使用到的框架可…

Feign的使用及原理剖析

feign使用及原理剖析 一、简介 Feign是一个http请求调用的轻量级框架&#xff0c;可以以Java接口注解的方式调用Http请求。Feign通过处理注解&#xff0c;将请求模板化&#xff0c;当实际调用的时候&#xff0c;传入参数&#xff0c;根据参数再应用到请求上&#xff0c;进而转…

Feign基本使用(超详细)

目录 一、Feign概述 二、Feign入门 1.创建服务提供者(provider) 2.创建feign接口 3、创建服务消费者(consumer) 三、Feign 原理 四、Feign优化 1、开启feign日志 2、feign超时问题 3、http连接池 4、gzip压缩 前言 当我们通过RestTemplate调用其它服务的API时&#xff0c;所…

简单理解Feign的原理与使用

文章目录 SpringCloud 总架构图一、简介1.1、负载均衡的概念2.2、Feign概念 二、入门案例2.1、导入依赖2.2、Feign的客户端2.3、调用Feign2.4、开启Feign功能2.5、启动测试2.6、Feign实现原理简单分析 三、负载均衡(Ribbon)四、熔断器支持五、请求压缩和响应压缩六、配置日志级…

【第四章】详解Feign的实现原理

1.1 Feign概述 这篇文章主要讲述如何通过Feign去消费服务&#xff0c;以及Feign的实现原理的解析。 Feign是Netflix开发的声明式、模板化的HTTP客户端&#xff0c;Feign可以帮助我们更快捷、优雅地调用HTTP API。 Feign是⼀个HTTP请求的轻量级客户端框架。通过 接口 注解的…

Feign底层原理分析-自动装载动态代理

本篇文章仅介绍Feign的核心机制&#xff0c;包括如何交由Spring容器托管、动态代理机制等内容&#xff0c;不会过分深入细节。 1、什么是Feign&#xff1f; 这里套用Feign官方Github上的介绍&#xff1a;“Feign是一个灵感来自于Retrofit、JAXRS-2.0、WebSocket的Java Http客户…

Feign原理 (图解)

1.1 简介&#xff1a;Feign远程调用的 Feign远程调用&#xff0c;核心就是通过一系列的封装和处理&#xff0c;将以JAVA注解的方式定义的远程调用API接口&#xff0c;最终转换成HTTP的请求形式&#xff0c;然后将HTTP的请求的响应结果&#xff0c;解码成JAVA Bean&#xff0c;放…

Feign的工作原理

Feign的工作原理 Feign是一个伪Java Http 客户端&#xff0c;Feign 不做任何的请求处理。Feign 通过处理注解生成Request模板&#xff0c;从而简化了Http API 的开发。开发人员可以使用注解的方式定制Request API模板。 在发送Http Request请求之前&#xff0c;Feign通过处理…

Linux命令——tar与gzip详解:文件的打包压缩与解压缩解打包

Linux系统中&#xff0c;最常用的打包命令就是tar了&#xff0c;不仅如此&#xff0c;tar命令还可以解打包解压缩&#xff0c;十分方便。如果单纯想压缩文件&#xff0c;就需要我们的gzip命令了。 使用tar打包归档的包叫做tar包&#xff0c;以.tar结尾 使用gzip压缩的文件&…

Linux 下使用 tar 命令打包指定目录下的所有文件,不包括目录

一&#xff0c;问题描述 无论是 Linux 系统&#xff0c;还是 macOS 系统&#xff0c;我们都可以使用 tar 命令进行文件的压缩打包。命令格式如下&#xff1a; # tar cvf xxxx.tar 要压缩的文件或目录名称但如果要压缩的目录层级比较多时&#xff0c;比如&#xff1a; # tar cvf…

linux tar (打包、压缩、解压)命令

打包程序&#xff1a;tar c: 创建文档t&#xff1a; 列出存档内容x&#xff1a;提取存档f&#xff1a; filename 要操作的文档名v&#xff1a;详细信息 一&#xff1a;打包 打包&#xff1a;是指把文件整合在一起&#xff0c;不压缩 1.将文件打包&#xff1a;tar cf a.ta…

(21)tar打包命令详解

Linux 系统中,最常用的归档(打包)命令就是 tar,该命令可以将许多文件一起保存到一个单独的磁盘中进行归档。不仅如此,该命令还可以从归档文件中还原所需文件,也就是打包的反过程,称为解打包。1.tar命令做打包操作  当 tar 命令用于打包操作时,该命令的基本格式为: …

vector中删除某个指定元素

class Solution { public:int removeElement(vector<int>& v, int val) {for (auto it v.begin(); it ! v.end(); it) {if (*it val) { // 条件语句v.erase(it); // 移除他it--; // 让该迭代器指向前一个}}return v.size();} };