本文来说下关于Feign 的几个问题

文章目录

  • 概述
  • 什么是 Feign
  • 什么是 Open Feign
  • Feign 和 Openfeign 的区别
    • Starter Openfeign
  • 环境准备
    • 生产者服务
    • 消费者服务
  • Feign 的启动原理
    • 注入@Import
    • 添加全局配置
    • 注册 FeignClient 接口
  • Feign 的工作原理
    • FactoryBean 接口特征
    • 初始化父子容器
    • 动态代理生成
  • Feign 如何负载均衡
  • 本文小结

概述

Feign是Netflix开发的声明式、模板化的HTTP客户端, Feign可以帮助我们更快捷、优雅地调用HTTP API

什么是Fegin,在解释之前,我们先梳理一下我们之前学习到的,在微服模式下,解决服务间的调用可以通过Grpc、HttpClient、(Spring中的RestTemplate是对HttpClient的封装)等开源框架,这种调用我们称之为远程过程的调用,即RPC,那么进行RPC调用需要解决几个重要的问题,一个是序列化/反序列化,比如Json/xml等怎样序列化和反序列化等,再一个就是以什么样的协议实现这样的调用。这两个问题在开源社区都有了很好的技术方案。

那么Spring Cloud Fegin主要是为了更简单的实现开发,封装了Http的调用流程,更适合面向接口化编程的习惯。我们虽然能通过Ribbon和RestTemplate通过URL进行远程调用,但是这样拼接参数,并不是特别的优雅,为此,我们可以通过使用Feign让远程调用变的更简洁。

Feign是一个声明式的Web Service客户端


现在的微服务在互联网圈子里应用已经相关广泛了,SpringCloud 是微服务领域当之无愧的 “头牌”。加上现在的一些轮子项目,新建一个全套的 SpringCloud 项目分分钟的事情,而我们要做的事情,就是不把认知停留在使用层面,所以要深入到源码中去理解 SpringCloud,为什么要选择 OpenFiegn? 因为它足够的 “小”,Feign 的源代码中,Java 代码才 3w 多行,放眼现在热门的开源项目,包括不限于 Dubbo、Nacos、Skywalking 中 Java 代码都要 30w 行起步。

通过本篇文章,希望读者朋友可以掌握如下知识

  • 什么是 Feign
  • Feign 和 Openfeign 的区别
  • OpenFeign 的启动原理
  • OpenFeign 的工作原理
  • OpenFeign 如何负载均衡

什么是 Feign

Feign 是声明式 Web 服务客户端,它使编写 Web 服务客户端更加容易。Feign 不做任何请求处理,通过处理注解相关信息生成 Request,并对调用返回的数据进行解码,从而实现 简化 HTTP API 的开发。


什么是 Open Feign

有了 Eureka ,RestTemplate ,Ribbon, 我们就可以愉快地进行服务间的调用了,但是使用 RestTemplate 还是不方便,我们每次都要进行这样的调用

@Autowired
private RestTemplate restTemplate;// 这里是提供者A的ip地址,但是如果使用了 Eureka 那么就应该是提供者A的名称
private static final String SERVICE_PROVIDER_A = "http://localhost:8081";@PostMapping("/judge")
public boolean judge(@RequestBody Request request) {String url = SERVICE_PROVIDER_A + "/service1";// 是不是太麻烦了???每次都要 url、请求、返回类型的 return restTemplate.postForObject(url, request, Boolean.class);
}

这样每次都调用 RestTemplate 的 API 是否太麻烦,我能不能像调用原来代码一样进行各个服务间的调用呢?聪明的同学肯定想到了,那就用 映射 呀,就像域名和IP地址的映射。我们可以将被调用的服务代码映射到消费者端,这样我们就可以 无缝开发啦

OpenFeign 也是运行在消费者端的,使用 Ribbon 进行负载均衡,所以 OpenFeign 直接内置了 Ribbon。

在导入了 Open Feign 之后我们就可以进行愉快编写 Consumer 端代码了

// 使用 @FeignClient 注解来指定提供者的名字
@FeignClient(value = "eureka-client-provider")
public interface TestClient {// 这里一定要注意需要使用的是提供者那端的请求相对路径,这里就相当于映射了@RequestMapping(value = "/provider/xxx", method = RequestMethod.POST)CommonResponse<List<Plan>> getPlans(@RequestBody planGetRequest request);
}

