OpenFeign服务接口调用

1、OpenFeign简介

Feign是一个声明性web服务客户端。它使编写web服务客户端变得更容易。使用Feign创建一个接口并对其进行注释。它具有可插入的注释支持,包括Feign注释和JAX-RS注释。Feign还支持可插拔编码器和解码器。Spring Cloud添加了对Spring MVC注释的支持,以及对使用Spring Web中默认使用的HttpMessageConverter的支持。Spring Cloud集成了Eureka、Spring Cloud CircuitBreaker以及Spring Cloud LoadBalancer,以便在使用Feign时提供负载平衡的http客户端。

官网:Spring Cloud OpenFeign

一句话:openfeign是一个声明式的Web服务客户端,我们只需要创建一个Rest接口并在该接口上添加注解@FeignClint即可使用,OpenFeign基本上就是当前微服务之间调用的事实标准。

官网演示了一个案例

  • 先在springboot应用中开启FeignClients

    @SpringBootApplication
    @EnableFeignClients
    public class Application {
    ​
        public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
        }
    ​
    }
  • 然后写个接口StoreClient打上@FeignClint注解

    @FeignClient("stores")
    public interface StoreClient {
        @RequestMapping(method = RequestMethod.GET, value = "/stores")
        List<Store> getStores();
    ​
        @RequestMapping(method = RequestMethod.GET, value = "/stores")
        Page<Store> getStores(Pageable pageable);
    ​
        @RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json")
        Store update(@PathVariable("storeId") Long storeId, Store store);
    ​
        @RequestMapping(method = RequestMethod.DELETE, value = "/stores/{storeId:\\d+}")
        void delete(@PathVariable Long storeId);
    }

2、OpenFeign能干什么?

前面在使用SpringCloud LoadBalancer+RestTemplate时,利用RestTemplate对http请求的封装处理形成了一套模版化的调用方法。

但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。

所以,OpenFeign在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。 在OpenFeign的实现下,我们只需创建一个接口并使用注解的方式来配置它(在一个微服务接口上面标注一个@FeignClient注解即可),即可完成对服务提供方的接口绑定,统一对外暴露可以被调用的接口方法,大大简化和降低了调用客户端的开发量,也即由服务提供者给出调用接口清单,消费者直接通过OpenFeign调用即可。

OpenFeign同时还集成SpringCloud LoadBalancer

可以在使用OpenFeign时提供Http客户端的负载均衡,也可以集成阿里巴巴Sentinel来提供熔断、降级等功能。而与SpringCloud LoadBalancer不同的是,通过OpenFeign只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用。

OpenFeign主要能干的事

  • 可插拔的注解支持,包括Feign注解和JAX-RS注解;

  • 支持可插拔的HTTP编码器和解码器;

  • 支持Sentinel和它的Fallback;

  • 支持SpringCloudLoadBalancer的负载均衡;

  • 支持HTTP请求和响应的压缩;

我们目前是80消费者端的服务去调8001支付服务模块和8002订单服务模块,在80消费者端我们使用的是RestTemplate实现调用,但是,微服务之间也需要相互调用呢?8002调用8001呢?难不成每一个微服务想调用其他服务时再写一次RestTemplate?8001作为一个支付服务含有很多支付流水信息,会有很多其他微服务会调用它,根据解耦和面向接口的原则,我们最好在8001上写出对外暴露的接口,其他服务想调用它,就要找8001定义的接口。

3、使用

架构说明图:

image-20240313105409039

3.1、新建module

新建module,cloud-consumer-feign-order80,导入依赖。

pom.xml

 <dependencies>
        <!--openfeign新加的坐标-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--SpringCloud consul discovery-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>
        <!-- 引入自己定义的api通用包 -->
        <dependency>
            <groupId>com.zm.cloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!--web + actuator-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--hutool-all-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>
        <!--fastjson2-->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
        </dependency>
        <!-- swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
        </dependency>
    </dependencies>
​
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

写YML

