Appearance
Spring Cloud OpenFeign 简介
Feign 是 Netflix 开发的声明式,模板化的 HTTP 客户端,其灵感来自 Retrofit,JAXRS-2.0 以及 WebSocket。
Feign 可更加便捷,优雅的调用 HTTP API。在 Spring Cloud 中,使用 Feign 非常简单。创建一个接口,并在接口上添加一些注解,代码就完成了。
Feign 支持多种注解,例如 Feign 自带的注解或者 JAX-RS 注解等。Spring Cloud 对 Feign 进行了增强,使 Feign 支持了 SpringMVC 注解,并整合了 Ribbon 和 Eureka,从而让 Feign 的使用更加方便。
Feign 和 OpenFeign
OpenFeign 组件的前身是 Netflix Feign 项目,它最早是作为 Netflix OSS 项目的一部分,由 Netflix 公司开发。后来 Feign 项目被贡献给了开源组织,于是就有了 Spring Cloud OpenFeign 组件。
Feign 和 OpenFeign 有很多大同小异之处,不同的是 OpenFeign 支持 MVC 注解。可以认为 OpenFeign 为 Feign 的增强版。
OpenFeign 的作用
- OpenFeign 是声明式的 HTTP 客户端,让远程调用更简单。
- 提供了 HTTP 请求的模板,编写简单的接口和插入注解,就可以定义好 HTTP 请求的参数、格式、地址等信息
- 整合了 Ribbon(负载均衡组件)和 Hystix(服务熔断组件),不需要显示使用这两个组件
- Spring Cloud Feign 在 Netflix Feign 的基础上扩展了对 SpringMVC 注解的支持
基于 eureka 的 Feign 服务调用示例
示例工程准备
复用之前 eureka 单机版的示例项目spring-cloud-sample-eureka,命名为spring-cloud-sample-feign
引入 Feign 依赖
在服务消费者 shop-service-order 工程添加 Feign 依赖
xml
<!-- SpringCloud整合的openFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>开启 Feign 的支持
在服务消费者的启动类上,通过@EnableFeignClients注解开启 Spring Cloud Feign 的支持功能
java
@SpringBootApplication(scanBasePackages = "com.moon.order")
@EntityScan("com.moon.entity") // 指定扫描实体类的包路径
@EnableFeignClients // 开启Feign的支持
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}创建 Feign 服务调用的接口
基础使用步骤
在服务消费者 shop-service-order 创建一个 Feign 接口,此接口是在 Feign 中调用微服务的核心接口。
java
/*
* @FeignClient 注解,用于标识当前接口为Feign调用微服务的核心接口
* value/name属性:指定需要调用的服务提供者的名称
*/
@FeignClient("shop-service-product") // 或者:@FeignClient(name = "shop-service-product")
public interface ProductFeignClient {
/*
* 创建需要调用的微服务接口方法,SpringCloud 对 Feign 进行了增强兼容了 SpringMVC 的注解
* 在使用的两个注意点:
* 1. FeignClient 接口有参数时,必须在参数加@PathVariable("XXX")和@RequestParam("XXX")注解,并且必须要指定对应的参数值(原来SpringMVC是可以省略)
* 2. feignClient 返回值为复杂对象时,其对象类型必须有无参构造函数
* 3. 方法的名称不需要与被调用的服务接口名称一致
*/
@GetMapping("/product/{id}")
Product findById(@PathVariable("id") Long id);
}基础使用步骤总结
- 启动类添加
@EnableFeignClients注解,表示开启对Feign的支持,Spring会扫描标记了@FeignClient注解的接口,并生成此接口的代理对象 @FeignClient注解通过name/value属性指定需要调用的微服务的名称,用于创建Ribbon的负载均衡器。所以Ribbon从注册中心中获取服务列表,并通过负载均衡算法调用相应名称的服务。如:@FeignClient("service-xxx")即指定了服务提供者的名称service-xxx,Feign会从注册中心获取服务列表,并通过负载均衡算法进行服务调用名为service-xxx的服务- 在接口方法中使用
@GetMapping("/xxxx")等SpringMVC的注解,指定调用的url,Feign将根据url进行远程调用
Feign组件使用注意事项
- 定义接口方法对于形参绑定时,
@PathVariable、@RequestParam、@RequestHeader等可以指定参数属性,在Feign中绑定参数必须通过value属性来指明具体的参数名,不然会抛出异常 FeignClient返回值为复杂对象时,其对象类型必须有无参构造函数
配置消费者调用服务接口
修改消费者shop-service-order的OrderController控制类,注入ProductFeignClient接口实例,并在相应的方法中使用ProductFeignClient实例方法完成微服务调用即可
java
@RestController
@RequestMapping("order")
public class OrderController {
/* 日志对象 */
private static final Logger LOGGER = LoggerFactory.getLogger(OrderController.class);
// 注入FeignClient服务调用接口
@Autowired
private ProductFeignClient productFeignClient;
/**
* 根据商品id创建订单
*
* @param id 商品的id
* @return
*/
@PostMapping("/{id}")
public String createOrder(@PathVariable Long id) {
// 使用Feign组件实现服务远程调用,直接调用FeignClient的接口定义的相应方法即可
Product product = productFeignClient.findById(id);
LOGGER.info("当前下单的商品是: ${}", product);
return "创建订单成功";
}
}启动相应的服务,进行测试
基于 Nacos 的 Feign 服务调用示例
实践步骤

