概述

Feign 是一个声明式的 Web 服务,通过定义一个添加相应注解的接口,即可完成一个 Web 服务的接口。SpringCloud 对 Feign 进行了封装以后,其开始能够支持 Spring MVC 标准注解,同时在 SpringCloud 架构上结合 Eureka 和 Ribbon,还能够支持负载均衡。

既然是一个 Web 服务,必然服务端模块与客户端模块都加入 Feign 依赖以及对接的 api 接口,这是 Feign 服务的基本前提。因此双方引入的 Feign 接口都要保持一致,包括服务地址、入参定义、返回值等。

基本配置

要启用 @FeignClient 接口,首先需要引入 Feign 依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

在父 pom.xml 里还需要引入 SpringCloud 依赖,这样使用 FeignClient 才有意义。

在启动类上添加启动注解 @EnableFeignClients

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
 
@SpringBootApplication
@EnableFeignClients
public class FeignDemoApp {
    public static void main(String[] args) {
        SpringApplication.run(FeignDemoApp.class, args);
    }
}

然后定义 Feign 客户端提供的服务接口,示例代码如下:

import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
 
import com.demo.feign.model.request.FeignDemoDetailRequest;
import com.demo.feign.model.response.FeignDemoDetailResponse;
import com.demo.feign.model.response.FeignDemoListResponse;

@FeignClient("spring-boot-feign-demo")
public interface FeignDemoClient {
 
    @RequestMapping(value="/demo/list", method=RequestMethod.GET)
    public FeignDemoListResponse queryListByCode(@PathVariable("code") String code);
     
    @RequestMapping(value="/demo/detail", method=RequestMethod.POST)
    public FeignDemoDetailResponse queryDetail(@RequestBody FeignDemoDetailRequest request);
}

这里定义的 FeignDemoDetailRequest,FeignDemoDetailResponse 和 FeignDemoListResponse 是定义的实体类,具体就不展示了。

常见问题

  • Feign 的 maven 依赖报红,或者主启动类上@EnableFeignClients不识别,一直报红,或者 feign 接口注入到控制器报红。

可能的原因及处理方案:

  1. maven 包没有成功导入,或者 maven 包下载不完整,可以从仓库删掉依赖包后重新下载导入;
  2. 版本冲突,或者无法获取适当版本,此时可以在 maven 包导入时指定版本号,尝试其他可用的版本。
  • 项目 Application 启动时,使用 @Autowired 自动注入的 FeignClient 接口被告知对应的 Bean 无法寻获
***************************
APPLICATION FAILED TO START
***************************
 
Description:
Field feignDemoClient in com.xxx.xxx.service.impl.DemoServiceImpl required a bean of type 'com.xxx.xxx.feign.FeignDemoClient' that could not be found.
The injection point has the following annotations:
	- @org.springframework.beans.factory.annotation.Autowired(required=true)
 
Action:
Consider defining a bean of type 'com.xxx.xxx.feign.FeignDemoClient' in your configuration.
Disconnected from the target VM, address: '127.0.0.1:51645', transport: 'socket'
Process finished with exit code 1

可能的原因及处理方案:

  1. 服务所在模块没有在 pom.xml 引入 spring-cloud-starter-openfeign 这个 maven 依赖,补上这个依赖重新构建项目即可。
  2. 项目启动没有扫描 FeignClient 接口所在的包。项目通过启动类启动时默认会扫描同目录及同目录下级目录的类文件。所以,Spring注入第三方包或者其他模块的包,需要扫描需要注入的包。这种情况,只需要在启动类的注解中指定需要扫描的包路径即可,如:
// 开启Feign客户端,指定扫描 FeignClient 接口类所在的包
@EnableFeignClients("com.xxx.xxx.feign")
@SpringBootApplication
  • 正确添加 Feign 依赖,启动类扫描 api 接口所有包路径,但注入接口仍然报红

这种情况现在比较少见,但是在一段时间以前出现比较频繁,主要是在 SpringBoot 2.0 版本后优化了相关逻辑。在 SpringBoot 2.0 之前,如果你的 Feign 接口使用 GetMapping 注解,那么注入该接口都会报红,无法注入。相应的,修改成 RequestMapping 或者 PostMapping 就能注入了。

处理方案:

1、采用合适的注解形式定义 Feign 接口。目前可行的定义方式基本有以下几种:

@FeignClient("spring-boot-feign-demo")
public interface FeignDemoClient {
    
