Bootstrap

OpenFeign 入门与实战

一、什么是 OpenFeign 

OpenFeign 的全称为 Spring Cloud OpenFeign,是Spring Cloud 团队开发的一款基于Feign 的框架,声明式 Web 服务客户端

Feign 是 Netflix 开源的一个声明式的 Web 服务客户端,它简化了基于HTTP 的服务调用,使得服务间通讯变得更加简单灵活。Feign 通过定义接口、注解和动态代理等方式,将服务调用的过程封装起来,开发者只需要定义服务接口,而无需关心底层的 HTTP 请求和序列化等细节。

OpenFeign vs Feign 的功能升级

  1.  更好的集成 Spring Cloud 组件:OpenFeign 与 Spring Cloud 其他组件(如服务发现,负载均衡等)紧密集成,可以无缝地与其他Spring Cloud 组件一起使用。
  2. 支持 @FeignClient 注解:OpenFeign 引入了 @FeignClient 注解作为 Feign 客户端的标识,可以方便地定义和使用远程服务地声明式接口。
  3. 错误处理改进:OpenFeign 对异常的处理做了增强,提供了更好的错误信息和错误异常处理机制,使得开发者可以更方便地进行错误处理。例如:OpenFeign 提供返回错误响应或请求失败时,OpenFeign 会调用回退策略中的逻辑,提供一个默认的处理结果。
  4. 更丰富的配置项:OpenFeign 提供了丰富的配置选项,可以对 Feign 客户端的行为进行灵活的配置,例如超时设置、重试策略等。

二、OpenFeign 基础使用

OpenFeign 通常要配合注册中心一起使用,并且新版本 OpenFeign 也必须和负载均衡器一起使用,所以它的基础使用步骤如下:

  1. 添加依赖(Nacos 注册中心、OpenFeign、Spring Cloud LoadBalancer)
  2. 配置 Nacos 服务端信息
  3. 在项目中开启 OpenFeign
  4. 编写 OpenFeign 调用代码
  5. 编写代码通过 OpenFeign 调用生产者

具体参考 Nacos-注册中心-CSDN博客

三、超时重传机制

在微服务框架中,服务之间是通过网络进行通信的,而网络是非常复杂和不稳定的,所以在调用服务时可能会失败或超时,那么在这种情况下,我们就需要个 OpenFeign 配置超时重试机制了。

什么时超时重试?

超时重试就是一种在网络通信中常用的策略,用于处理请求在一定时间内未能得到响应或得到超时响应的情况。当发起请求后,如果在规定的时间内没有得到预期的响应,就会触发超时重试机制,重新发送请求。

超时重试的目的主要是提高请求的可靠性和稳定性,以应对网络不稳定、服务不可用、响应延迟等不确定因素。

3.1 配置超时重试 

spring:
  cloud:    
    openfeign:
      client:
        config:
          default:            #全局配置
            connect-timeout: 1000 #连接超时时间
            read-timeout: 1000    #读取超时时间

 3.2 覆盖 Retryer

@Configuration
public class MyRetryer{
    @Bean
    public Retryer retryer(){
        return new Retryer.Default(
                2000,    //重试间隔时间
                2000,    //最大重试时间
                3        //最大重试次数);
    }
}

四、自定义超时重传机制

自定义重试类

public class MyRetryer implements Retryer {
    private final int maxAttempts;
    private final long period;
    int attempt;

    public MyRetryer(){
        this.maxAttempts=3;
        this.period=1;
        this.attempt=0;
    }

    public MyRetryer(int maxAttempts,long period){
        this.maxAttempts=maxAttempts;
        this.period=period;
        this.attempt=0;
    }



    @Override
    public void continueOrPropagate(RetryableException e) {
        if(attempt++>=maxAttempts){
            throw e;
        }
        long interval=period;
        try {
            //重试间隔时间
            Thread.sleep((interval*(attempt)));
        } catch (InterruptedException ex) {
            throw new RuntimeException(ex);
        }
    }