- 创建一个服务提供者,整合 Nacos
- 创建一个服务消费者,整合 Nacos
- 服务消费者添加 Feign 依赖
- 服务消费者创建 Feign 客户端接口
- 服务消费者使用 Feign 接口调用服务提供者
- 启动并测试
服务提供者
创建 feign-nacos-provider 工程,添加 Nacos 依赖:
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<!-- spring boot 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.1.13.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- spring cloud alibaba 依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.4.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>在启动类或者配置类上,添加 @EnableDiscoveryClient 注解开启服务发现
java
@SpringBootApplication
@EnableDiscoveryClient
public class FeignNacosProviderApp {
public static void main(String[] args) {
SpringApplication.run(FeignNacosProviderApp.class, args);
}
}修改项目配置文件,添加 nacos 地址
yml
server:
port: 8081
spring:
application:
name: service-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848创建测试的接口
java
@RestController
public class ProviderTestController {
@GetMapping("/hello")
public String hello(@RequestParam String name) {
return "hello " + name + "!";
}
}服务消费者
创建 feign-nacos-consumer 工程,添加 Nacos 与 feign 依赖:
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<!-- spring boot 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.1.13.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- spring cloud 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR6</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- spring cloud alibaba 依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.4.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>修改项目配置文件,添加 nacos 地址
yml
server:
port: 8080
spring:
application:
name: service-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848在启动类或者配置类上,添加 @EnableDiscoveryClient 注解开启服务发现、@EnableFeignClients 注解开启 Feign 支持
java
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class FeignNacosConsumer {
public static void main(String[] args) {
SpringApplication.run(FeignNacosConsumer.class, args);
}
}创建 feign 代理接口
java
@FeignClient(name = "service-provider")
public interface FeignClientDemo {
@GetMapping("/hello")
String hello(@RequestParam("name") String name);
}创建测试请求接口,通过 feign 代理接口调用服务提供者
java
@RestController
public class ComsumerTestController {
@Autowired
private FeignClientDemo feignClientDemo;
@GetMapping("/hello")
public String hello(@RequestParam String name) {
return feignClientDemo.hello(name);
}
}测试
发送get请求,观察是否成功返回
http://127.0.0.1:8080/hello?name=MooNkirAFeign 的负载均衡
Feign 和 Ribbon 的联系:
- Ribbon 是一个基于 HTTP 和 TCP 客户端的负载均衡的工具。它可以在客户端配置
RibbonServerList(服务端列表),使用HttpClient或RestTemplate模拟 http 请求,步骤比较繁琐 - Feign 是在 Ribbon 的基础上进行了一次改进,是一个使用起来更加方便的 HTTP 客户端。采用接口的方式,只需要创建一个接口,然后在上面添加注解即可,将需要调用的其他服务的方法定义成抽象方法即可,不需要自己构建 http 请求。然后就像是调用自身工程的方法调用,而感觉不到是调用远程方法,使得编写客户端变得非常容易
Feign 本身已经集成了 Ribbon 依赖和自动配置,因此不需要额外引入依赖,也不需要再注册 RestTemplate 对象。
配置负载均衡的方式与使用 Ribbon 的配置方式一致,即也可以通过修改项目配置文件中 ribbon.xx 来进行全局配置。也可以通过服务名.ribbon.xx 来对指定服务配置
启动两个shop-service-product服务,重新测试可以发现使用 Ribbon 的轮询策略进行负载均衡