然后我们在 Controller 就可以像原来调用 Service 层代码一样调用它了

@RestController
public class TestController {// 这里就相当于原来自动注入的 Service@Autowiredprivate TestClient testClient;// controller 调用 service 层代码@RequestMapping(value = "/test", method = RequestMethod.POST)public CommonResponse<List<Plan>> get(@RequestBody planGetRequest request) {return testClient.getPlans(request);}
}

Feign 和 Openfeign 的区别

Feign 最早是由 Netflix 公司进行维护的,后来 Netflix 不再对其进行维护,最终 Feign 由社区进行维护,更名为 Openfeign

为了少打几个字,下文简称 Opefeign 为 Feign

并将原项目迁移至新的仓库,所以我们在 Github 上看到 Feign 的坐标如下

<groupId>io.github.openfeign</groupId>
<artifactId>parent</artifactId>
<version>...</version>

Starter Openfeign

当然了,基于 SpringCloud 团队对 Netflix 的情有独钟,你出了这么好用的轻量级 HTTP 客户端,我这老大哥不得支持一下,所以就有了基于 Feign 封装的 Starter。

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

Spring Cloud 添加了对 Spring MVC 注解的支持,并支持使用 Spring Web 中默认使用的相同 HttpMessageConverters。

另外,Spring Cloud 老大哥同时集成了 Ribbon 和 Eureka 以及 Spring Cloud LoadBalancer,以在使用 Feign 时提供负载均衡的 HTTP 客户端。

针对于注册中心的支持,包含但不限于 Eureka,比如 Consul、Naocs 等注册中心均支持

在我们 SpringCloud 项目开发过程中,使用的大多都是这个 Starter Feign


环境准备

为了方便大家理解,这里写出对应的生产方、消费方 Demo 代码,以及使用的注册中心。


注册中心使用的 Nacos,生产、消费方代码都比较简单。另外为了阅读体验感,文章原则是少放源码,更多的是给大家梳理核心逻辑。


生产者服务

添加 Nacos 服务注册发现注解以及发布出 HTTP 接口服务

@EnableDiscoveryClient
@SpringBootApplication
public class NacosProduceApplication {public static void main(String[] args) {SpringApplication.run(NacosProduceApplication.class, args);}@RestControllerstatic class TestController {@GetMapping("/hello")public String hello(@RequestParam("name") String name) {return "hello " + name;}}
}

消费者服务

定义 FeignClient 消费服务接口

@FeignClient(value = "nacos-produce")
public interface DemoFeignClient {@RequestMapping(value = "/hello", method = RequestMethod.GET)String sayHello(@RequestParam("name") String name);
}

因为生产者使用 Nacos,所以消费者除了开启 Feign 注解,同时也要开启 Naocs 服务注册发现

@RestController
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class NacosConsumeApplication {public static void main(String[] args) {SpringApplication.run(NacosConsumeApplication.class, args);}@Autowired private DemoFeignClient demoFeignClient;@GetMapping("/test")public String test() {String result = demoFeignClient.sayHello("hello world");return result;}
}

Feign 的启动原理

我们在 SpringCloud 的使用过程中,如果想要启动某个组件,一般都是 @Enable… 这种方式注入,Feign 也不例外,我们需要在类上标记此注解 @EnableFeignClients

@EnableFeignClients
@SpringBootApplication
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);}
}

继续深入看一下注解内部都做了什么。注解内部的方法就不说明了,不加会有默认的配置,感兴趣可以跟下源码

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

前三个注解看着平平无奇,重点在第四个 @Import 上,一般使用此注解都是想要动态注册 Spring Bean 的


注入@Import

通过名字也可以大致猜出来,这是 Feign 注册 Bean 使用的,使用到了 Spring 相关的接口,一起看下起了什么作用


ResourceLoaderAware、EnvironmentAware 为 FeignClientsRegistrar 中两个属性 resourceLoader、environment 赋值,对 Spring 了解的小伙伴理解问题不大。

ImportBeanDefinitionRegistrar 负责动态注入 IOC Bean,分别注入 Feign 配置类、FeignClient Bean。

// 资源加载器,可以加载 classpath 下的所有文件
private ResourceLoader resourceLoader;
// 上下文,可通过该环境获取当前应用配置属性等
private Environment environment;@Override
public void setEnvironment(Environment environment) {this.environment = environment;
}@Override
public void setResourceLoader(ResourceLoader resourceLoader) {this.resourceLoader = resourceLoader;
}@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {// 注册 @EnableFeignClients 提供的自定义配置类中的相关 Bean 实例registerDefaultConfiguration(metadata,registry);// 扫描 packge,注册被 @FeignClient 修饰的接口类为 IOC BeanregisterFeignClients(metadata, registry);
}