server:
  port: 80
spring:
    application:
      name: cloud-consumer-openfeign-order
    ####Spring Cloud Consul for Service Discovery
    cloud:
      consul:
        host: localhost
        port: 8500
        discovery:
          prefer-ip-address: true #优先使用服务ip进行注册
          service-name: ${spring.application.name} 

主启动类

@SpringBootApplication
@EnableDiscoveryClient //使用consul为注册中心时注册服务
@EnableFeignClients  //开启OpenFeign功能并激活
public class MainOpenFeign80 {
    public static void main(String[] args) {
        SpringApplication.run(MainOpenFeign80.class,args);
    }
}

要把接口PayFeignApi创建在通用的api模块cloud-api-commons中

修改cloud-api-commons模块,pom文件中添加openfeign的依赖

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

参考微服务8001的Controller层,新建PayFeignApi接口

@FeignClient(value = "cloud-payment-service")
public interface PayFeignApi {
    /**
     * 新增一条支付相关流水记录
     * @param payDTO
     * @return
     */
    @PostMapping("/pay/add")
    public ResultData addPay(@RequestBody PayDTO payDTO);
    //通过id查询
    @GetMapping(value = "/pay/get/{id}")
    public ResultData getPayById(@PathVariable("id") Integer id);
    //删除
    @DeleteMapping("/pay/del/{id}")
    public ResultData delById(@PathVariable("id") Integer integer);
    //修改
    @PutMapping("/pay/update")
    public ResultData update(@RequestBody PayDTO payDTO);
    //查全部
    @GetMapping("/pay/getall")
    public ResultData getAll();
​
    //openfeign天然支持负载均衡演示
    @GetMapping("/pay/getInfo")
    public String myLB();
​
}

我们把原来微服务80的controller复制一份到cloud-consumer-feign-order80中,原来的内容全部删除,重新使用PayFeignApi实现。当应用启动时,Feign 使用 Java 的动态代理机制生成接口的实现。这个过程由 Spring Cloud 集成提供支持Feign 客户端在内部构建了请求的详细信息,并将接口方法调用转换为 HTTP 调用。

@RestController
public class OrderController {
   @Resource
   private PayFeignApi payFeignApi;
​
   @PostMapping("/feign/pay/add")
   public ResultData addPay(@RequestBody PayDTO payDTO){
      System.out.println("1、使用本地addOrder新增订单功能(省略sql操作),2、开启addPay支付微服务远程调用");
      ResultData resultData = payFeignApi.addPay(payDTO);
      return resultData;
   }
   @GetMapping("/feign/pay/get/{id}")
   public ResultData getByID(@PathVariable("id") Integer id){
      System.out.println("-------支付微服务远程调用,按照id查询订单支付流水信息");
      ResultData payById = payFeignApi.getPayById(id);
      return payById;
   }
   @DeleteMapping("/feign/pay/del/{id}")
   public ResultData delById(@PathVariable("id") Integer id){
      System.out.println("-------支付微服务远程调用,按照id查删除订单支付流水信息");
      return payFeignApi.delById(id);
   }
   @PutMapping("/feign/pay/update")
   public ResultData update(@RequestBody PayDTO payDTO){
      System.out.println("-------支付微服务远程调用,修改订单支付流水信息");
      return payFeignApi.update(payDTO);
   }
   @GetMapping("/feign/pay/getall")
   public ResultData getAll(){
      return payFeignApi.getAll();
   }
   @GetMapping("/feign/pay/getInfo")
   public String mylb(){
      return payFeignApi.myLB();
   }
​
}

测试启动新的80服务,启动微服务8001和8002.

image-20240313140818314

使用浏览器或者其他测试工具先查询一个看看

image-20240313141906611

添加一个数据

image-20240313142141138

查询全部

image-20240313142211054

删除刚才新增的

image-20240313142303242

下面测试负载均衡

image-20240313142738116

因为OpenFeign默认的集成了LoadBalancer,所以就会负载均衡。