Feign 进阶使用
Feign 可配置项说明
从 Spring Cloud Edgware 版本开始,Feign 支持使用属性自定义 Feign。对于一个指定名称的 Feign Client(例如该 Feign Client 的名称为 feignName ),Feign支持如下配置项:
yml
# Feign 属性配置
feign:
client:
config:
shop-service-product: # 需要调用的服务名称
connectTimeout: 5000 # 相当于Request.Options
readTimeout: 5000 # 相当于Request.Options
loggerLevel: full # 配置Feign的日志级别,相当于代码配置方式中的Logger
errorDecoder: com.example.SimpleErrorDecoder # Feign的错误解码器,相当于代码配置方式中的ErrorDecoder
retryer: com.example.SimpleRetryer # 配置重试,相当于代码配置方式中的Retryer
requestInterceptors: # 配置拦截器,相当于代码配置方式中的RequestInterceptor
- com.example.FooRequestInterceptor
- com.example.BarRequestInterceptor
decode404: false部分属性配置说明:
feignName:FeignClient 的名称,即上面例子的shop-service-productconnectTimeout:建立链接的超时时长readTimeout:读取超时时长loggerLevel:Feign 的日志级别errorDecoder:Feign 的错误解码器retryer:配置重试requestInterceptors:添加请求拦截器decode404:配置熔断不处理404异常
请求压缩配置
Spring Cloud Feign 支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗。通过下面的参数即可开启请求与响应的压缩功能:
yml
feign:
compression: # Feign 请求压缩配置
request:
enabled: true # 开启请求压缩
response:
enabled: true # 开启响应压缩也可以对请求的数据类型,以及触发压缩的大小下限进行设置:
yml
feign:
compression: # Feign 请求压缩配置
request:
enabled: true # 开启请求压缩
mime-types: text/html,application/xml,application/json # 设置压缩的数据类型
min-request-size: 2048 # 设置触发压缩的大小下限注:上面的数据类型、压缩大小下限均为默认值。
日志级别
Feign 在构建服务客户端时,会为每个客户端都创建一个 feign.logger 实例,此日志对象的调试模式可以记录 Feign 请求过程细节日志记录。
默认情况下 Feign 的日志是关闭的,可以通过以下几种方式开启日志记录。
代码方式局部配置日志
- 在配置文件中设置 Feigin client 接口的日志级别
yml
logging:
level:
com.moon.example.feign: debug # 设置 Feign client 接口 package 日志级别为 debug- 创建 Feigin 配置类,配置日志级别对象实例
java
package com.moon.example.feign;
import feign.Logger;
import org.springframework.context.annotation.Bean;
public class FeignClientConfig {
// 配置日志级别
@Bean
public Logger.Level level(){
return Logger.Level.FULL;
}
}- 在需要开启日志功能的 Feigin client 接口的
@FeignClient注解中,指定日志级别配置类
java
// 通过 configuration 指定配置类,此配置类中设置了 Feigin client 调用服务的日志级别
@FeignClient(name = "service-provider", configuration = FeignClientConfig.class)
public interface FeignClientDemo {
@GetMapping("/hello")
String hello(@RequestParam("name") String name);
}- 启动服务,访问测试
http://127.0.0.1:8080/hello?name=MooNkirA

属性方式局部配置日志
在 application.yml 中以属性配置方式,配置 Feigin client 接口的日志级别与目标调用服务的日志级别
yml
# 配置目标调用服务的日志级别
feign:
client:
config:
shop-service-product: # 需要调用的服务名称
loggerLevel: full # 配置Feign的日志级别,相当于代码配置方式中的Logger
# Feigin client 日志配置
logging:
level:
# 配置只输出ProductFeignClient接口的日志
com.moon.order.feign.ProductFeignClient: debug配置参数说明:
logging.level.xx: debug:设置 Feigin client 接口的日志级别。示例是只对日志级别为 debug 的做出响应。feign.client.config.服务名称.loggerLevel:配置 Feign 目标调用服务的日志级别,有以下四种日志级别可选值:NONE:不记录任何日志(默认值)。【性能最佳,适用于生产】BASIC:只记录基本的日志信息,包括请求方法、URL、响应状态代码、执行时间。【适用于生产环境追踪问题】HEADERS:在 BASIC 级别的基础上,记录请求和响应的 Header 信息。FULL:记录最全的日志信息。包括请求和响应的 Header、Body、元数据信息。【比较适用于开发及测试环境定位问题】

