一、概述
Feign
是声明式Web
服务客户端,它使编写Web
服务客户端更加容易。
Feign
不做任何请求处理,通过处理注解相关信息生成Request
,并对调用返回的数据进行解码,从而实现简化HTTP API的开发。
如果要使用Feign
,需要创建一个接口并对其添加Feign
相关注解,另外Feign
还支持可插拔编码器和解码器,致力于打造一个轻量级HTTP
客户端。
1.1 Feign和Openfeign的区别
Feign
最早是由Netflix
公司进行维护的,后来Netflix
不再对其进行维护,最终Feign
由社区进行维护,更名为Openfeign
。
为了少打俩字,下文简称Openfeign为Feign。
并将原项目迁移至新的仓库,所以我们在Github
上看到Feign
的坐标如下:
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>parent</artifactId>
<version>...</version>
</dependency>
1.2 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、Nacos等注册中心均支持。
在我们SpringCloud
项目开发过程中,使用的大多都是这个Starter Feign
。
二、案例
为了方便大家理解,这里写出对应的生产方、消费方Demo代码,以及使用的注册中心。
注册中心使用的Nacos
,生产、消费方代码都比较简单。另外为了阅读体验感,文章原则是少放源码,更多的是给大家梳理核心逻辑。
2.1 生产者服务
添加Nacos
服务注册发现注解以及发布出HTTP接口服务。
@EnableDiscoveryClient
@SpringBootApplication
public class NacosProduceApplication {
public static void main(String[] args) {
SpringApplication.run(NacosProduceApplication.class, args);
}
@RestController
static class TestController {
@GetMapping("/hello")
public String hello(@RequestParam("name") String name) {
return "hello " + name;
}
}
}
2.2 消费者服务
定义FeignClient
消费服务接口。
@FeignClient(value = "nacos-produce")
public interface DemoFeignClient {
@RequestMapping(value = "/hello", method = RequestMethod.GET)
String sayHello(@RequestParam("name") String name);
}
因为生产者使用Nacos
,所以消费者除了开启Feign
注解,同时也要开启Nacos
服务注册发现。
@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("Test");
return result;
}
}
三、Feign的启动原理
我们在SpringCloud
的使用过程中,如果想要启动某个组件,一般都是@EnableXXX这种方式注入,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
的。
3.1 注入@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);
// 扫描package,注册被@FeignClient修饰的接口类为IOC Bean
registerFeignClients(metadata, registry);
}
3.2 添加全局配置
registerDefaultConfiguration
方法流程如下
- 获取
@EnableFeignClients
注解上的属性以及对应Value
; - 生成FeignClientSpecification(存储
Feign
中的配置类)对应的构造器BeanDefinitionBuilder
; FeignClientSpecification Bean
名称为default. + @EnableFeignClients
修饰类全限定名称 +FeignClientSpecification
;@EnableFeignClients defaultConfiguration
默认为{},如果没有相关配置,默认使用FeignClientsConfiguration
并结合name
填充到FeignClientSpecification
,最终注册为IOC Bean
。
3.3 注册FeignClient接口
将重点放在registerFeignClients
上,该方法主要就是将修饰了@FeignClient
的接口注册为IOC Bean
。
- 扫描
@EnableFeignClients
注解,如果有clients
,则加载指定接口,为空则根据scanner
规则扫描出修饰了@FeignClient
的接口; - 获取
@FeignClient
上对应的属性,根据configuration
属性去创建接口级的FeignClientSpecification配置类IOC Bean
; - 将
@FeignClient
的属性设置到FeignClientFactoryBean对象上,并注册IOC Bean
。
@FeignClient
修饰的接口实际上使用了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
,先来看下它是什么。
4.1 FactoryBean接口特征
这里说一下FeignClientFactoryBean
都有哪些特征。
- 它会在类初始化时执行一段逻辑,依据Spring InitializingBean接口;
- 如果它被别的类
@Autowired
进行注入,返回的不是它本身,而是FactoryBean#getObject
返回的类,依据Spring FactoryBean接口; - 它能够获取
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容器获取FeignContext
FeignContext context = applicationContext.getBean(FeignContext.class);
// 通过context创建Feign构造器
Feign.Builder builder = feign(context);
//...
}
这里提出一个疑问?FeignContext
什么时候、在哪里被注入到Spring
容器里的?
看到图片小伙伴就明了了,用了SpringBoot
怎么会不使用自动装配的功能呢,FeignContext
就是在FeignAutoConfiguration
中被成功创建。
4.2 初始化父子容器
feign
方法里日志工厂、编码、解码等类均是通过get(...)
方法得到。
这里涉及到Spring
父子容器的概念,默认子容器Map
为空,获取不到服务名对应Context
则新建。
从下图中看到,注册了一个FeignClientsConfiguration类型的Bean
,我们上述方法feign
中的获取的编码、解码器等组件都是从此类中获取默认。
默认注册如下,FeignClientsConfiguration
是由创建FeignContext
调用父类Super
构造方法传入的。
关于父子类容器对应关系,以及提供@FeignClient
服务对应子容器的关系(每一个服务对应一个子容器实例)。
回到getInstance
方法,子容器此时已加载对应Bean
,直接通过getBean
获取FeignLoggerFactory。
如法炮制,Feign.Builder
、Encoder
、Decoder
、Contract
都可以通过子容器获取对应Bean
。
configureFeign
方法主要进行一些配置赋值,比如超时、重试、404配置等,就不再细说赋值代码了。
到这里有必要总结一下创建Spring
代理工厂的前半场代码
- 注入
@FeignClient
服务时,其实注入的是FactoryBean#getObject
返回代理工厂对象; - 通过
IOC
容器获取FeignContext
上下文; - 创建
Feign.Builder
对象时会创建Feign
服务对应的子容器; - 从子容器中获取日志工厂、编码器、解码器等
Bean
; - 为
Feign.Builder
设置配置,比如超时时间、日志级别等属性,每一个服务都可以个性化设置。
4.3 动态代理生成
继续嗑,上面都是开胃菜,接下来是最最最重要的地方了,小板凳坐板正了..
因为我们在@FeignClient
注解是使用name
而不是url
,所以会执行负载均衡策略的分支。
Client
:Feign
发送请求以及接收响应等都是由Client
完成,该类默认Client.Default
,另外支持HttpClient
、OkHttp
等客户端。
代码中的Client
、Targeter
在自动装配时注册,配合上文中的父子容器理论,这两个Bean
在父容器中存在。
因为我们并没有对Hystrix
进行设置,所以走入此分支。
创建反射类ReflectiveFeign
,然后执行创建实例类。
newInstance
方法对@FeignClient
修饰的接口中SpringMvc
等配置进行解析转换,对接口类中的方法进行归类,生成动态代理类。
可以看出Feign
创建动态代理类的方式和Mybatis Mapper
处理方式是一致的,因为两者都没有实现类。
根据newInstance
方法按照行为大致划分,共做了四件事:
- 处理
@FeignCLient
注解(SpringMvc
注解等)封装为MethodHandler包装类; - 遍历接口中所有方法,过滤
Object
方法,并将默认方法以及FeignClient
方法分类; - 创建动态代理对应的InvocationHandler并创建
Proxy
实例; - 接口内
default
方法绑定动态代理类。
MethodHandler
将方法参数、方法返回值、参数集合、请求类型、请求路径进行解析存储。
到这里我们也就可以Feign
的工作方式了。前面那么多封装铺垫,封装个性化配置等等,最终确定收尾的是创建动态代理类,也就是说在我们调用@FeignClient
接口时,会被FeignInvocationHandler#invoke
拦截,并在动态代理方法中执行下述逻辑:
- 接口注解信息封装为
HTTP Request
; - 通过
Ribbon
获取服务列表,并对服务列表进行负载均衡调用(服务名转换为ip+port); - 请求调用后,将返回的数据封装为
HTTP Response
,继而转换为接口中的返回类型。
既然已经明白了调用流程,那就正儿八经的试一哈,试过才知有没有...
RequestTemplate
:构建Request
模版类。Options
:存放连接、超时时间等配置类。Retryer
:失败重试策略类。
重试这一块逻辑看了很多遍,但是怎么看,一个continue关键字放到while的最后面都有点多余...
执行远端调用逻辑中使用到了Rxjava(响应式编程),可以看到通过底层获取server
后将服务名称转变为ip+port
的方式。
这种响应式编程的方式在SpringCloud
中很常见,Hystrix源码底层也有使用
网络调用默认使用HttpURLConnection,可以配置使用HttpClient
或者OkHttp
,调用远端服务后,再将返回值解析正常返回,到这里一个完成的Feign
调用链就聊明白了。
五、Feign如何负载均衡
一般而言,我们生产者注册多个服务,消费者调用时需要使用负载均衡从中选取一个健康并且可用的生产者服务。
因为Feign
内部集成Ribbon
,所以也支持此特性,一起看下它是怎么做的。
我们在Nacos
上注册了两个服务,端口号8080
、8081
。在获取负载均衡器时就可以获取服务集合。
然后通过chooseServer
方法选择一个健康实例返回,后面会新出一篇文章对Ribbon的负载均衡详细说明。
通过返回的Server
替换URL
中的服务名,最后使用网络调用服务进行远端调用。
六、总结
- 通过
@EnableFeignCleints
注解启动Feign Starter
组件; Feign Starter
在项目启动过程中注册全局配置,扫描包下所有的@FeignClient
接口类,并进行注册IOC
容器;@FeignClient
接口类被注入时,通过FactoryBean#getObject
返回动态代理类;- 接口被调用时被动态代理类逻辑拦截,将
@FeignClient
请求信息通过编码器生成Request
; - 交由
Ribbon
进行负载均衡,挑选出一个健康的Server
实例; - 继而通过
Client
携带Request
调用远端服务返回请求响应; - 通过解码器生成
Response
返回客户端,将信息流解析成为接口返回数据。