    // 使用RequestMapping指定Get方法,入参单个传入
    @RequestMapping(value="/demo/list", method=RequestMethod.GET)
    public FeignDemoListResponse queryListByCode(@PathVariable("code") String code);
     
    // 使用RequestMapping指定Post方法,传入实体使用注解@RequestBody
    @RequestMapping(value="/demo/detail", method=RequestMethod.POST)
    public FeignDemoDetailResponse queryDetail(@RequestBody FeignDemoDetailRequest request);
    
    // 使用PostMapping,传入实体使用注解@RequestBody
    @PostMapping("/demo/seq")
    public FeignDemoSeqResponse querySeq(@RequestBody FeignDemoSeqRequest request);
}
  • 通过 FeignClient 发起 Get 请求报 405 错误

通过在服务端断点捕获异常可以发现,报 405 错误的直接原因,其实是因为定义为 Get 的 Feign 接口,接收到 Post 方法的调用。但是在调用方调用时大多数也是采用 Get 方法,那么到底是什么原因呢?

暂且预设 Feign 接口的定义如下:

@FeignClient("spring-boot-feign-demo")
public interface FeignDemoClient {
    // 服务方接收到的请求是Post方法,而不是Get方法
    @GetMapping(value="/demo/list")
    public Response queryList(Request request);

实际上,通过断点跟踪接口调用时的调用路径,就会发现 FeignClient 最后是通过 HttpURLConnection 发起的网络连接,在发起的过程中,Connection 会判断自身请求的 body 是否为空。如果 body 不为空,则将 Get 方法转换成 Post 方法。因为 body 形式的数据只能方法 RequestBody 内以流的形式进行传输,而根据 Http 协议 param 形式的数据可以直接放在 URL 上进行传输和获取。

之所以 FeignClient 在网络请求时会出现这种转换,这跟它的初始化规则有关。在项目启动过程中,@FeignClient 直接的类会被初始化一个动态代理的类,并通过一个 RequestTemplate.Factory 的工厂类生成请求模板,具体规则如下:

  1. 如果基本类型参数有 @RequestParam 注解,则会将参数作为 key 放入 RequestTemplate.Factory 中,通过 urlIndex 记录数组索引。在进行解析时,通过 key 从数组中获取具体的参数值,拼接在 url 后面。
  2. 如果参数没有任何注解,或者有 @RequestBody 注解,那么初始化时会使用 bodyIndex 维护参数索引,并通过 bodyType 记录参数的具体类型。

需要注意的是,@RequestParam 只能用于对单个基本类型参数的注解,不能用来注解一个实体类。使用实体类作为入参出参时,建议还是使用 Post 方法进行请求。

如果在开发中觉得维护以上的对应关系不方便的话,还有另外一种修改方法可供使用,基本原理就是使用 Apache 的 HTTP Client 替换 Feign 原生的 Http Client,替换后 Get 方法也可以用一个实体类作为请求参数而不用担心请求被转换成 Post 方法了。具体修改方式如下:

  1. 引入 http client 依赖
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId><br>    
    <version>4.5.2</version>
</dependency>
<dependency>
    <groupId>com.netflix.feign</groupId>
    <artifactId>feign-httpclient</artifactId>
    <version>8.18.0</version>
</dependency>
  1. 开启 Feign 对 httpClient 的设置
feign.httpclient.enabled=true

完成配置修改后,内部就可以使用 Apache 的 http client。Feign 接口的定义是相同的,但是由于 Get 方法支持了自定义实体类,与 Post 有类似的处理方式,因此参数的传输需要我们额外指定其类型,以确保 JSON 序列化与反序列化的正常进行。

这里给出一个示例接口定义:

@FeignClient("spring-boot-feign-demo")
public interface FeignDemoClient {
    @PostMapping(value="/demo/detail", consumes = MediaType.APPLICATION_JSON_VALUE)
    public Response<List<ResultDTO>> query(@RequestBody FeignQueryRequest request, 
                                           @PathVariable("page") Integer page, 
                                           @PathVariable("page_size") Integer pageSize);
}
  • JSON 反序列化失败
Error while extracting response for type [com.xxx.xxx.feign] and content type [application/json].Can not deserialize instance of java.util.ArrayList out of START_OBJECT token

翻译过来的意思大致就是,Feign 接口的返回值无法通过 application/json 的格式解析出来,也就是调用方返回值的定义就服务方不一致(结构不同)。

处理方案:

  1. 确保双方引入的接口 api 完全一致,可通过将接口所在模块打包成 jar 包,双方引用同一个 jar 等方式以保证接口定义一致。
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