全局方式配置日志
前面的方式都是局部的日志配置,只是针对某个 Feign 接口的,如果需要配置的接口比较多,比较适合使用全局配置。全局配置同样可以通过代码的方式或者属性方式来配置。
- 代码方式全局配置日志功能,通过
@EnableFeignClients的defaultConfiguration属性,指定相应的日志级别配置类(即前面示例的FeignClientConfig)开启全局日志级别配置
java
@SpringBootApplication
@EnableDiscoveryClient
// defaultConfiguration 属性指定全局配置
@EnableFeignClients(defaultConfiguration = FeignClientConfig.class)
public class FeignNacosConsumer {
public static void main(String[] args) {
SpringApplication.run(FeignNacosConsumer.class, args);
}
}- 属性方式全局配置日志功能,
feign.client.config.default.loggerLevel配置开启全局日志级别配置
yml
feign:
client:
config:
default: # 全局配置需要调用的服务名称
loggerLevel: FULL # 配置 Feign 的日志级别,相当于代码配置方式中的Logger多参数传递
多参数传递问题描述
服务提供者有一个需要多个参数请求的接口,Feign 中是需要特别注意“多参数传递问题”。假设服务提供者写一个复杂参数的接口如下:
- 定义用于测试的实体类
java
@Data
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Integer id;
private String name;
private String city;
private String email;
}- 服务提供者定义请求参数为对象的接口:
java
@RestController
public class ProviderTestController {
@GetMapping("/user")
public User getUser(User user) {
return User.builder().id(1).name(user.getName()).city(user.getCity()).email("moon@moon.com").build();
}
}- 在服务消费者也定义 Feign 接口,并且方法中的参数定义与服务提供者中接口定义一致:
java
@FeignClient(name = "service-provider")
public interface FeignClientDemo {
@GetMapping("/user")
User getUser(User user);
}- 在服务消费者测试控制类中增加方法,调用 Feign 接口
java
@GetMapping("/user")
public User getUser(Integer id, String name, String city, String email){
User user = User.builder().id(id).name(name).city(city).email(email).build();
return feignClientDemo.getUser(user);
}- 启动服务测试

即使 Feign 接口中的参数定义与服务提供者中接口定义一致,但实际会报错。这就是『多参数传递问题』!

解决方案1:参数注解
Spring MVC 的 get 方法支持直接绑定 POJO,而 Feign 并未覆盖所有 Spring MVC 功能,不能直接绑定 POJO。需要修改服务消费者 Feign 接口方法,在方法参数前添加 org.springframework.cloud.openfeign.SpringQueryMap 注解。
java
@FeignClient(name = "service-provider")
public interface FeignClientDemo {
// Feign 多参数传递问题解决方案1:参数注解 @SpringQueryMap
@GetMapping("/user1")
User getUserBySpringQueryMap(@SpringQueryMap User user);
}启动服务测试,可以成功请求.
解决方案2:独立参数
另一种解决方案是,URL 中有几个参数,Feign 接口中的方法使用 @RequestParam 注解指定相应的请求的参数。修改服务消费者 Feign 接口方法
java
@FeignClient(name = "service-provider")
public interface FeignClientDemo {
// Feign 多参数传递问题解决方案2:使用 @RequestParam 注解指定多个独立参数
@GetMapping("/user")
User getUserByParam(@RequestParam("id") Integer id,
@RequestParam("name") String name,
@RequestParam("city") String city,
@RequestParam("email") String email);
}修改服务消费者调用新的 Feign 接口,启动服务测试,可以成功请求.
处理复杂参数形式
实际开发中,可能会通过多种方式进行参数传递,例如服务提供者有以下形式的接口:
java
@RestController
public class DemoController {
@PostMapping("/demo_complex/{id}")
public String demo_complex(@PathVariable("id") String id,
@RequestBody Map<String, Object> map,
@RequestParam String name) {
Object json = JSON.toJSON(map);
return "PathVariable id :" + id + "\nRequestParam name : " + name + "\nRequestBody map: " + json.toString();
}
}服务消费者定义接口,与服务提供者的接口一致即可
java
@FeignClient(name = "service-provider")
public interface FeignClientDemo {
@RequestMapping(path = "/demo_complex/{id}", method = RequestMethod.POST)
String demo_complex(@PathVariable("id") String id,
@RequestBody Map<String, Object> map,
@RequestParam("name") String name);
}问题记录
做些示例项目时,使用 spring-cloud-starter-openfeign 是 2.1.5.RELEASE 版本,启动时会提示 RequestParam.value() was empty on parameter 2 错误。

