06.OpenFeign接口调用

1.提问

1.1 已经有RestTemplate + LoadBalancer的方式进行服务的调用,为什么还要有OpenFeign?

因为OpenFeign的功能更强大,和使用更便携。

1.2 使用那个?

推荐使用OpenFeign

2.OpenFeign是什么

2.1 官网翻译

https://docs.spring.io/spring-cloud-openfeign/reference/spring-cloud-openfeign.html


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客户端。

JAX-RS注解:JAX-RS(Java API for RESTful Web Services)是一套用于构建RESTful Web服务的标准Java API。JAX-RS提供了一组注解来简化HTTP请求处理和资源的定义。这些注解可以帮助开发者轻松地创建RESTful服务。

@Path,@GET,@POST,@PUT,@DELETE,@HEAD,@OPTIONS,@PATCH等。-- 来源,通义千问2.5。

2.2 GitHub

https://github.com/spring-cloud/spring-cloud-openfeign

2.3 总结

OpenFeign是一个声明式的Web服务客户端,只需要创建一个Rest接口,并在改接口上添加注解@FeignClient即可使用。

OpenFeign基本上就是当前微服务之间的调用的事实标准。

3.OpenFeign作用

前面在使用SpringCloud LoadBalancer+RestTemplate时,利用RestTemplate对http请求的封装处理形成了一套模版化的调用方法。但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以,OpenFeign在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。在OpenFeign的实现下,我们只需创建一个接口并使用注解的方式来配置它(在一个微服务接口上面标注一个@FeignClient注解即可),即可完成对服务提供方的接口绑定,统一对外暴露可以被调用的接口方法,大大简化和降低了调用客户端的开发量,也即由服务提供者给出调用接口清单,消费者直接通过OpenFeign调用即可,O(∩_∩)O。

OpenFeign同时还集成SpringCloud LoadBalancer


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

总结

  • 可插拔的注解支持,包括Feign注解和JAX-RS注解;
  • 可插拔的HTTP编码器和解码器;
  • 支持Sentinel和Fallback;
  • 支持SpringCloudLoadBalancer的复杂均衡;
  • 支持HTTP请求和响应的压缩。

4.使用

4.1 接口 + 注解

微服务API接口 + @FeignClient注解

架构说明

4.2 使用步骤

4.2.1 新建Module

略,新建一个使用OpenFeign的模块。

4.2.2 引入依赖

这是新增的依赖,其他的依赖,还需引入。

<!--openfeign-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
4.2.3 yml(基础)

4.2.4 启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableDiscoveryClient //该注解用于向使用consul为注册中心时注册服务
@EnableFeignClients//启用feign客户端,定义服务+绑定接口,以声明式的方法优雅而简单的实现服务调用
public class MainOpenFeign80 {

    public static void main(String[] args) {
        SpringApplication.run(MainOpenFeign80.class, args);
    }

}
4.2.5 业务类

订单模块要去调用支付模块,订单和支付两个微服务,需要通过Api接口解耦,一般不要在订单模块写非订单相关的业务,

自己的业务自己做+其它模块走FeignApi接口调用。

将Feign接口,写在公共的依赖模块中,因为其他模块,可以也会需要远程调用支付模块。

commons模块修改

引入OpenFeign依赖:略

新建Feign接口:

@FeignClient(value = "cloud-payment-service")
public interface PayFeignApi {
    /**
     * 新增一条支付相关流水记录
     * @param payDTO
     * @return
     */
    @PostMapping("/pay/add")
    ResultData<String> addPay(@RequestBody PayDTO payDTO);

    /**
     * 按照主键记录查询支付流水信息
     * @param id
     * @return
     */
    @GetMapping("/pay/get/{id}")
    ResultData getById(@PathVariable("id") Integer id);//注解中的id必须写,

    /**
     * 负载均衡演示
     * @return
     */
    @GetMapping("/pay/get/info")
    String mylb();
}