添加全局配置

registerDefaultConfiguration 方法流程如下

  1. 获取 @EnableFeignClients 注解上的属性以及对应 Value
  2. 生成 FeignClientSpecification(存储 Feign 中的配置类) 对应的构造器 BeanDefinitionBuilder
  3. FeignClientSpecification Bean 名称为 default. + @EnableFeignClients 修饰类全限定名称 + FeignClientSpecification
  4. @EnableFeignClients defaultConfiguration 默认为 {},如果没有相关配置,默认使用 FeignClientsConfiguration 并结合 name 填充到 FeignClientSpecification,最终注册为 IOC Bean

注册 FeignClient 接口

将重点放在 registerFeignClients 上,该方法主要就是将修饰了 @FeignClient 的接口注册为 IOC Bean

  1. 扫描 @EnableFeignClients 注解,如果有 clients,则加载指定接口,为空则根据 scanner 规则扫描出修饰了 @FeignClient 的接口
  2. 获取 @FeignClient 上对应的属性,根据 configuration 属性去创建接口级的 FeignClientSpecification 配置类 IOC Bean
  3. 将 @FeignClient 的属性设置到 FeignClientFactoryBean 对象上,并注册 IOC Bean

@FengnClient 修饰的接口实际上使用了 Spring 的代理工厂生成代理类,所以这里会把修饰了 @FeignClient 接口的 BeanDefinition 设置为 FeignClientFactoryBean 类型,而 FeignClientFactoryBean 继承自 FactoryBean。

也就是说,当我们定义 @FeignClient 修饰接口时,注册到 IOC 容器中 Bean 类型变成了 FeignClientFactoryBean。

在 Spring 中,FactoryBean 是一个工厂 Bean,用来创建代理 Bean。工厂 Bean 是一种特殊的 Bean,对于需要获取 Bean 的消费者而言,它是不知道 Bean 是普通 Bean 或是工厂 Bean 的。工厂 Bean 返回的实例不是工厂 Bean 本身,而是会返回执行了工厂 Bean 中 FactoryBean#getObject 逻辑的实例。


Feign 的工作原理

说 Feign 的工作原理,核心点围绕在被 @FeignClient 修饰的接口,如何发送及接收 HTTP 网络请求。

上面说到 @FeignClient 修饰的接口最终填充到 IOC 容器的类型是 FeignClientFactoryBean,先来看下它是什么


FactoryBean 接口特征

这里说一下 FeignClientFactoryBean 都有哪些特征

  1. 它会在类初始化时执行一段逻辑,依据 Spring InitializingBean 接口
  2. 如果它被别的类 @Autowired 进行注入,返回的不是它本身,而是 FactoryBean#getObject 返回的类,依据 Spring FactoryBean 接口
  3. 它能够获取 Spring 上下文对象,依据 Spring ApplicationContextAware 接口

先来看它的初始化逻辑都执行了什么

@Override
public void afterPropertiesSet() {Assert.hasText(contextId, "Context id must be set");Assert.hasText(name, "Name must be set");
}

没有特别的操作,只是使用断言工具类判断两个字段不为空。ApplicationContextAware 也没什么说的,获取上下文对象赋值到对象的局部变量里,重点以及关键就是 FactoryBean#getObject 方法。

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

getTarget 源码方法还是挺长的,这里采用分段的形式展示

<T> T getTarget() {// 从 IOC 容器获取 FeignContextFeignContext context = applicationContext.getBean(FeignContext.class);// 通过 context 创建 Feign 构造器Feign.Builder builder = feign(context);...
}

这里提出一个疑问?FeignContext 什么时候、在哪里被注入到 Spring 容器里的?

看到图片小伙伴就明了了,用了 SpringBoot 怎么会不使用自动装配的功能呢,FeignContext 就是在 FeignAutoConfiguration 中被成功创建


初始化父子容器

feign 方法里日志工厂、编码、解码等类均是通过 get(…) 方法得到


这里涉及到 Spring 父子容器的概念,默认子容器 Map 为空,获取不到服务名对应 Context 则新建。