梳理一遍

image-20240313144743890

在使用Feign进行微服务间的通信时,当发出一个请求到PayFeignApi接口时,该请求实际上是通过Feign客户端进行代理的。Feign会根据你提供的@FeignClient(value = "cloud-payment-service")注解中的value值(即服务名)来定位目标服务。

当请求到达PayFeignApi接口时大致流程:

  1. 路由到目标服务:Feign会根据我们在注解上填的微服务名称去注册中心(consul)找有没有这个微服务,然后找到这个微服务下的具体示例列表,因为Feign本身就有负载均衡能力,默认还是轮询的方式进行调度。

  2. 选择具体的端点:选择完目标示例之后,Feign会根据你定义的路径(/pay/add)来拼凑完整的URL,比如说选择了8001,那么URL就是:http://localhost:8001/pay/add

  3. 发送请求:URL拿到后,Feign就使用HTTP客户端发送请求到服务提供者,然后就是具体的处理请求了,至此整个服务流程完成。

4、OpenFeign高级特性

4.1、OpenFeign超时控制

在Spring Cloud微服务架构中,大部分公司都是利用OpenFeign进行服务间的调用,而比较简单的业务使用默认配置是不会有多大问题的,但是如果是业务比较复杂,服务要进行比较繁杂的业务计算,那后台很有可能会出现Read Timeout这个异常,因此定制化配置超时时间就有必要了。

image-20240313151559540

4.1.1、设置超时演示

我们故意设置超时出错情况,让8001的服务睡一会儿。

服务提供方cloud-provider-payment8001故意写暂停62秒钟程序,把getById的controller修改一下,睡一会儿