@PathVariable注解的使用方式进行了调整,要求显式地指定路径变量的名称,以确保代码的可读性和维护性。在之前的版本中,如果路径变量名与方法参数名一致,可以省略name属性。但在SpringBoot 3.0及以后的版本中,为了遵循更好的编程实践和避免潜在的混淆,要求必须明确指定路径变量的名称。

修改Controller层的调用:略,将RestTemplate方式,改成Feign远程调用。

4.3 测试

5.OpenFiegn高级特性

5.1 超时控制

5.1.1 测试


在cloud-provider-payment8001模块,故意写暂停62s程序,order模块,写捕捉异常,进行默认超时时间测试。

//8001模块
@GetMapping("/get/{id}")
@Operation(summary = "根据id查流水", description = "查询支付流水方法")
public ResultData<Pay> getById(@PathVariable("id") Integer id) {
    if (id <= 0) {
        throw new RuntimeException("id必须为正数");
    }

    try {
        TimeUnit.SECONDS.sleep(62);
    } catch (InterruptedException e) {
        logger.error(e.getMessage());
    }

    return ResultData.success(payService.getById(id));
}
//使用openFeign的order模块
@GetMapping("/feign/get/{id}")
public ResultData getById(@PathVariable("id")Integer id){
    logger.info("-------支付微服务远程调用,按照id查询订单支付流水信息");
    ResultData resultData = null;
    try {
        logger.info("调用开始------: " + DateUtil.now());
        resultData = payFeignApi.getById(id);
    } catch (Exception e){
        e.printStackTrace();
        logger.info("调用结束------: " + DateUtil.now());
        ResultData.fail(ReturnCodeEnum.RC500.getCode(), e.getMessage());
    }
    return resultData;
}

结论:

OpenFeign默认等待60s,超时后报错

feign.RetryableException: Read timed out executing GET http://cloud-payment-service/pay/get/1
Caused by: java.net.SocketTimeoutException: Read timed out
	at java.base/sun.nio.ch.NioSocketImpl.timedRead(NioSocketImpl.java:283)
	at java.base/sun.nio.ch.NioSocketImpl.implRead(NioSocketImpl.java:309)
	at java.base/sun.nio.ch.NioSocketImpl.read(NioSocketImpl.java:350)
	at java.base/sun.nio.ch.NioSocketImpl$1.read(NioSocketImpl.java:803)
	at java.base/java.net.Socket$SocketInputStream.read(Socket.java:966)
	at java.base/java.io.BufferedInputStream.fill(BufferedInputStream.java:244)
	at java.base/java.io.BufferedInputStream.read1(BufferedInputStream.java:284)
	at java.base/java.io.BufferedInputStream.read(BufferedInputStream.java:343)
	at java.base/sun.net.www.http.HttpClient.parseHTTPHeader(HttpClient.java:791)
	at java.base/sun.net.www.http.HttpClient.parseHTTP(HttpClient.java:726)
	at java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1688)
	at java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1589)
	at java.base/java.net.HttpURLConnection.getResponseCode(HttpURLConnection.java:529) //java.net.HttpURLConnection使用JDK默认的进行http请求。
5.1.2 配置处理


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

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

yml文件中开启配置:

connectTimeout 连接超时时间

readTimeout 请求处理超时时间

5.1.3 自定义超时配置

官网出处


关键内容

spring:
    openfeign:
      client:
        config:
          #全局配置
          default:
            #连接超时时间
            connectTimeout: 3000
            #读取超时时间
            readTimeout: 3000
          #单个微服务的配置,细粒度的重写全局
          cloud-payment-service:
            #连接超时时间
            connect-timeout: 4000
            #读取超时时间
            read-timeout: 4000
5.1.4 测试

5.2 重试机制

5.2.1 默认

重试机制默认是关闭的。一次失败后,就结束。

5.2.2 开启Retryer功能
@Configuration
public class FeignConfig {

    @Bean
    public Retryer retryer(){

        //return Retryer.NEVER_RETRY;
        //maxAttempts - 1才是重试次数
        return new Retryer.Default(100,1,3);最大请求次数(1 + 2),初始间隔时间100ms,重试间最大间隔时间1s
    }

}
public interface Retryer extends Cloneable {
    ...
    class Default implements Retryer {