默认注册如下,FeignClientsConfiguration 是由创建 FeignContext 调用父类 Super 构造方法传入的

关于父子类容器对应关系,以及提供 @FeignClient 服务对应子容器的关系(每一个服务对应一个子容器实例)


回到 getInstance 方法,子容器此时已加载对应 Bean,直接通过 getBean 获取 FeignLoggerFactory

如法炮制,Feign.Builder、Encoder、Decoder、Contract 都可以通过子容器获取对应 Bean

configureFeign 方法主要进行一些配置赋值,比如超时、重试、404 配置等,就不再细说赋值代码了


动态代理生成

继续嗑,上面都是开胃菜,接下来是最最最重要的地方了,小板凳坐板正了


因为我们在 @FeignClient 注解是使用 name 而不是 url,所以会执行负载均衡策略的分支


Client: Feign 发送请求以及接收响应等都是由 Client 完成,该类默认 Client.Default,另外支持 HttpClient、OkHttp 等客户端。

代码中的 Client、Targeter 在自动装配时注册,配合上文中的父子容器理论,这两个 Bean 在父容器中存在

因为我们并没有对 Hystix 进行设置,所以走入此分支


创建反射类 ReflectiveFeign,然后执行创建实例类


newInstance 方法对 @FeignClient 修饰的接口中 SpringMvc 等配置进行解析转换,对接口类中的方法进行归类,生成动态代理类


可以看出 Feign 创建动态代理类的方式和 Mybatis Mapper 处理方式是一致的,因为两者都没有实现类。

根据 newInstance 方法按照行为大致划分,共做了四件事

  1. 处理 @FeignCLient 注解(SpringMvc 注解等)封装为 MethodHandler 包装类
  2. 遍历接口中所有方法,过滤 Object 方法,并将默认方法以及 FeignClient 方法分类
  3. 创建动态代理对应的 InvocationHandler 并创建 Proxy 实例
  4. 接口内 default 方法 绑定动态代理类

MethodHandler 将方法参数、方法返回值、参数集合、请求类型、请求路径进行解析存储


到这里我们也就可以 Feign 的工作方式了。前面那么多封装铺垫,封装个性化配置等等,最终确定收尾的是创建动态代理类

也就是说在我们调用 @FeignClient 接口时,会被 FeignInvocationHandler#invoke 拦截,并在动态代理方法中执行下述逻辑

  1. 接口注解信息封装为 HTTP Request
  2. 通过 Ribbon 获取服务列表,并对服务列表进行负载均衡调用(服务名转换为 ip+port)
  3. 请求调用后,将返回的数据封装为 HTTP Response,继而转换为接口中的返回类型

既然已经明白了调用流程,那就正儿八经的试一哈,试过才知有没有…


RequestTemplate:构建 Request 模版类

Options:存放连接、超时时间等配置类

Retryer:失败重试策略类

重试这一块逻辑看了很多遍,但是怎么看,一个 continue 关键字放到 while 的最后面都有点多余…

执行远端调用逻辑中使用到了 Rxjava (响应式编程),可以看到通过底层获取 server 后将服务名称转变为 ip+port 的方式

这种响应式编程的方式在 SpringCloud 中很常见,Hystix 源码底层也有使用


网络调用默认使用 HttpURLConnection,可以配置使用 HttpClient 或者 OkHttp

调用远端服务后,再将返回值解析正常返回,到这里一个完成的 Feign 调用链就聊明白了。


Feign 如何负载均衡

一般而言,我们生产者注册多个服务,消费者调用时需要使用负载均衡从中 选取一个健康并且可用的生产者服务。


因为 Feign 内部集成 Ribbon,所以也支持此特性,一起看下它是怎么做的


我们在 Nacos 上注册了两个服务,端口号 8080、8081。在获取负载均衡器时就可以获取服务集合


然后通过 chooseServer 方法选择一个健康实例返回,后面会新出一篇文章对 Ribbon 的负载均衡详细说明


通过返回的 Server 替换 URL 中的服务名,最后使用网络调用服务进行远端调用,完美的一匹.。


本文小结

