OpenFeign:声明式 RESTful 客户端
类似于 RestTemplate ,OpenFeign 是对 JDK 的 HttpURLConnection(以及第三方库 HttpClient 和 OkHttp)的包装和简化,并且还自动整合了 Ribbon 。
#1. 什么是 OpenFeign
Feign 早先由 Netflix 公司提供并开源,在它的 8.18.0
之后,Nefflix 将其捐赠给 Spring Cloud 社区,并更名为 OpenFeign 。OpenFeign 的第一个版本就是 9.0.0
。
OpenFeign 会完全代理 HTTP 的请求,在使用过程中我们只需要依赖注入 Bean,然后调用对应的方法传递参数即可。这对程序员而言屏蔽了 HTTP 的请求响应过程,让代码更趋近于『调用』的形式。
#2. Feign 的入门案例
#2.1 启动 Nacos 注册中心
启动你本地(或服务器)上的 Nacos Server ,确保其正在运行。
#2.2 创建被调用服务
注意
在调用和被调关系中,被调方是不需要 OpenFeign 的,主调方才需要。
创建一个 Spring Boot Maven 项目作为被调方,命名为 b-service(或其他),确保:
-
对外暴露出一个 URL ,即 ,对外提供一个功能。未来,我们的 a-service 会向这个 URL 发出 HTTP 请求,触发 b-service 的这个功能的执行,并从 b-service 这里获得 HTTP 响应。
-
b-service 能启动、运行,并能连上 Nacos Server ,即,在 Nacos Server 上能看到 b-service 。
#2.3 创建主调服务
创建一个 Spring Boot Maven 项目作为主调方(调用发起方、HTTP 请求发起方),命名为 a-service(或其他)。
- 在 Spring Initializer 中引入依赖:在 Initializer 的搜索框内搜索并选择 Spring Web 、 Nacos Service Discovery 和 OpenFeign 。
注意
这里自动引入的 OpenFeign 的 maven 依赖为:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
Copied!
-
为项目添加配置 application.yaml :
server:
Copied!port: 8080 spring: application: name: a-service cloud: nacos: discovery: server-addr: 127.0.0.1:8848 username: nacos password: nacos namespace: public # group: hemiao
-
最后创建一个启动类 AserviceApplication:
@SpringBootApplication
Copied!@EnableDiscoveryClient @EnableFeignClients(basePackages = "...") // 看这里,看这里,看这里 public class AserviceApplication { public static void main(String[] args) { SpringApplication.run(AserviceApplication.class, args); } }
我们可以看到启动类增加了一个新的注解: @EnableFeignClients ,如果我们要使用 OpenFeign ,必须要在启动类加入这个注解,以开启 OpenFeign 。
这样,我们的 Feign 就已经集成完成了,那么如何通过 Feign 去调用之前我们写的 HTTP 接口呢?
和 MyBatis 类似:
首先创建一个接口 BServiceClient(名字任意),并且通过注解配置要调用的服务地址:
@FeignClient(value = "b-service") // 这里要和 b-service 在 nacos-server 上登记的名字相呼应
public interface BServiceClient {
@GetMapping("/")
public String index();
@PostMapping("/login1")
public String login1(@RequestParam("username") String username,
@RequestParam("uesrname") String password);
@PostMapping("/login2")
public String login2(@SpringQueryMap LoginToken token);
@PostMapping("/login3")
public String login3(@RequestBody LoginToken token);
}
Copied!
@FeignClient 注解的 name 属性的值是被调方(也就是服务的提供者)在 Nacos 注册中心上所注册的名字,通常也就是被调方(服务提供者)的 spring.application.name 。
注意
一个服务只能被一个类绑定,不能让多个类绑定同一个远程服务,否则,会在启动项目是出现 “已绑定” 异常。
然后在 OpenFeign 里面通过单元测试来查看效果。
@Test
public void test() {
try {
log.debug("{}", bService.index());
} catch (Exception e) {
e.printStackTrace();
}
}
Copied!
说明
OpenFeign 的能力包括但不仅包括这个。
#3. FeignClient 抛出异常
当调用方 b-service 正常返回时,b-service(的 Spring MVC)的返回就是正常的 HTTP 200 响应,而在 a-service 这边,Openfeign 会帮我们做数据(从 HTTP 响应体中的)提取、转换操作,并从 FeignClient 中返回。
当被调方 b-service 返回的是非 200 的响应(比如,500、429 等)时,在 a-service 这边,Openfeign 则会在 FeignClient 方法中抛出一个异常(一个 RuntimeException 的子类)。
#4. OpenFeign 的配置
#4.1 超时和超时重试
OpenFeign 本身也具备重试能力,在早期的 Spring Cloud 中,OpenFeign 默认使用的是 feign.Retryer.Default#Default ,重试 5 次。但 OpenFeign 整合了 Ribbon ,而 Ribbon 也有重试的能力,此时,就可能会导致行为的混乱。(总重试次数 = OpenFeign 重试次数 x Ribbon 的重试次数,这是一个笛卡尔积。)
后来 Spring Cloud 意识到了此问题,因此做了改进(issues 467 (opens new window)),将 OpenFeign 的默认重试改为 feign.Retryer#NEVER_RETRY ,即,默认关闭 。
简单来说,OpenFeign 对外表现出的超时和重试的行为,实际上是它所用到的 Ribbon 的超时和超时重试行为。我们在项目中进行的配置,也都是配置 Ribbon 的超时和超时重试。
# 全局配置
ribbon:
readTimeout: 1000 # 请求处理的超时时间
MaxAutoRetries: 5 # 最大重试次数
MaxAutoRetriesNextServer: 1 # 切换实例的重试次数
# 是否开启对所有请求进行超时重试。一般不会开启这个功能。默认值是 false ,表示仅对 get 请求进行超时重试
# okToRetryOnAllOperations: true
Copied!
整个 OpenFeign(实际上是 Ribbon)的最大重试次数为:
(1 + MaxAutoRetries) x (1 + MaxAutoRetriesNextServer)
Copied!
这里需要注意的是『重试』次数是不包含『本身那一次』的。
故意加大被调服务的返回响应时长,你会看到主调服务中打印类似如下消息:
feign.RetryableException: Read timed out executing GET http://SERVICE-PRODUCER/demo?username=tom&password=123
at feign.FeignException.errorExecuting(FeignException.java:249)
at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:129)
at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:89)
...
Copied!
另外,在被调服务方,你会发现上述配置会导致被调服务收到 12 次请求:
请求次数 = (1 + 5) x (1 + 1)
Copied!
你也可以指定对某个特定服务的超时和超时重试:
# 针对自己向 b-service 发出请求超时的设置
b-service:
ribbon:
readTimeout: 3000
MaxAutoRetries: 2
MaxAutoRetriesNextServer: 0
Copied!
#4.2 替换底层 HTTP 实现(了解)
类似 RestTemplate,本质上是 OpenFeign 的底层会用到 JDK 的HttpURLConnection 发出 HTTP 请求。另外,如果有需要,你也可以换成第三方库 HttpClient 或 OkHttp 。
替换成 HTTPClient将 OpenFeign 的底层 HTTP 客户端替换成 HTTPClient 需要 2 步:
-
引入依赖:
Copied!<dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-httpclient</artifactId> </dependency>
-
在配置文件中启用它:
feign: httpclient: enabled: true # 激活 httpclient 的使用
将 OpenFeign 的底层 HTTP 客户端替换成 OkHttp 需要 2 步:
-
引入依赖:
Copied!<dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-okhttp</artifactId> </dependency>
-
在配置文件中启用它:
feign: okhttp: enabled: true # 激活 okhttp 的使用
#4.3 日志配置(了解)
SpringCloudFeign 为每一个 FeignClient 都提供了一个 feign.Logger 实例。可以根据 logging.level.<FeignClient> 参数配置格式来开启 Feign 客户端的 DEBUG 日志,其中 <FeignClient> 部分为 Feign 客户端定义接口的完整路径。如:
logging:
level:
com.woniu.outlet.client: DEBUG
Copied!
然后再在配置类(比如主程序入口类)中加入 Looger.Level 的 Bean:
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
Copied!
级别 | 说明 |
---|---|
NONE | 不输出任何日志 |
BASIC | 只输出 Http 方法名称、请求 URL、返回状态码和执行时间 |
HEADERS | 输出 Http 方法名称、请求 URL、返回状态码和执行时间 和 Header 信息 |
FULL | 记录 Request 和 Response 的 Header,Body 和一些请求元数据 |
#5. OpenFeign 的底层原理概述
虽然在使用 OpenFeign 时,我们( 程序员 )定义的是接口,但是 OpenFeign 框架会通过 JDK 动态代理生成 @FeignClient 接口的代理对象。逻辑相当于:
@Autowired
XxxServiceClient client = Proxy.newProxyInstance(invocationHandler);
Copied!
在这里,出现了一个 InvocationHandler 对象,结合 JDK 动态代理的知识,我们知道,当你调用 client 的某个方法时,实际上触发的就是这个 InvocationHandler 对象的 invoke 方法。InvocationHandler 对象逻辑相当于:
public class SimpleInvocationHandler implements InvocationHandler {
Map<Method, MethodHandler> methodToHandler = new LinkedHashMap();
public SimpleInvocationHandler(Map<Method, MethodHandler> methodToHandler) {
this.methodToHandler = methodToHandler;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
MethodHandler handler = methodToHandler.get(method);
return handler.invoke();
}
}
Copied!
在 InvocationHandler 中最核心的在于它有一个 Map ,这个 map 以 InvocationHandler 所代理的那个 FeignClient 中所声明的方法的 Method 对象为 key ,值是一个一个的 MethodHandler 对象。
假设有一个 @FeignClient 为如下形式:
@FeignClient("a-service")
public interface AService {
@RequestMapping("/hello")
public String hello();
@RequestMapping("/world")
public String world();
}
Copied!
那么,AService 有一个代理对象 InvocationHandler ,它里面的 Map 逻辑上形如:
key | value |
---|---|
helloMethod | helloMethodHandler |
worldMethod | worldMethodHandler |
那么,当你调用 bService.hello();
方法时,实际上是 InvocationHandler 对象的 invoke 方法被执行,而 InvocationHandler 对象会从它的 Map 中以 hello 方法的 Method 对象为 key 找到对应的一个 MethodHandler 对象,然后调用 MethodHandler 对象的 invoke 方法。
调用关系和流程形如:
bService.hello()
└──> invocationHandler.invoke()
└──> methodHandler.invoke()
├──> 第一件事 ...
└──> 第二件事 ...
Copied!
MethodHandler 的 invoke() 方法核心就是干了 2 件事情:
-
传给 Ribbon 目标服务的服务名,找它 “要” 一个该服务的实例的具体的地址;
-
根据 Ribbon 返回的具体地址,发出 HTTP 请求,并等待、解析响应。