    @Override
    public Retryer clone() {
        return new MyRetryer(maxAttempts,period);
    }
}

设置配置文件

spring:
  application:
    name: nacos-consumer-demo
  cloud:
    nacos:
      discovery:
        username: nacos
        password: nacos
        server-addr: localhost:8848
        register-enabled: true
    openfeign:
      client:
        config:
          default:            #全局配置
            connect-timeout: 1000 #连接超时时间
            read-timeout: 1000    #读取超时时间
            retryer: com.example.consumer.config.MyRetryer #自定义重试类

server:
  port: 58080

五、超时重试底层实现

5.1 超时重试底层原理

OpenFeign 超时的底层实现是通过配置底层的HTTP 客户端来实现的。OpenFeign 允许你在请求连接和读取数据阶段设置超时时间,具体的超时时间可以通过设置HTTP 客户端的连接超时和读取超时来实现,可以在配置文件中设置超时参数。OpenFeign 底层的HTTP 客户端,可以使用 Apache HttpClient 或 OkHttpClient 来实现,默认使用的是 ApacheClient 实现的。

5.2 重试代码底层实现

通过观察 OpenFeign 的源码实现可以了解重试功能的底层实现,它的源码在 SynchronousMethodHandler 的 invoke 方法下,如下所示

public Object invoke(Object[] argv) throws Throwable {
     RequestTemplate template = this.buildTemplateFromArgs.create(argv);
     Request.Options options = this.findOptions(argv);
     Retryer retryer = this.retryer.clone();
     // 死循环,如果成功或者重试结束就返回【通过 throw 终止 weile 循环】
     while(true) {
        try {   
             //通过 HTTP Client 发起通信
             return this.executeAndDecode(template, options);
        } catch (RetryableException var9) {
             RetryableException e = var9;
             try {
                // 判断是否重试
                    retryer.continueOrPropagate(e);
                } catch (RetryableException var8) {
                    Throwable cause = var8.getCause();
                    if (this.propagationPolicy == ExceptionPropagationPolicy.UNWRAP && cause != null) {
                        throw cause;
                    }
                    throw var8;
                }
                if (this.logLevel != Level.NONE) {
                    this.logger.logRetry(this.metadata.configKey(), this.logLevel);
                }
            }
        }
    }

retryer 中 continueOrPropagate 的实现源码如下:

public interface Retryer extends Cloneable {
    Retryer NEVER_RETRY = new Retryer() {
        public void continueOrPropagate(RetryableException e) {
            throw e;
        }

        public Retryer clone() {
            return this;
        }
    };

    void continueOrPropagate(RetryableException var1);

    Retryer clone();

    public static class Default implements Retryer {
        private final int maxAttempts;
        private final long period;
        private final long maxPeriod;
        int attempt;
        long sleptForMillis;

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

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

        protected long currentTimeMillis() {
            return System.currentTimeMillis();
        }

        public void continueOrPropagate(RetryableException e) {
            if (this.attempt++ >= this.maxAttempts) {
                throw e;
            } else {
                long interval;
                if (e.retryAfter() != null) {
                    interval = e.retryAfter().getTime() - this.currentTimeMillis();
                    if (interval > this.maxPeriod) {
                        interval = this.maxPeriod;
                    }

                    if (interval < 0L) {
                        return;
                    }
                } else {
                    interval = this.nextMaxInterval();
                }

                try {
                    Thread.sleep(interval);
                } catch (InterruptedException var5) {
                    Thread.currentThread().interrupt();
                    throw e;
                }

                this.sleptForMillis += interval;
            }
        }

        long nextMaxInterval() {
            long interval = (long)((double)this.period * Math.pow(1.5, (double)(this.attempt - 1)));
            return interval > this.maxPeriod ? this.maxPeriod : interval;
        }

        public Retryer clone() {
            return new Default(this.period, this.maxPeriod, this.maxAttempts);
        }
    }
}

 

;