        private final int maxAttempts;
        private final long period;
        private final long maxPeriod;
        int attempt;
        long sleptForMillis;

        public Default() {
          this(100, SECONDS.toMillis(1), 5);
        }

        public Default(long period, long maxPeriod, int maxAttempts) {
          this.period = period;
          this.maxPeriod = maxPeriod;
          this.maxAttempts = maxAttempts;
          this.attempt = 1;
        }
        ...

    }
    ...
}
5.2.3 测试

5.3 默认HttpClient的修改

OpenFeign中http client(发起HTTP请求的组件)

如果不做特殊配置,OpenFeign默认使用JDK自带的HttpURLConnection发送HTTP请求,由于默认HttpURLConnection没有连接池、性能和效率比较低,如果采用默认,性能上不是最好的,所以加到最大。

5.3.1 使用Apache HttpClient5替换默认

5.3.2 引入依赖
<!-- 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>
5.3.3 配置项

新增配置项

#  Apache HttpClient5 配置开启
spring:
  cloud:
    openfeign:
      httpclient:
        hc5:
          enabled: true

完整配置项

server:
  port: 8080

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:
      httpclient:
        hc5:
          enabled: true
      client:
        config:
          #全局配置
          default:
            #连接超时时间
            connectTimeout: 3000
            #读取超时时间
            readTimeout: 3000
          #单个微服务的配置,细粒度的重写全局
          cloud-payment-service:
            #连接超时时间
            connect-timeout: 4000
            #读取超时时间
            read-timeout: 4000
5.3.4 替换前后对比
//替换前
Caused by: java.net.SocketTimeoutException: Read timed out
	at java.base/sun.nio.ch.NioSocketImpl.timedRead(NioSocketImpl.java:283)
	at java.base/sun.nio.ch.NioSocketImpl.implRead(NioSocketImpl.java:309)
	at java.base/sun.nio.ch.NioSocketImpl.read(NioSocketImpl.java:350)
	at java.base/sun.nio.ch.NioSocketImpl$1.read(NioSocketImpl.java:803)
	at java.base/java.net.Socket$SocketInputStream.read(Socket.java:966)
	at java.base/java.io.BufferedInputStream.fill(BufferedInputStream.java:244)
	at java.base/java.io.BufferedInputStream.read1(BufferedInputStream.java:284)
	at java.base/java.io.BufferedInputStream.read(BufferedInputStream.java:343)
	at java.base/sun.net.www.http.HttpClient.parseHTTPHeader(HttpClient.java:791)
	at java.base/sun.net.www.http.HttpClient.parseHTTP(HttpClient.java:726)
	at java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1688)
	at java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1589)
	at java.base/java.net.HttpURLConnection.getResponseCode(HttpURLConnection.java:529) //java.net.HttpURLConnection使用JDK默认的进行http请求。