//通过id查询
@GetMapping("/pay/get/{id}")
@Operation(summary = "查询",description = "通过ID查询")
public ResultData<Pay> payById(@PathVariable("id") Integer id){
    System.out.println("---------正在查询--------");
    try {
        TimeUnit.SECONDS.sleep(62);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    return ResultData.success(payServiceImp.getById(id));
}

为啥是62秒???试出超时时间。

服务调用方cloud-consumer-feign-order80写好捕捉超时异常

@GetMapping("/feign/pay/get/{id}")
public ResultData getByID(@PathVariable("id") Integer id){
   System.out.println("-------支付微服务远程调用,按照id查询订单支付流水信息");
   ResultData payById = null;
   try {
      System.out.println("调用开始时间------>"+ DateUtil.now());
      payById = payFeignApi.getPayById(id);
   }catch (Exception e){
      e.printStackTrace();
      System.out.println("调用结束时间------>"+ DateUtil.now());
      ResultData.fail(ReturnCodeEnum.RC500.getCode(), e.getMessage());
   }
   return payById;
}

开始测试,重新启动8001和80,浏览器输入localhost/feign/pay/get/1:

注意:如果测试的时候你的8002服务没有关闭的话,轮询还是存在的,可能第一次请求就是成功的,刷新一下就轮到8001了,你要不想轮询就把8002关闭。

image-20240313154153904

所以说,OpenFeign的默认超时时间是60秒,所以故意弄的睡眠时间就是60秒以上。

默认OpenFeign客户端等待60秒钟,但是服务端处理超过规定时间会导致Feign客户端返回报错。

为了避免这样的情况,有时候我们需要设置Feign客户端的超时控制,默认60秒太长或者业务时间太短都不好。

这就需要在yml中开始配置了:

connectTimeout       连接超时时间

readTimeout          请求处理超时时间

官网默认配置参数

image-20240313155357974

全局配置

在application.yml文件中配置

server:
  port: 80
spring:
    application:
      name: cloud-consumer-openfeign-order
    ####Spring Cloud Consul for Service Discovery
    cloud:
      consul:
        host: localhost
        port: 8500
        discovery:
          prefer-ip-address: true #优先使用服务ip进行注册
          service-name: ${spring.application.name}
      openfeign:
        client:
          config:
            default:
             #连接超时时间
             connectTimeout: 3000
             #读取超时时间
             readTimeout: 3000

重启80服务测试一下:

image-20240313185725554

有1秒的误差,打印信息了。

指定配置

还可以指定某个服务的超时时间,但是不能和default一起写,这样会覆盖掉default的时间,用你指定服务的超时时间。演示一下两者都写的情况。

server:
  port: 80
spring:
    application:
      name: cloud-consumer-openfeign-order
    ####Spring Cloud Consul for Service Discovery
    cloud:
      consul:
        host: localhost
        port: 8500
        discovery:
          prefer-ip-address: true #优先使用服务ip进行注册
          service-name: ${spring.application.name}
      openfeign:
        client:
          config:
            default:
             connectTimeout: 3000            #连接超时时间
             readTimeout: 3000             #读取超时时间
            cloud-payment-service:
             connectTimeout: 5000
             readTimeout: 5000

再次测试

image-20240313190714409

结果是5秒,所以自己指点的时间就会覆盖掉默认设置的时间。

4.2、OpenFeign重试机制

OpenFeign的重试机制默认是关闭的

image-20240313192605874

想开启重试就新增一个配置类FeignConfig并修改Retryer配置,把指定的8001超时时间设置为4秒

@Configuration
public class FeignConfig {
    @Bean
    public Retryer myRetryer(){
        //return Retryer.NEVER_RETRY; //Feign默认配置是不走重试策略的
        //最大请求次数为(1+2)次,初始时间间隔为100ms,重试最大间隔时间为1秒
        return new Retryer.Default(100,1,3);
    }
}

现在测试一下,如果走了重试应该就是5*3=15秒

image-20240313200641627

4.3、OpenFeign默认HttpClient修改

OpenFeign中http client,如果不做特殊配置,OpenFeign默认使用JDK自带HttpURLConnection发送HTTP请求,由于默认HttpURLConnection没有连接池、性能和效率比较低,如果采用默认,性能上不是最强大的,所以需要换掉推荐使用阿帕奇的HC5

我们看到官网

image-20240313202234136

我们先把超时重试的配置关闭。

@Configuration
public class FeignConfig {
    @Bean
    public Retryer myRetryer(){
        return Retryer.NEVER_RETRY; //Feign默认配置是不走重试策略的
        //最大请求次数为(1+2)次,初始时间间隔为100ms,重试最大间隔时间为1秒
        //return new Retryer.Default(100,1,3);
    }
}

修改POM文件,添加依赖

<!-- httpclient5-->
<dependency>
    <groupId>org.apache.httpcomponents.client5</groupId>
    <artifactId>httpclient5</artifactId>
    <version>5.3</version>
</dependency>
<!-- feign-hc5-->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-hc5</artifactId>
    <version>13.1</version>
</dependency>

在application.yml配置文件中打开Apache HttpClient5

server:
  port: 80
spring:
    application:
      name: cloud-consumer-openfeign-order
    ####Spring Cloud Consul for Service Discovery
    cloud:
      consul:
        host: localhost
        port: 8500
        discovery:
          prefer-ip-address: true #优先使用服务ip进行注册
          service-name: ${spring.application.name}
      openfeign:
        client:
          config:
#            default:
#             connectTimeout: 3000            #连接超时时间
#             readTimeout: 3000             #读取超时时间
            cloud-payment-service:
             connectTimeout: 5000
             readTimeout: 5000
        httpclient:
          hc5:
            enabled: true

重启再看一下现在报错信息来至于哪里

image-20240313203114165

4.4、OpenFeign请求/响应压缩

对请求和响应进行GZIP压缩

Spring Cloud OpenFeign支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗。

通过下面的两个参数设置,就能开启请求与相应的压缩功能:

spring.cloud.openfeign.compression.request.enabled=true

spring.cloud.openfeign.compression.response.enabled=true

细粒度化设置

对请求压缩做一些更细致的设置,比如下面的配置内容指定压缩的请求数据类型并设置了请求压缩的大小下限,只有超过这个大小的请求才会进行压缩:

spring.cloud.openfeign.compression.request.enabled=true

spring.cloud.openfeign.compression.request.mime-types=text/xml,application/xml,application/json #触发压缩数据类型

spring.cloud.openfeign.compression.request.min-request-size=2048 #最小触发压缩的大小

我们可以在配置文件中设置一下

server:
  port: 80
spring:
    application:
      name: cloud-consumer-openfeign-order
    ####Spring Cloud Consul for Service Discovery
    cloud:
      consul:
        host: localhost
        port: 8500
        discovery:
          prefer-ip-address: true #优先使用服务ip进行注册
          service-name: ${spring.application.name}
      openfeign:
        client:
          config:
#            default:
#             connectTimeout: 3000            #连接超时时间
#             readTimeout: 3000             #读取超时时间
            cloud-payment-service:
             connectTimeout: 5000
             readTimeout: 5000
        httpclient:
          hc5:
            enabled: true
        compression:
          request:
            enabled: true
            min-request-size: 2048 #最小触发压缩的大小
            mime-types: text/xml,application/xml,application/json #触发压缩数据类型
          response:
            enabled: true

压缩效果在日志打印功能中展现......

4.5、OpenFeign日志打印功能

Feign 提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解 Feign 中 Http 请求的细节,说白了就是对Feign接口的调用情况进行监控和输出。

日志级别

  • NONE:默认的,不显示任何日志;BASIC:仅记录请求方法、URL、响应状态码及执行时间;

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

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

官网说明:

image-20240313204326264

image-20240313204046193

我们在FeignConfig中可以配置日志,注意导包要导feign的。

@Configuration
public class FeignConfig {
    @Bean
    public Retryer myRetryer(){
        return Retryer.NEVER_RETRY; //Feign默认配置是不走重试策略的
        //最大请求次数为(1+2)次,初始时间间隔为100ms,重试最大间隔时间为1秒
        //return new Retryer.Default(100,1,3);
    }
    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

然后在配置文件中开启日志的Feign客户端。

这里有一个套路可以这样写:

公式(三段):logging.level + 含有@FeignClient注解的完整带包名的接口名+debug

完整的application.yml

server:
  port: 80
spring:
    application:
      name: cloud-consumer-openfeign-order
    ####Spring Cloud Consul for Service Discovery
    cloud:
      consul:
        host: localhost
        port: 8500
        discovery:
          prefer-ip-address: true #优先使用服务ip进行注册
          service-name: ${spring.application.name}
      openfeign:
        client:
          config:
#            default:
#             connectTimeout: 3000            #连接超时时间
#             readTimeout: 3000             #读取超时时间
            cloud-payment-service:
             connectTimeout: 5000
             readTimeout: 5000
        httpclient:
          hc5:
            enabled: true
        compression:
          request:
            enabled: true
            min-request-size: 2048 #最小触发压缩的大小
            mime-types: text/xml,application/xml,application/json #触发压缩数据类型
          response:
            enabled: true
            

# feign日志以什么级别监控哪个接口
logging:
  level:
    com:
      zm:
        cloud:
          apis:
            PayFeignApi: debug 

现在可以开始测试了,重启服务测试。

image-20240313205737543

我们可以看到开了压缩和没开压缩的还是有些差别的,没开请求响应压缩看不到任何的请求响应信息。

重试机制控制台看到3次过程

现在有了日志打印功能就可以看到具体的3次重试过程了。

先把重试功能打开,为了快速展示我们把超时时间改成1秒,那么一会儿总时间应该是3秒。

@Configuration
public class FeignConfig {
    @Bean
    public Retryer myRetryer(){
        //return Retryer.NEVER_RETRY; //Feign默认配置是不走重试策略的
        //最大请求次数为(1+2)次,初始时间间隔为100ms,重试最大间隔时间为1秒
        return new Retryer.Default(100,1,3);
    }
    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

重启服务开始测试。