后面查询相关资料,才发现原来忘记 FeignClient 接口定义接口时一个很重要的注意点(之前已经做过笔记,居然忘记了,看来技术还是要经常使用,才能记得细节),当 FeignClient 方法参数使用了 @PathVariable("XXX")和@RequestParam("XXX")注解,就必须要指定对应的参数值(原来SpringMVC是可以省略),否则启动报错。
以上示例修改为 @RequestParam("name") String name 后就能正常启动。
文件上传
早期版本的 Feign 是没有上传文件的功能,需要开发者去编写 Encoder 去实现上传。但目前 Feign 官方提供了子项目 feign-form,实现了上传所需的 Encoder。实现步骤如下:
- 在服务提供者定义接口文件上传请求的接口
java
@PostMapping(value = "/uploadFile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String fileUpload(MultipartFile file) throws Exception {
// 测试只作简单的返回文件名称
return file.getOriginalFilename();
}- 服务消费者工程 pom.xml 引入 feign-form 依赖
xml
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form</artifactId>
<version>3.8.0</version>
</dependency>
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form-spring</artifactId>
<version>3.8.0</version>
</dependency>- 服务消费者 Feign Client 接口中定义相关调用文件上传接口的方法
java
@FeignClient(name = "service-provider")
public interface FeignClientDemo {
@RequestMapping(value = "/uploadFile", method = RequestMethod.POST,
produces = {MediaType.APPLICATION_JSON_UTF8_VALUE},
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
String handleFileUpload(@RequestPart(value = "file") MultipartFile file);
}- 编写服务消费者控制层接口,通过 Feign Client 调用文件上传
java
@PostMapping(value = "/upload")
public String fileUpload(MultipartFile file) throws Exception {
return feignClientDemo.handleFileUpload(file);
}- 使用 postman 上传文件测试

使用 HttpClient 与 OkHttp 远程调用
Feign 的 HTTP 客户端支持 3 种框架:
- HttpURLConnection(默认)
- HttpClient
- OkHttp
传统的 HttpURLConnection 是 JDK 自带的,并不支持连接池,效率非常低。为了提高效率,Feign 支持替换支持连接池的的请求客户端 appache httpclient 和 okhttp
HttpClient
在消费者工程中,使用 HttpClient 做为 Feign HTTP 客户端步骤如下:
- 添加 HttpClient 依赖
xml
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>Tips: feign-httpclient 不需要指定版本号,因为 Spring Cloud 中导入 spring-cloud-openfeign-dependencies 已指定 feign-httpclient 的依赖版本管理
- 修改核心配置文件
feign.httpclient.enabled选项,开启 HttpClient 的支持。
yml
feign:
httpclient:
enabled: true # 开启 httpclient
max-connections: 200 # 最大连接数
max-connections-per-route: 50 # 单个路由的最大连接数
logging:
level:
org.apache.http.wire: debug # 开启 HttpClient 相关接口的日志
org.apache.http.headers: debug- 启动服务发送 get 请求
/hello接口测试,观察日志验证是否输出 HttpClient 相关日志

OkHttp
在消费者工程中,使用 OkHttp 做为 Feign HTTP 客户端步骤如下:
- 添加 OkHttp 依赖
xml
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
<version>10.2.0</version>
</dependency>
<dependency>
<groupId>com.parkingwang</groupId>
<artifactId>okhttp3-loginterceptor</artifactId>
<version>0.5</version>
</dependency>- 修改核心配置文件
feign.okhttp.enabled选项,开启 OkHttp 的支持。
yml
feign:
okhttp:
enabled: true # 开启 okhttp
logging:
level:
okhttp3: debug # 开启 OkHttp 相关接口的日志- 创建 OKHttp 配置类,配置连接池,以及日志拦截器
java
@Configuration
@ConditionalOnClass(Feign.class)
@AutoConfigureBefore(FeignAutoConfiguration.class)
public class OkHttpConfiguration {
/* 配置 OkHttp Client */
@Bean
public OkHttpClient okHttpClient() {
// 设置相关配置项
return new OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS) // 1.连接超时时间
.readTimeout(20, TimeUnit.SECONDS) // 2.响应超时时间
.writeTimeout(20, TimeUnit.SECONDS) // 3.写超时时间
.retryOnConnectionFailure(true) // 4.自动重连
.connectionPool(new ConnectionPool()) // 5.配置连接池
.addInterceptor(new LogInterceptor()) // 6.添加日志拦截器
.build();
}
}- 启动服务发送 get 请求
/hello接口测试,观察日志验证是否输出 OkHttp 相关日志

