OpenFeign 简介和使用详解

1. 概述

1.1 OpenFeign是什么

Feign是一个声明式的Web服务客户端(Http客户端),让编写WEB服务客户端变得非常容易,只需要创建一个接口并在接口上添加注解即可

HTTP 客户端: 当我们自己的后端项目需要调用别的项目的接口时,就需要通过HTTP客户端来调用,在实际开发过程中经常会遇到这种场景,比如微服务之间调用,

除了微服务之外,可能会涉及到对接一些第三方接口也需要用到HTTP客户端来调用第三方接口。

1.2 OpenFeign能干什么

Java当中常见的HTTP客户端有很多,除了Feign,类是的还有Apache的HttpClient以及OKHttp3,还有SpringBoot自带的RestTemplate,这些都是Java当中常见的HTTP客户端。

所有的客户端相比较:Feign更加简单一点,在Feign的实现下,我们只需要创建一个接口并时用注解的方式来配置它,即可完成对服务提供方的接口绑定。

1.3 OpenFeign和Feign的区别

  • Feign

    Feign是Spring Cloud组件中一个轻量级RESTful的HTTP服务客户端,Feign内置了Ribbon用来做客户端负载均衡,去调用服务注册中心的服务;
    Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用注册中心的服务

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-feign</artifactId>
    </dependency>
  • OpenFeign

    OpenFeign是Spring cloud在Feign的基础上支持了Springmvc的注解,如@RequestMapping等。OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>

Feign是在2019年就不再更新了,通过Maven网站就可以看出来,随之取代的是OpenFeign,从名字上就可以知道,是Feign的升级版。

1.4 @FeignClient

使用OpenFeign就一定会用到这个注解,@FeignClient属性如下:

  • name: 指定该类的容器名称,类似于@Service(容器名称)

  • url: url一般用于调试,可以手动指定@FeignClient调用的地址

  • decode404: 当发生HTTP 404错误是,如果该字段为true,会调用decoder进行解码,否则抛出FeignClientException

  • configuration: Feign配置类,可以自定义Feign的Encoder、Decoder、LogLevel、Contract

  • fallback: 定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback指定的类必须实现@FeignClient标记的接口

  • fallbackFactory: 工厂类,用于生成fallback类实例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复代码

  • path:定义当前FeignClient统一前缀,当我们项目中配置了server.context-path时使用

下面这两种写法本质没有区别,他们都是将FeignClient注入到Spring容器中

  • 写法1

    1
    2
    3
    @FeignClient(name = "feignTestService", url = "http://localhost/8001")
    public interface FeignTestService {
    }
  • 写法2

    1
    2
    3
    4
    @Component
    @FeignClient(url = "http://localhost/8001")
    public interface FeignTestService {
    }

远程调用接口当中,一般我们称提供接口的服务为提供者,而调用接口的服务为消费者。而OpeneFeign一定是用在消费者上。

2. OpenFeign使用

2.1 OpenFeign常规远程调用