关于Feign的几个问题相关推荐

  1. 快速排查feign.FeignException: status 500 …

    feign.FeignException: status 500 - 总结一下feign报500的时候快速排查问题的方法, 这个bug容易出现的地方分别为: 1. 远程调用的时候feign的注册信息有 ...

  2. 使用feign调用注解在eureka上的微服务,简单学会微服务

    使用feign调用注解在eureka上的微服务. 首先,确保所有服务(调用方与被调用方)都被注册在同一个eureka服务上. 1. 在调用方添加依赖(万事第一步,加依赖) <dependency ...

  3. SpringCloud Alibaba微服务实战(三) - Nacos服务创建消费者(Feign)

    什么是Feign Feign 是一个声明式的伪 Http 客户端,它使得写 Http 客户端变得更简单.使用 Feign,只需要创建一个接口并注解.它具有可插拔的注解特性,可使用 Feign 注解和 ...

  4. 【微服务架构】SpringCloud之Feign

    什么是Feign Feign 是一个声明web服务客户端,这便得编写web服务客户端更容易,使用Feign 创建一个接口并对它进行注解,它具有可插拔的注解支持包括Feign注解与JAX-RS注解,Fe ...

  5. 如何通过HTTP优雅调用第三方-Feign

    Java常用HTTP客户端 Java原生HttpURLConnection Apache HttpClient OkHttp Spring RestTemplate 示例 public interfa ...

  6. SpringCloud Feign声明式服务调用

    SpringCloud Feign声明式服务调用 1. 加入pom依赖 2. Application.java上声明@EnableFeignClients 3. @FeignClient声明接口调用服 ...

  7. 企业分布式微服务云SpringCloud SpringBoot mybatis - 服务消费者(Feign)

    一.Feign简介 Feign是一个声明式的伪Http客户端,它使得写Http客户端变得更简单.使用Feign,只需要创建一个接口并注解.它具有可插拔的注解特性,可使用Feign 注解和JAX-RS注 ...

  8. java B2B2C springmvc mybatis多租户电子商城系统-Spring Cloud Feign

    1.什么是Feign? 愿意了解源码的朋友直接企鹅求求:二一四七七七五六三三 Feign 的英文表意为"假装,伪装,变形", 是一个http请求调用的轻量级框架,可以以Java接口 ...

  9. Feign实现服务调用

    上一篇博客我们使用ribbon+restTemplate实现负载均衡调用服务,接下来我们使用feign实现服务的调用,首先feign和ribbon的区别是什么呢? ribbon根据特定算法,从服务列表 ...

  10. Spring Cloud(四)服务提供者 Eureka + 服务消费者 Feign

    上一篇文章,讲述了如何通过RestTemplate + Ribbon去消费服务,这篇文章主要讲述如何通过Feign去消费服务. Feign简介 Feign是一个声明式的伪Http客户端,它使得写Htt ...

最新文章

  1. javascript闭包学习
  2. 简单案例:unittest+HTMLTestRunner实现生成测试报告
  3. 推荐系统炼丹笔记:边缘计算+奉送20个推荐系统强特
  4. 你真的会用storyboard开发吗?
  5. idea解决activiti(*.bpmn)文件乱码问题。
  6. Android签名详解(debug和release)
  7. 【LeetCode】【HOT】17. 电话号码的字母组合(递归)
  8. python-PyQuery详解
  9. 路由跳转的时候地址栏的地址变了 但是页面不变_斐讯路由器如何设置上网 斐讯路由器设置上网方法【图文】...
  10. 16.Java中的String详解
  11. 数据结构笔记(二)--- 顺序实现线性表
  12. 在linux下用C语言编写贪吃蛇小游戏
  13. static关键字的用法
  14. 微信公众号申请相关问题
  15. 2018冬令营模拟测试赛(十九)
  16. 用大白话聊聊JavaSE -- 自定义注解入门
  17. 一切还要从副总裁在朋友圈卖内裤说起
  18. Halium 9 尝鲜 -- 在小米平板4上的移植 (一)
  19. Android面试题总结(三)
  20. 中国联通dns服务器未响应,关于光猫设置的说明和常见问题

热门文章

  1. Exchange server 2007搭建私有邮件系统测试备忘
  2. C++复习总结(涵盖所有C++基本考点)!
  3. 创建一个简单的ArcGIS Server ASP.NET网页
  4. 東方 project 联机版开发日记(1)
  5. 互联生活:业务模式聚焦
  6. 《iOS 9 开发指南》——第6章,第6.7节iOS 9控件的属性
  7. Golang string处理
  8. SQL Server 调优系列基础篇 - 子查询运算总结
  9. where、having、group by、order by、limit的区别和使用顺序
  10. 云平台队列服务-Qbus实践