//替换后
Caused by: java.net.SocketTimeoutException: Read timed out
	at java.base/sun.nio.ch.NioSocketImpl.timedRead(NioSocketImpl.java:283)
	at java.base/sun.nio.ch.NioSocketImpl.implRead(NioSocketImpl.java:309)
	at java.base/sun.nio.ch.NioSocketImpl.read(NioSocketImpl.java:350)
	at java.base/sun.nio.ch.NioSocketImpl$1.read(NioSocketImpl.java:803)
	at java.base/java.net.Socket$SocketInputStream.read(Socket.java:966)
	at org.apache.hc.core5.http.impl.io.SessionInputBufferImpl.fillBuffer(SessionInputBufferImpl.java:149)
	at org.apache.hc.core5.http.impl.io.SessionInputBufferImpl.readLine(SessionInputBufferImpl.java:280)
	at org.apache.hc.core5.http.impl.io.AbstractMessageParser.parse(AbstractMessageParser.java:247)
	at org.apache.hc.core5.http.impl.io.AbstractMessageParser.parse(AbstractMessageParser.java:54)
	at org.apache.hc.core5.http.impl.io.DefaultBHttpClientConnection.receiveResponseHeader(DefaultBHttpClientConnection.java:299)
	at org.apache.hc.core5.http.impl.io.HttpRequestExecutor.execute(HttpRequestExecutor.java:175)
	at org.apache.hc.core5.http.impl.io.HttpRequestExecutor.execute(HttpRequestExecutor.java:218)
	at org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager$InternalConnectionEndpoint.execute(PoolingHttpClientConnectionManager.java:712)
	at org.apache.hc.client5.http.impl.classic.InternalExecRuntime.execute(InternalExecRuntime.java:216)
	at org.apache.hc.client5.http.impl.classic.MainClientExec.execute(MainClientExec.java:116)
	at org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51)
	at org.apache.hc.client5.http.impl.classic.ConnectExec.execute(ConnectExec.java:188)
	at org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51)
	at org.apache.hc.client5.http.impl.classic.ProtocolExec.execute(ProtocolExec.java:192)
	at org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51)
	at org.apache.hc.client5.http.impl.classic.HttpRequestRetryExec.execute(HttpRequestRetryExec.java:113)
	at org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51)
	at org.apache.hc.client5.http.impl.classic.ContentCompressionExec.execute(ContentCompressionExec.java:152)
	at org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51)
	at org.apache.hc.client5.http.impl.classic.RedirectExec.execute(RedirectExec.java:116)
	at org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51)
	at org.apache.hc.client5.http.impl.classic.InternalHttpClient.doExecute(InternalHttpClient.java:170)
	at org.apache.hc.client5.http.impl.classic.CloseableHttpClient.execute(CloseableHttpClient.java:87)
	at org.apache.hc.client5.http.impl.classic.CloseableHttpClient.execute(CloseableHttpClient.java:55)
	at feign.hc5.ApacheHttp5Client.execute(ApacheHttp5Client.java:88)

5.4 请求/响应压缩

5.4.1 官网说明

5.4.2 是什么

对请求和响应进行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 #最小触发压缩的大小

5.4.3 YML修改
server:
  port: 8080

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:
      httpclient:
        hc5:
          enabled: true
      #新增
      compression: 
        request:
          enabled: true
          min-request-size: 2048 #最小触发压缩的大小
          mime-types: text/xml,application/xml,application/json #触发压缩数据类型
        response:
          enabled: true
      client:
        config:
          #全局配置
          default:
            #连接超时时间
            connectTimeout: 3000
            #读取超时时间
            readTimeout: 3000
          #单个微服务的配置,细粒度的重写全局
          cloud-payment-service:
            #连接超时时间
            connect-timeout: 4000
            #读取超时时间
            read-timeout: 4000

5.5 日志打印功能

5.5.1 是什么

Feign 提供了日志打印功能,我们可以通过配置来调整日志级别,

从而了解 Feign 中 Http 请求的细节,

说白了就是对Feign接口的调用情况进行监控和输出

5.5.2 日志级别
public enum Level {
/**
 * No logging.
 */
NONE,
/**
 * Log only the request method and URL and the response status code and execution time.
 */
BASIC,
/**
 * Log the basic information along with request and response headers.
 */
HEADERS,
/**
 * Log the headers, body, and metadata for both requests and responses.
 */
FULL
}
  • NONE:默认的,不显示任何日志;
  • BASIC:仅记录请求方法、URL、响应状态码及执行时间;
  • HEADERS:除了 BASIC 中定义的信息之外,还有请求和响应的头信息;
  • FULL:除了 HEADERS 中定义的信息之外,还有请求和响应的正文及元数据。
5.5.3 配置
@Bean
public Logger.Level logger(){
    return Logger.Level.FULL;
}
logging:
  level:
    com.atguigu.cloud.apis.PayFeignApi: debug

5.5.4 测试输出日志




6.OpenFeign和Sentinel集成实现fallback服务降级

后续Alibaba课程会讲到

只是为了记录自己的学习历程,且本人水平有限,不对之处,请指正。