前段时间 Hystrix 宣布不再维护之后,Feign 作为一个跟 Hystrix 强依赖的组件,必然会有所担心后续的使用。
作为 Spring Cloud Alibaba 体系中的熔断器 Sentinel,Sentinel Starter 目前整合了 Feign,本文对整合过程做一次总结,欢迎大家讨论和使用。
Feign 是什么?
- Feign 是一个 Java 实现的 Http 客户端,用于简化 Restful 调用
- Feign 跟 OkHttp、HttpClient 这种客户端实现理念不一样。Feign 强调接口的定义,接口中的一个方法对应一个 Http 请求,调用方法即发送一个 Http 请求;OkHttp 或 HttpClient 以过程式的方式发送 Http 请求。Feign 底层发送请求的实现可以跟 OkHttp 或 HttpClient 整合
Feign 的使用及执行过程
要想整合 Feign,首先要了解 Feign 的使用以及执行过程,然后看 Sentinel 如何整合进去。
Feign 的使用
需要两个步骤:
- 使用
@EnableFeignClients注解开启 Feign 功能
1 |
|
@EnableFeignClients 属性介绍:
- value:String[] 包路径。比如
org.my.pkg,会扫描这个包路径下带有@FeignClient注解的类并处理 - basePackages:String[] 跟 value 属性作用一致
- basePackageClasses:Class<?>[] 跟 basePackages 作用一致,basePackages 是个 String 数组,而 basePackageClasses 是个 Class 数组,用于扫描这些类对应的 package
- defaultConfiguration:Class<?>[] 默认的配置类,对于所有的 Feign Client,这些配置类里的配置都会对它们生效,可以在配置类里构造
feign.codec.Decoder,feign.codec.Encoder或feign.Contract等bean - clients:Class<?>[] 表示
@FeignClient注解修饰的类集合,如果指定了该属性,那么扫描功能相关的属性就是失效。比如 value、basePackages 和 basePackageClasses
- 使用
@FeignClient注解修饰接口,这样会基于跟接口生成代理类
1 | (name = "service-provider") |
只要确保这个被 @FeignClient 注解修饰到的接口能被 @EnableFeignClients 注解扫描到,就会基于 java.lang.reflect.Proxy 根据这个接口生成一个代理类。
生成代理类之后,会被注入到 ApplicationContext 中,直接 AutoWired 就能使用,使用的时候调用 echo 方法就相当于是发起一个 Restful 请求。
@FeignClient 属性介绍:
- value:String 服务名。比如
service-provider,http://service-provider。比如EchoService中如果配置了value=service-provider,那么调用echo方法的 url 为http://service-provider/echo;如果配置了value=https://service-provider,那么调用echo方法的 url 为https://service-provider/divide - serviceId:String 该属性已过期,但还能用。作用跟 value 一致
- name:String 跟 value 属性作用一致
- qualifier:String 给 FeignClient 设置
@Qualifier注解 - url:String 绝对路径,用于替换服务名。优先级比服务名高。比如
EchoService中如果配置了url=aaa,那么调用echo方法的 url 为http://aaa/echo;如果配置了url=https://aaa,那么调用echo方法的 url 为https://aaa/divide - decode404:boolean 默认是 false,表示对于一个 http status code 为 404 的请求是否需要进行 decode,默认不进行 decode,当成一个异常处理。设置为true之后,遇到 404 的 response 还是会解析 body
- configuration:Class<?>[] 跟
@EnableFeignClients注解的defaultConfiguration属性作用一致,但是这个对于单个 FeignClient 的配置,而@EnableFeignClients里的defaultConfiguration属性是作用域全局的,针对所有的 FeignClient - fallback:Class<?> 默认值是
void.class,表示 fallback 类,需要实现 FeignClient 对应的接口,当调用方法发生异常的时候会调用这个 Fallback 类对应的 FeignClient 接口方法。如果配置了 fallback 属性,那么会把这个 Fallback 类包装在一个默认的FallbackFactory实现类FallbackFactory.Default上,而不使用 fallbackFactory 属性对应的FallbackFactory实现类 - fallbackFactory:Class<?> 默认值是
void.class,表示生产 fallback 类的 Factory,可以实现feign.hystrix.FallbackFactory接口,FallbackFactory内部会针对一个Throwable异常返回一个 Fallback 类进行 fallback 操作 - path:String 请求路径。 在服务名或 url 与 requestPath 之间
- primary:boolean 默认是 true,表示当前这个 FeignClient 生成的 bean 是否是 primary。 所以如果在
ApplicationContext中存在一个实现EchoService接口的 Bean,但是注入的时候并不会使用该Bean,因为 FeignClient 生成的 Bean 是 primary
Feign 的执行过程
了解了 Feign 的使用之后,接下来我们来看 Feign 构造一个 Client 的过程。
从 @EnableFeignClients 注解可以看到,入口在该注解上的 FeignClientsRegistrar 类上,整个链路是这样的:
从这个链路上我们可以得到几个信息:
@FeignClient注解修饰的接口最终会被转换成FeignClientFactoryBean这个FactoryBean,FactoryBean内部的 getObject 方法最终会返回一个 Proxy- 在构造 Proxy 的过程中会根据
org.springframework.cloud.openfeign.Targeter接口的target方法去构造。如果启动了hystrix开关(feign.hystrix.enabled=true),会使用HystrixTargeter,否则使用默认的DefaultTargeter Targeter内部构造 Proxy 的过程中会使用feign.Feign.Builder去调用它的build方法构造feign.Feign实例(默认只有一个子类ReflectiveFeign)。如果启动了 hystrix 开关(feign.hystrix.enabled=true),会使用feign.hystrix.HystrixFeign.Builder,否则使用默认的feign.Feign.Builder- 构造出
feign.Feign实例之后,调用newInstance方法返回一个 Proxy
简单看下这个 newInstance 方法内部的逻辑:
1 | public <T> T newInstance(Target<T> target) { |
这里的 InvocationHandlerFactory 是通过构造 Feign 的时候传入的:
- 使用原生的
DefaultTargeter: 那么会使用feign.InvocationHandlerFactory.Default这个 factory,并且构造出来的InvocationHandler是feign.ReflectiveFeign.FeignInvocationHandler - 使用 hystrix 的
HystrixTargeter: 那么会在feign.hystrix.HystrixFeign.Builder#build(feign.hystrix.FallbackFactory<?>)方法中调用父类的invocationHandlerFactory方法传入一个匿名的InvocationHandlerFactory实现类,该类内部构造出的InvocationHandler为HystrixInvocationHandler
Sentinel 整合 Feign
理解了 Feign 的执行过程之后,Sentinel 想要整合 Feign,可以参考 Hystrix 的实现:
- ❌ 实现
Targeter接口SentinelTargeter。 很不幸,Targeter这个接口属于包级别的接口,在外部包中无法使用,这个Targeter无法使用。没关系,我们可以沿用默认的HystrixTargeter(实际上会用DefaultTargeter,下文 Note 有解释) - ✅
FeignClientFactoryBean内部构造Targeter、feign.Feign.Builder的时候,都会从FeignContext中获取。所以我们沿用默认的DefaultTargeter的时候,内部使用的feign.Feign.Builder可控,而且这个 Builder 不是包级别的类,可在外部使用- 创建
SentinelFeign.Builder继承feign.Feign.Builder,用来构造Feign SentinelFeign.Builder内部需要获取FeignClientFactoryBean中的属性进行处理,比如获取fallback,name,fallbackFactory。很不幸,FeignClientFactoryBean这个类也是包级别的类。没关系,我们知道它存在在ApplicationContext中的 beanName, 拿到 bean 之后根据反射获取属性就行(该过程在初始化的时候进行,不会在调用的时候进行,所以不会影响性能)SentinelFeign.Builder调用build方法构造Feign的过程中,我们不需要实现一个新的Feign,跟 hystrix 一样沿用ReflectiveFeign即可,在沿用的过程中调用父类feign.Feign.Builder的一些方法进行改造即可,比如invocationHandlerFactory方法设置InvocationHandlerFactory,contract的调用
- 创建
- ✅ 跟 hystrix 一样实现自定义的
InvocationHandler接口SentinelInvocationHandler用来处理方法的调用 - ✅
SentinelInvocationHandler内部使用 Sentinel 进行保护,这个时候涉及到资源名的获取。SentinelInvocationHandler内部的feign.Target能获取服务名信息,feign.InvocationHandlerFactory.MethodHandler的实现类feign.SynchronousMethodHandler能拿到对应的请求路径信息。很不幸,feign.SynchronousMethodHandler这个类也是包级别的类。没关系,我们可以自定义一个feign.Contract的实现类SentinelContractHolder在处理MethodMetadata的过程把这些 metadata 保存下来(feign.Contract这个接口在 Builder 构造 Feign 的过程中会对方法进行解析并验证)。在SentinelFeign.Builder中调用contract进行设置,SentinelContractHolder内部保存一个Contract使用委托方式不影响原先的Contract过程
Note: spring-cloud-starter-openfeign 依赖内部包含了 feign-hystrix。所以是说默认使用 HystrixTargeter 这个 Targeter ,进入 HystrixTargeter 的 target 方法内部一看,发现有段逻辑这么写的:
1 |
|
在 SentinelInvocationHandler 内部我们对资源名的处理策略是: http方法:protocol://服务名/请求路径跟参数
比如这个 TestService:
1 | (name = "test-service") |
echo方法对应的资源名:GET:http://test-service/echo/{str}divide方法对应的资源名:GET:http://test-service/divide
总结
- Feign 的内部很多类都是 package 级别的,外部 package 无法引用某些类,这个时候只能想办法绕过去,比如使用反射
- 目前这种实现有风险,万一哪天 starter 内部使用的 Feign 相关类变成了 package 级别,那么会改造代码。所以把 Sentinel 的实现放到 Feign 里并给 Feign 官方提 pr 可能更加合适
- Feign的处理流程还是比较清晰的,只要能够理解其设计原理,我们就能容易地整合进去
欢迎大家对整合方案进行讨论,并能给出不合理的地方,当然能提pr解决不合理的地方就更好了。
Sentinel Starter 整合 Feign 的代码目前已经在 github 仓库上,对应的版本是 0.2.1.RELEASE 或 0.1.1.RELEASE。
最后再附上一个使用 Nacos 做服务发现和 Sentinel 做限流的 Feign 例子 。