SpringCloud(1-5) OpenFeign

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(或其他),确保:

  1. 对外暴露出一个 URL ,即 ,对外提供一个功能。未来,我们的 a-service 会向这个 URL 发出 HTTP 请求,触发 b-service 的这个功能的执行,并从 b-service 这里获得 HTTP 响应。

  2. b-service 能启动、运行,并能连上 Nacos Server ,即,在 Nacos Server 上能看到 b-service 。

#2.3 创建主调服务

创建一个 Spring Boot Maven 项目作为主调方(调用发起方、HTTP 请求发起方),命名为 a-service(或其他)

  1. 在 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!
  1. 为项目添加配置 application.yaml :

    server:
      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
    
    Copied!
  2. 最后创建一个启动类 AserviceApplication:

    @SpringBootApplication
    @EnableDiscoveryClient 
    @EnableFeignClients(basePackages = "...") // 看这里,看这里,看这里
    public class AserviceApplication {
        public static void main(String[] args) {
            SpringApplication.run(AserviceApplication.class, args);
        }
    }
    
    Copied!

我们可以看到启动类增加了一个新的注解: @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 步:

  1. 引入依赖:

    <dependency>
      <groupId>io.github.openfeign</groupId>
      <artifactId>feign-httpclient</artifactId>
    </dependency>
    
    Copied!
  2. 在配置文件中启用它:

    feign:
      httpclient:
        enabled: true # 激活 httpclient 的使用
替换成 OkHttp

将 OpenFeign 的底层 HTTP 客户端替换成 OkHttp 需要 2 步:

  1. 引入依赖:

    <dependency>
      <groupId>io.github.openfeign</groupId>
      <artifactId>feign-okhttp</artifactId>
    </dependency>
    
    Copied!
  2. 在配置文件中启用它:

    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 件事情:

  1. 传给 Ribbon 目标服务的服务名,找它 “要” 一个该服务的实例的具体的地址;

  2. 根据 Ribbon 返回的具体地址,发出 HTTP 请求,并等待、解析响应。

上一篇:IDE


下一篇:微服务系列:服务调用 Spring Cloud 之 OpenFeign 详细入门