所谓常规远程调用,指的是对接第三方接口,和第三方并不是微服务模块关系,所以肯定不能通过注册中心来调用服务

  1. 添加OpenFeign依赖
    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
  2. 启动类添加@EnableFeignClients
    1
    2
    3
    4
    5
    6
    7
    @SpringBootApplication
    @EnableFeignClients
    public class ConsumeApplication {
    public static void main(String[] args) {
    SpringApplication.run(ConsumeApplication.class, args);
    }
    }
  3. provider提供接口
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    @RestController
    @RequestMapping("/provider")
    public class ProviderController {

    @GetMapping("/select")
    public String select(@RequestParam int pageId, @RequestParam int pageSize) {
    JSONObject jsonpObject = new JSONObject();
    jsonpObject.append("pageId", pageId);
    jsonpObject.append("pageSize", pageSize);
    jsonpObject.append("mesg","查询select接口成功");
    return jsonpObject.toString();
    }

    @GetMapping("/selectByPrice")
    public String selectByPrice(@RequestParam double price) {
    JSONObject jsonpObject = new JSONObject();
    jsonpObject.append("price", price);
    jsonpObject.append("mesg","查询selectByPrice接口成功");
    return jsonpObject.toString();
    }

    @PostMapping(value = "/create",consumes = "application/json")
    public String create(@RequestBody double price) {
    JSONObject jsonpObject = new JSONObject();
    jsonpObject.append("price", price);
    jsonpObject.append("mesg","create接口成功");
    return jsonpObject.toString();
    }
    }
  4. consume调用provider接口
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /**
    * provider服务设置了context-path=/provider,所以接口上需要加上/provider
    * @author xiaoyuge
    */
    @FeignClient(value = "feignService", url = "http://localhost:8082")
    public interface FeignService {

    @GetMapping(value = "/provider/select")
    String select(@RequestParam("pageId") int pageId, @RequestParam("pageSize") int pageSize);

    @GetMapping(value = "/provider/selectByPrice")
    String selectByPrice(@RequestParam("price") double price);

    @PostMapping(value = "/provider/create", consumes = "application/json")
    String create(@RequestBody double price);
    }
  5. 消费者添加Controller 测试
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    /**
    * @author xiaoyuge
    */
    @RestController
    public class ExampleController {

    @Resource
    private FeignService feignService;

    @RequestMapping("/select")
    public String select() {
    return feignService.select(1, 10);
    }

    @RequestMapping("/selectByPrice")
    public String selectByPrice() {
    return feignService.selectByPrice(50d);
    }

    @RequestMapping("/create")
    public String create() {
    return feignService.selectByPrice(40d);
    }
    }
  6. 测试访问http://localhost:8081/select

@SpringQueryMap注解

Spring cloud项目使用feign的时候都会发现一个问题,就是get方式无法解析对象参数,其实feign是支持对象传递的,但是得是Map形式,而且不能为空,与Spring在机制上不兼容,因此无法使用

Spring cloud 2.1x版本提供了 @SpringQueryMap注解,可以传递对象参数,框架自动解析

2.2 OpenFeign微服务使用步骤

微服务之间使用OpenFeign,肯定是要通过注册中心来访问服务的,提供者将自己的IP+端口注册到注册中心,然后对外提供一个服务名称,消费者根据服务名称去注册中心寻找IP+端口

那么对上面的代码进行改造:

  1. 作为消费者,想要调用提供者服务需要以下内容

    消费者想要通过服务名称来调用提供者,那么就一定需要配置注册中心的服务发现功能,假如提供者使用的是Eureka,那么消费者就必须配置Eureka服务发现。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //PROVIDER-SERVICE 是提供者的服务名称
    @FeignClient(value = "SLEUTH-PROVIDER")
    public interface FeignService {

    @GetMapping(value = "/provider/select")
    String select(@RequestParam("pageId") int pageId, @RequestParam("pageSize") int pageSize);

    @GetMapping(value = "/provider/selectByPrice")
    String selectByPrice(@RequestParam("price") double price);

    @PostMapping(value = "/provider/create", consumes = "application/json")
    String create(@RequestBody double price);
    }
  2. 提供者的接口,提供者可以是集群
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @RestController
    public class ProviderController {

    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/select")
    public String select(@RequestParam int pageId, @RequestParam int pageSize) {
    JSONObject jsonpObject = new JSONObject();
    jsonpObject.append("pageId", pageId);
    jsonpObject.append("pageSize", pageSize);
    jsonpObject.append("mesg", "当前端口:" + serverPort);
    return jsonpObject.toString();
    }
    }
  3. 启动注册中心Eureka以及两个服务提供者
    访问 http://localhost:8081/select 会轮询调用两个服务提供者的接口

    使用OpenFeign,根据服务名称调用,OpenFeign他本身就集成了ribbon自带负载均衡。