Feign 工作原理
开发编码层面流程梳理

FeignClient 代理工作流程

Feign 最终整体调用流程

OpenFeign 的核心流程梳理
- 在 Spring 项目启动阶段,服务 A 的 OpenFeign 框架会发起一个主动的扫包流程。
- 从指定的目录下扫描并加载所有被
@FeignClient注解修饰的接口,然后将这些接口转换成 Bean,统一交给 Spring 来管理。 - 根据这些接口会经过 MVC Contract 协议解析,将方法上的注解都解析出来,放到 MethodMetadata 元数据中。
- 基于上面加载的每一个 FeignClient 接口,会生成一个动态代理对象,指向了一个包含对应方法的 MethodHandler 的 HashMap。MethodHandler 对元数据有引用关系。生成的动态代理对象会被添加到 Spring 容器中,并注入到对应的服务里。
- 服务 A 调用接口,准备发起远程调用。
- 从动态代理对象 Proxy 中找到一个 MethodHandler 实例,生成 Request,包含有服务的请求 URL(不包含服务的 IP)。
- 经过负载均衡算法找到一个服务的 IP 地址,拼接出请求的 URL
- 服务 B 处理服务 A 发起的远程调用请求,执行业务逻辑后,返回响应给服务 A。
Feign 源码分析
通过使用过程可知,@EnableFeignClients和@FeignClient两个注解就实现了Feign的功能,所以从@EnableFeignClients注解开始分析Feign的源码
@EnableFeignClients 注解
java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
// ....省略代码
}通过 @EnableFeignClients 引入了 FeignClientsRegistrar 客户端注册类。
FeignClientsRegistrar 客户端注册类
java
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar,
ResourceLoaderAware, EnvironmentAware {
// ....省略代码
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
// 注册默认配置
registerDefaultConfiguration(metadata, registry);
registerFeignClients(metadata, registry);
}
// ....省略代码
}根据源码可知,FeignClientsRegistrar类实现了ImportBeanDefinitionRegistrar接口,在实现的registerBeanDefinitions()里就会解析和注册BeanDefinition,主要注册的对象类型有两种:
- 注册缺省配置的配置信息
java
private void registerDefaultConfiguration(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
Map<String, Object> defaultAttrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName(), true);
if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
String name;
if (metadata.hasEnclosingClass()) {
name = "default." + metadata.getEnclosingClassName();
}
else {
name = "default." + metadata.getClassName();
}
registerClientConfiguration(registry, name,
defaultAttrs.get("defaultConfiguration"));
}
}- 注册添加了标识
@FeignClient注解的类或接口
java
public void registerFeignClients(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);
Set<String> basePackages;
Map<String, Object> attrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName());
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
FeignClient.class);
final Class<?>[] clients = attrs == null ? null
: (Class<?>[]) attrs.get("clients");
if (clients == null || clients.length == 0) {
scanner.addIncludeFilter(annotationTypeFilter);
basePackages = getBasePackages(metadata);
}
else {
final Set<String> clientClasses = new HashSet<>();
basePackages = new HashSet<>();
for (Class<?> clazz : clients) {
basePackages.add(ClassUtils.getPackageName(clazz));
clientClasses.add(clazz.getCanonicalName());
}
AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
@Override
protected boolean match(ClassMetadata metadata) {
String cleaned = metadata.getClassName().replaceAll("\\$", ".");
return clientClasses.contains(cleaned);
}
};
scanner.addIncludeFilter(
new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
}
for (String basePackage : basePackages) {
Set<BeanDefinition> candidateComponents = scanner
.findCandidateComponents(basePackage);
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) {
// verify annotated class is an interface
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
Assert.isTrue(annotationMetadata.isInterface(),
"@FeignClient can only be specified on an interface");
Map<String, Object> attributes = annotationMetadata
.getAnnotationAttributes(
FeignClient.class.getCanonicalName());
String name = getClientName(attributes);
registerClientConfiguration(registry, name,
attributes.get("configuration"));
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
}registerFeignClients()方法主要是扫描类路径,对所有的FeignClient生成对应的BeanDefinition。同时又调用了 registerClientConfiguration 注册配置的方法。这里是第二次调用,主要是将扫描的目录下,每个项目的配置类加载的容器当中。调用 registerFeignClient 注册对象
FeignClient 对象的注册
在上一步中,获取@FeignClient注解的数据封装到一个map集合后,调用registerFeignClient(registry, annotationMetadata, attributes);方法,往spring容器中注册BeanDefinition对象
java
private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
// 1. 获取类名称,也就是本例中的FeignService接口
String className = annotationMetadata.getClassName();
/*
* 2. BeanDefinitionBuilder的主要作用就是构建一个AbstractBeanDefinition
* AbstractBeanDefinition类最终被构建成一个BeanDefinitionHolder然后注册到Spring容器中
* 注意:beanDefinition类为FeignClientFactoryBean,所以在Spring获取类的时候实际返回的是FeignClientFactoryBean类
*/
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientFactoryBean.class);
validate(attributes);
// 3. 添加FeignClientFactoryBean的属性,这些属性都是在@FeignClient中定义的属性
definition.addPropertyValue("url", getUrl(attributes));
definition.addPropertyValue("path", getPath(attributes));
String name = getName(attributes);
definition.addPropertyValue("name", name);
String contextId = getContextId(attributes);
definition.addPropertyValue("contextId", contextId);
definition.addPropertyValue("type", className);
definition.addPropertyValue("decode404", attributes.get("decode404"));
definition.addPropertyValue("fallback", attributes.get("fallback"));
definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
// 4. 设置别名 name就是我们在@FeignClient中定义的name属性
String alias = contextId + "FeignClient";
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
boolean primary = (Boolean)attributes.get("primary"); // has a default, won't be null
beanDefinition.setPrimary(primary);
String qualifier = getQualifier(attributes);
if (StringUtils.hasText(qualifier)) {
alias = qualifier;
}
// 5. 定义BeanDefinitionHolder,在本例中名称为FeignService,类为FeignClientFactoryBean
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
new String[] { alias });
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}通过源分析可知:最终是向Spring中注册了一个bean,bean的名称就是类或接口的名称(也就是本例中的FeignService),bean的实现类是FeignClientFactoryBean,其属性设置就是在@FeignClient中定义的属性。那么下面在Controller中对FeignService的的引入,实际就是引入了FeignClientFactoryBean类
FeignClientFactoryBean 类
对@EnableFeignClients注解的源码进行了分析,了解到其主要作用就是把带有@FeignClient注解的类或接口用FeignClientFactoryBean类注册到Spring容器中。
java
class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,
ApplicationContextAware {
// ....省略代码
@Override
public Object getObject() throws Exception {
return getTarget();
}
// ....省略代码
}通过 FeignClientFactoryBean 类结构可以发现其实现了FactoryBean<T>类,那么当从ApplicationContext中获取该bean的时候,实际调用的是其getObject()方法。方法中返回是调用getTarget()方法
java
<T> T getTarget() {
FeignContext context = applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context);
if (!StringUtils.hasText(this.url)) {
if (!this.name.startsWith("http")) {
url = "http://" + this.name;
}
else {
url = this.name;
}
url += cleanPath();
return (T) loadBalance(builder, context, new HardCodedTarget<>(this.type,
this.name, url));
}
if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
this.url = "http://" + this.url;
}
String url = this.url + cleanPath();
Client client = getOptional(context, Client.class);
if (client != null) {
if (client instanceof LoadBalancerFeignClient) {
// not load balancing because we have a url,
// but ribbon is on the classpath, so unwrap
client = ((LoadBalancerFeignClient)client).getDelegate();
}
builder.client(client);
}
Targeter targeter = get(context, Targeter.class);
return (T) targeter.target(this, builder, context, new HardCodedTarget<>(
this.type, this.name, url));
}代码流程说明:
FeignClientFactoryBean类实现三个接口,分别是:FactoryBean接口的getObject、getObjectType、isSingleton方法;- 实现了
InitializingBean接口的afterPropertiesSet方法; - 实现了
ApplicationContextAware接口的setApplicationContext方法
getObject()方法中调用的是getTarget()方法,它从applicationContext取出FeignContext,然后构造Feign.Builder并设置了logger、encoder、decoder、contract,之后通过configureFeign根据FeignClientProperties来进一步配置Feign.Builder的retryer、errorDecoder、request.Options、requestInterceptors、queryMapEncoder、decode404- 初步配置完
Feign.Builder之后再判断是否需要loadBalance,如果需要则通过loadBalance方法来设置,不需要则在Client是LoadBalancerFeignClient的时候进行unwrap
发送请求的实现
从上面的源码分析可知,FeignClientFactoryBean.getObject()具体返回的是一个代理类,具体为FeignInvocationHandler
java
public class ReflectiveFeign extends Feign {
// ....省略代码
static class FeignInvocationHandler implements InvocationHandler {
private final Target target;
private final Map<Method, MethodHandler> dispatch;
FeignInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) {
this.target = checkNotNull(target, "target");
this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("equals".equals(method.getName())) {
try {
Object otherHandler =
args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
return equals(otherHandler);
} catch (IllegalArgumentException e) {
return false;
}
} else if ("hashCode".equals(method.getName())) {
return hashCode();
} else if ("toString".equals(method.getName())) {
return toString();
}
return dispatch.get(method).invoke(args);
}
@Override
public boolean equals(Object obj) {
if (obj instanceof FeignInvocationHandler) {
FeignInvocationHandler other = (FeignInvocationHandler) obj;
return target.equals(other.target);
}
return false;
}
@Override
public int hashCode() {
return target.hashCode();
}
@Override
public String toString() {
return target.toString();
}
}
// ....省略代码
}FeignInvocationHandler类说明:
FeignInvocationHandler实现了InvocationHandler接口,是动态代理的代理类。- 当执行非
Object方法时进入到this.dispatch.get(method)).invoke(args) - dispatch是一个map集合,根据方法名称获取
MethodHandler。具体实现类为SynchronousMethodHandler
java
final class SynchronousMethodHandler implements MethodHandler {
// ....省略代码
@Override
public Object invoke(Object[] argv) throws Throwable {
RequestTemplate template = buildTemplateFromArgs.create(argv);
Retryer retryer = this.retryer.clone();
while (true) {
try {
return executeAndDecode(template);
} catch (RetryableException e) {
// ....省略代码
}
}
}
Object executeAndDecode(RequestTemplate template) throws Throwable {
Request request = targetRequest(template);
if (logLevel != Logger.Level.NONE) {
logger.logRequest(metadata.configKey(), logLevel, request);
}
Response response;
long start = System.nanoTime();
try {
response = client.execute(request, options);
} catch (IOException e) {
if (logLevel != Logger.Level.NONE) {
logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
}
throw errorExecuting(request, e);
}
// ....省略代码
}
// ....省略代码
}SynchronousMethodHandler内部创建了一个RequestTemplate对象,是Feign中的请求模板对象。内部封装了一次请求的所有元数据。retryer中定义了用户的重试策略。- 调用
executeAndDecode方法通过client完成请求处理,client的实现类是LoadBalancerFeignClient
相关知识
Ribbon 和 Feign 调用服务的区别
主要是调用方式不同:
- Ribbon 需要开发者自己构建 Http 请求,模拟 Http 请求然后通过 RestTemplate 发给其他服务,步骤相当繁琐;
- Feign 则是在 Ribbon 的基础上进行了一次改进,采用接口的形式,将需要调用的服务方法定义成抽象方法保存在本地就可以了,不需要开发者构建 Http 请求了,直接调用接口即可。值得注意的是,调用方法要和本地抽象方法的签名完全一致。