# RESTful 客户端:OpenFeign
类似于 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>
为项目添加配置 application.yaml :
最后创建一个启动类 AserviceApplication:
@SpringBootApplication @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);
}
@FeignClient 注解的 name 属性的值是被调方(也就是服务的提供者)在 Nacos 注册中心上所注册的名字,通常也就是被调方(服务提供者)的 spring.application.name 。
注意
一个服务只能被一个类绑定,不能让多个类绑定同一个远程服务,否则,会在启动项目是出现 “已绑定” 异常。
然后在 OpenFeign 里面通过单元测试来查看效果。
@Test
public void test() {
try {
log.debug("{}", bService.index());
} catch (Exception e) {
e.printStackTrace();
}
}
说明
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 的超时和超时重试。
整个 OpenFeign(实际上是 Ribbon)的最大重试次数为:
(1 + MaxAutoRetries) x (1 + MaxAutoRetriesNextServer)
这里需要注意的是『重试』次数是不包含『本身那一次』的。
故意加大被调服务的返回响应时长,你会看到主调服务中打印类似如下消息:
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)
...
另外,在被调服务方,你会发现上述配置会导致被调服务收到 12 次请求:
请求次数 = (1 + 5) x (1 + 1)
你也可以指定对某个特定服务的超时和超时重试:
# 4.2 替换底层 HTTP 实现(了解)
类似 RestTemplate,本质上是 OpenFeign 的底层会用到 JDK 的HttpURLConnection 发出 HTTP 请求。另外,如果有需要,你也可以换成第三方库 HttpClient 或 OkHttp 。
# 4.3 日志配置(了解)
SpringCloudFeign 为每一个 FeignClient 都提供了一个 feign.Logger 实例。可以根据 logging.level.<FeignClient> 参数配置格式来开启 Feign 客户端的 DEBUG 日志,其中 <FeignClient> 部分为 Feign 客户端定义接口的完整路径。如:
然后再在配置类(比如主程序入口类)中加入 Looger.Level 的 Bean:
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
级别 | 说明 |
---|---|
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);
在这里,出现了一个 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();
}
}
在 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();
}
那么,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()
├──> 第一件事 ...
└──> 第二件事 ...
MethodHandler 的 invoke() 方法核心就是干了 2 件事情:
传给 Ribbon 目标服务的服务名,找它 “要” 一个该服务的实例的具体的地址;
根据 Ribbon 返回的具体地址,发出 HTTP 请求,并等待、解析响应。
# 6. OpenFeign 的拦截器机制
OpenFeign 有一个拦截器机制,对于它的作用 OpenFeign 的官方是这样描述的:
Zero or more may be configured for purposes such as adding headers to all requests.
你可以自定义类继承 RequestInterceptor ,当然,你也可以使用 lambda 表达式结合 @Bean 进行简化:
@Bean
public RequestInterceptor requestInterceptor() {
return requestTemplate -> {
requestTemplate.header("x-jwt-token", "...");
};
}
下面代码是将当前请求的所有请求头添加到 openfeign 将要发出的请求中:
@Bean
public RequestInterceptor requestInterceptor() {
return requestTemplate -> {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames == null)
return;
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
String values = request.getHeader(name);
requestTemplate.header(name, values);
}
}
}