2.3 OpenFeign超时控制

  1. 提供方接口,制造超时场景

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @GetMapping("/select")
    public String select(@RequestParam int pageId, @RequestParam int pageSize) {
    try{
    //暂停几秒
    TimeUnit.SECONDS.sleep(4);
    }catch (Exception ex){
    ex.printStackTrace();
    }
    JSONObject jsonpObject = new JSONObject();
    jsonpObject.append("pageId", pageId);
    jsonpObject.append("pageSize", pageSize);
    jsonpObject.append("mesg","当前端口:"+serverPort);
    return jsonpObject.toString();
    }
  2. 消费者增加超时设置,并调用该接口

    1
    2
    3
    4
    5
    feign:
    client:
    config:
    default:
    readTimeout: 3000 #请求超时时间

    当消费方调用提供方时候,超时时间为3s,超过后报错

  3. 在消费者增加如下配置

    1
    2
    3
    4
    5
    6
    #设置feign客户端超时时间(OpenFeign默认支持ribbon)
    ribbon:
    #指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
    ReadTimeout: 5000
    #指的是建立连接后从服务器读取到可用资源所用的时间
    ConnectTimeout: 5000

    在OpenFeign高版本中,可以默认客户端和命名客户端上配置超时,OpenFeign使用两个参数:

    • connectTimeout: 防止由于服务器处理时间长而阻塞调用者
    • readTimeout:从连接建立时开始,在返回响应时间过长触发

2.4 OpenFeign 日志打印

Feign提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解Feign中HTTP请求的细节。

说白了就是对Feign接口的调用请求进行监控和输出。

日志级别:

  • NONE:默认的,不显示任何日志

  • BASIC:仅记录请求方法、URL、响应状态吗和执行时间

  • HEADERS:除了BASIC中定义的信息之外,还有请求和响应头信息

  • FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文以及元数据

配置日志Bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* @author xiaoyuge
*/
@Configuration
public class FeignConfig {

/**
* 日志级别设置
*/
@Bean
Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}

/**
* 超时时间设置
*/
@Bean
public Request.Options options(){
return new Request.Options(5000, 400);
}
}

Yaml文件需要开启日志的Feign客户端

1
2
3
4
5
#设置日志级别
logging:
level:
org.example.FeignService: DEBUG #这是我的接口
#org.springframework.web.servlet.DispatcherServlet: DEBUG

后台日志查看:

1
2
3
4
5
6
7
8
9
10
11
2023-07-16 18:03:22.274 DEBUG 70940 --- [nio-8081-exec-1] org.example.FeignService  : [FeignService#select] ---> GET http://SLEUTH-PROVIDER/provider/select?pageId=1&pageSize=10 HTTP/1.1
2023-07-16 18:03:22.274 DEBUG 70940 --- [nio-8081-exec-1] org.example.FeignService : [FeignService#select] ---> END HTTP (0-byte body)
2023-07-16 18:03:25.477 DEBUG 70940 --- [nio-8081-exec-1] org.example.FeignService : [FeignService#select] <--- HTTP/1.1 200 (3199ms)
2023-07-16 18:03:25.477 DEBUG 70940 --- [nio-8081-exec-1] org.example.FeignService : [FeignService#select] connection: keep-alive
2023-07-16 18:03:25.477 DEBUG 70940 --- [nio-8081-exec-1] org.example.FeignService : [FeignService#select] content-length: 61
2023-07-16 18:03:25.477 DEBUG 70940 --- [nio-8081-exec-1] org.example.FeignService : [FeignService#select] content-type: text/plain;charset=UTF-8
2023-07-16 18:03:25.478 DEBUG 70940 --- [nio-8081-exec-1] org.example.FeignService : [FeignService#select] date: Sun, 16 Jul 2023 10:03:25 GMT
2023-07-16 18:03:25.478 DEBUG 70940 --- [nio-8081-exec-1] org.example.FeignService : [FeignService#select] keep-alive: timeout=60
2023-07-16 18:03:25.478 DEBUG 70940 --- [nio-8081-exec-1] org.example.FeignService : [FeignService#select]
2023-07-16 18:03:25.479 DEBUG 70940 --- [nio-8081-exec-1] org.example.FeignService : [FeignService#select] {"pageSize":[10],"pageId":[1],"mesg":["当前端口:8083"]}
2023-07-16 18:03:25.480 DEBUG 70940 --- [nio-8081-exec-1] org.example.FeignService : [FeignService#select] <--- END HTTP (61-byte body)

2.5 OpenFeign 添加Header

提供了四种方式:

  1. @RequestMapping中添加

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /**
    * @author xiaoyuge
    */
    @FeignClient(value = "SLEUTH-PROVIDER")
    public interface FeignService {

    @RequestMapping(value = "/provider/queryByIds",method = RequestMethod.GET
    ,headers = {"Content-Type=application/json;charset=UTF-8"})
    String queryByIds(@RequestParam("price") Long[] ids);

    }
  2. 在方法参数签名添加@RequestHeader注解

    1
    2
    3
    4
    5
    6
    7
    8
    9

    @FeignClient(value = "SLEUTH-PROVIDER")
    public interface FeignService {

    @RequestMapping(value = "/provider/queryByIds",method = RequestMethod.GET
    ,headers = {"Content-Type=application/json;charset=UTF-8"})
    String queryByIds(@RequestParam("price") @RequestHeader("Authorization") Long[] ids);

    }

    设置多个属性时,可以使用Map:

    1
    2
    3
    4
    5
    6
    7
    8
    @FeignClient(value = "SLEUTH-PROVIDER")
    public interface FeignService {

    @RequestMapping(value = "/provider/queryByIds",method = RequestMethod.GET
    ,headers = {"Content-Type=application/json;charset=UTF-8"})
    String queryByIds(@RequestParam("price") Long[] ids, @RequestHeader MultiValueMap<String, String> headers);

    }
  3. 使用@Header注解

    1
    2
    3
    4
    5
    6
    7
    8
    @FeignClient(value = "SLEUTH-PROVIDER")
    public interface FeignService {

    @RequestMapping(value = "/provider/queryByIds",method = RequestMethod.GET)
    @Headers({"Content-Type: application/json;charset=UTF-8"})
    String queryByIds(@RequestParam("price") Long[] ids);

    }
  4. 实现RequestInterceptor接口

    只要通过FeignClient访问的接口都会走这个拦截器,所以使用的时候需要注意一下

    1
    2
    3
    4
    5
    6
    7
    @Configuration
    public class FeignRequestInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate temp) {
    temp.header(HttpHeaders.AUTHORIZATION, "XXXXX");
    }
    }

2.6 手动创建Feign客户端

@FeignClient无法支持统一个service具有多种不同配置的FeignClient,因此,在必要时需要手动创建FeignClient

@FeignClient(value = “SLEUTH-PROVIDER”)

假如出现两个服务名称为SLEUTH-PROVIDER的FeignClient,项目会启动报错,有时候我们服务之间调用的地方较多,不可能将所有调用都放到一个FeignClient下,这时候就需要自定义来解决这个问题

注意:以下内容都在消费者服务里面添加

官网当中也明确提供了自定义FeignClient,以下是在官网基础上对自定义FeignClient的一个简单封装

首先创建FeignClientConfigure类,这个类相当于FeignClient的工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import feign.*;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.slf4j.Slf4jLogger;
import org.springframework.cloud.openfeign.FeignClientsConfiguration;
import org.springframework.context.annotation.Import;

/**
* @author xiaoyuge
*/
@Import(FeignClientsConfiguration.class)
public class FeignClientConfigure {
private Decoder decoder;
private Encoder encoder;
private Client client;
private Contract contract;

public FeignClientConfigure(Decoder decoder, Encoder encoder, Client client, Contract contract) {
this.decoder = decoder;
this.encoder = encoder;
this.client = client;
this.contract = contract;
}

public RequestInterceptor getUserFeignClientInterceptor() {
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate requestTemplate) {
// 添加header
}
};
}

public <T> T buildAuthorizedUserFeignClient(Class<T> clazz, String serviceName) {
return getBasicBuilder().requestInterceptor(getUserFeignClientInterceptor())
//默认是Logger.NoOpLogger
.logger(new Slf4jLogger(clazz))
//默认是Logger.Level.NONE(一旦手动创建FeignClient,全局配置的logger就不管用了,需要在这指定)
.logLevel(Logger.Level.FULL)
.target(clazz, buildServiceUrl(serviceName));
}

private String buildServiceUrl(String serviceName) {
return "http://" + serviceName;
}

protected Feign.Builder getBasicBuilder() {
return Feign.builder().client(client).encoder(encoder).decoder(decoder).contract(contract);
}
}

使用工具类的方法创建多个FeignClient配置

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class FeignClientConfiguration extends FeignClientConfigure {


public FeignClientConfiguration(Decoder decoder, Encoder encoder, @Qualifier("feignClient") Client client, Contract contract) {
super(decoder, encoder, client, contract);
}

@Bean
public FeignService feignService() {
return super.buildAuthorizedUserFeignClient(FeignService.class, "SLEUTH-PROVIDER");
}
}

其中,super.buildAuthorizedUserFeignClient()方法中,第一个参数为调用服务的接口类,第二个参数为被调用服务在注册中心的service-id。

1
2
3
4
5
6
//@FeignClient(value = "SLEUTH-PROVIDER") 这里就不需要@FeignClient注解了,通过配置去获取Service-id
public interface FeignService {

@PostMapping(value = "/provider/create", consumes = "application/json")
String create(@RequestBody double price);
}

使用的时候正常注入使用即可

1
2
@Resource
private FeignService feignService;

2.7 Feign继承支持

Feign通过单继承接口支持样板API,这允许将常用操作分组到方便的基本接口中

  • UserService
    1
    2
    3
    4
    public interface UserService {
    @RequestMapping(method = RequestMethod.GET, value ="/users/{id}")
    User getUser(@PathVariable("id") long id);
    }
  • UserResource
    1
    2
    3
    @RestController
    public class UserResource implements UserService {
    }
  • UserClient
    1
    2
    3
    4
    @FeignClient("users")
    public interface UserClient extends UserService {

    }

2.8 Feign与Cache集成

如果使用@EnableCaching注解,CachingCapability则创建并注册一个 bean,以便您的 Feign 客户端识别@Cache*其接口上的注解:

1
2
3
4
5
public interface DemoClient {
@GetMapping("/demo/{filterParam}")
@Cacheable(cacheNames = "demo-cache", key = "#keyParam")
String demoEndpoint(String keyParam, @PathVariable String filterParam);
}

您还可以通过 property 禁用该功能feign.cache.enabled=false

注意feign.cache.enabled=false只有在高版本才有

2.9 OAuth2支持

可以通过设置以下标志来启用OAuth2支持:

1
feign.oauth2.enabled=true

当标志为true并且存在Oauth2客户端上下文资源详细信息时,OAuth2FeignRequestInterceptor将创建一个Bean.在每个请求之前,拦截器解析所需要的访问令牌并将其作为表头包含在内。

有时,当为Feign客户端启用负载均衡时,如果也希望使用负载均衡来获取访问令牌。为此,需要确保负载均衡器位于类路径(spring-cloud-starter-loadbalancer))上,并通过设置以下标志显式启用OAuth2FeignRequestInterceptor 的负载均衡:

1
feign.oauth2.load-balanced=true

注意feign.cache.enabled=false只有在高版本才有

版权声明:本文为CSDN博主「怪 咖@」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_43888891/article/details/126171740