Spring Cloud OpenFeign源码解析

一、OpenFeign源码环境搭建

 本文采用nacos进行整合测试,具体源码见gitee链接:OpenFeign源码环境-develop

1.1 搭建生产者-订单服务

1.1.1 pom

    <dependencies>
        <!--openfeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!-- SpringCloud ailibaba nacos-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

1.1.2 yaml

server:
  port: 3333 #启动端口
spring:
  application:
    name: provider
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

1.1.3 启动类

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class NacosProviderApplication {
	public static void main(String[] args)
	{
		SpringApplication.run(NacosProviderApplication.class, args);
	}
}

1.1.4 业务类

@RestController
@RequestMapping("/provider")
public class ProviderController {

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

	@GetMapping("/find/instance")
	public String service() {

		String applicationName = "provider";
		String result = applicationName + "-" +  port;

		System.out.println("**********" + result);
		
		return result;
	}

}

1.2 搭建消费者-用户服务

1.2.1 pom

    <dependencies>
        <!--openfeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!-- SpringCloud alibaba nacos-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

1.2.2 yaml

server:
  port: 5555 #启动端口
spring:
  application:
    name: consumer
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

1.2.3 OpenFeign接口

// name: 微服务的名称 path: 服务生成者的调用前缀
@FeignClient(name = "provider", path = "/provider")
public interface ProviderClient {

	@GetMapping("/find/instance")
	String service();

}

1.2.4 启动类

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class NacosConsumerApplication {
	public static void main(String[] args) {
		SpringApplication.run(NacosConsumerApplication.class, args);
	}
}

1.2.5 业务类

@RestController
@RequestMapping("/consumer")
public class ConsumerController {

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

	// 动态代理对象,内部远程调用服务生产者
	@Autowired
	ProviderClient providerClient;

	@GetMapping("/find/instance")
	public String service() {

		// 远程调用
		String providerResult = providerClient.service();
		String result = "consumer invoke: " + port + " | " + providerResult;

		System.out.println("****************" + result);

		return result;
	}
}

1.3 启动测试

1.3.1 启动Nacos

startup.cmd -m standalone 

 1.3.2 启动生产者

 1.3.3 启动消费者

二、OpenFeign的应用

2.1 设置Feign的日志级别

官方文档:

https:/Icloud.spring.iolspring-cloud-static/Greenwich.SR2/single/spring-cloud.html

日志级别:

1.NONE【性能最佳,适用于生产】︰不记录任何日志(默认值)。

2.BASIC【适用于生产环境追踪问题】︰仅记录请求方法、URL、响应状态代码以及执行时间。

3.HEADERS:记录BASIC级别的基础上,记录请求和响应的header。

4.FULL【比较适用于开发及测试环境定位问题】︰记录请求和响应的header、body和元数据。

2.1.1  application.yaml配置

# 设置Feign接口的日志级别 (前提)
logging:
  level:
    org.springframework.web: trace
    com.best.nacos.client: debug

# 设置要调用微服务的日志级别:none, basic, headers, full
feign:
  client:
    config:
      provider.loggerLevel: full

full日志内容 

2.1.2 java配置

第1步:在Feign接口注解上面配置configuration

第2步:通过注册Bean来设置日志记录级别

// name: 微服务的名称 path: 服务生成者的调用前缀
@FeignClient(name = "provider", path = "/provider", configuration = FeignConfiguration.class)
public interface ProviderClient {

	@GetMapping("/find/instance")
	String service();

	@RequestLine("GET /find/instance")
	String service1();
}
@Configuration
public class FeignConfiguration {

    @Bean
    Logger.Level feignLoggerLevel() {
        // 设置日志级别: None,Basic,Headers,Full
        return Logger.Level.FULL;
    }
}

2.2  实现拦截器设置公共参数

2.2.1 编写拦截器,实现RequestInterceptor接口

public class MyInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        System.out.println("===============执行拦截器apply==============");
        template.header("token", "jak");
    }
}

2.2.2 加入拦截器配置

1.java配置

    @Bean
    public RequestInterceptor interceptor() {
        return new MyInterceptor();
    }

2.配置文件配置 

feign:
  client:
    config:
      # 配置拦截器
      provider.requestInterceptors[0]: com.best.nacos.interceptor.MyInterceptor

 3.消费端测试

 4.生产端测试

@RestController
@RequestMapping("/provider")
public class ProviderController {

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

	@GetMapping("/find/instance")
	public String service(@RequestHeader("token") String token) {

		String applicationName = "provider";
		String result = applicationName + "-" +  port;

		System.out.println("**********" + result);

		System.out.println("token: " + token);

		return result;
	}

}

三、Feign原理介绍

3.1 Feign的架构设计

RPC:(Remote Procedure Call):远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。

 1. 在Feign底层,通过基于面向接口的动态代理方式生成实现类,将请求调用委托到动态代理实现类。

2.根据Contract协议规则,解析接口类的注解信息,解析成内部表现

Feign默认的协议规范

 

 3.基于RequestBean,动态生成Request。

根据传入的Bean对象和注解信息,从中提取出相应的值,来构造Http Request对象

 4.使用Encoder将Bean转换成Http报文正文(消息解析和转码逻辑),Feign最终会将请求转换成Http消息发送出去,传入的请求对象最终会解析成消息体。

 

目前Feign有以下实现 

 

5.拦截器负责对请求和返回进行装饰处理

在请求转换的过程中,Feign抽象出来了拦截器接口,用于用户自定义对请求的操作

例如,希望Http消息传递过程中被压缩,可以定义一个请求拦截器

public class FeignAcceptGzipEncodingInterceptor extends BaseRequestInterceptor {

    protected FeignAcceptGzipEncodingInterceptor(FeignClientEncodingProperties properties) {
        super(properties);
    }

    @Override
    public void apply(RequestTemplate template) {
        // 在Header头部添加相应的数据信息
        addHeader(template, HttpEncoding.ACCEPT_ENCODING_HEADER, HttpEncoding.GZIP_ENCODING, HttpEncoding.DEFLATE_ENCODING);
    }
}

6.日志记录

在发送和接收请求的时候,Feign定义了统一的日志门面来输出日志信息,并且将日志的输出定义了四个等级:

 7.基于重试器发送HTTP请求

Feign内置了一个重试器,当HTTP请求出现IO异常时,Feign会有一个最大尝试次数发送请求,以下是Feign核心

重试器有如下几个控制参数:

  @Override
  public Object invoke(Object[] argv) throws Throwable {
    // 根据输入参数,构造Http请求
    RequestTemplate template = buildTemplateFromArgs.create(argv);
    Options options = findOptions(argv);
    // 克隆出一份重试器
    Retryer retryer = this.retryer.clone();
    // 尝试最大次数,如果中间有结果,直接返回
    while (true) {
      try {
        return executeAndDecode(template, options);
      } catch (RetryableException e) {
        try {
          retryer.continueOrPropagate(e);
        } catch (RetryableException th) {
          Throwable cause = th.getCause();
          if (propagationPolicy == UNWRAP && cause != null) {
            throw cause;
          } else {
            throw th;
          }
        }
        if (logLevel != Logger.Level.NONE) {
          logger.logRetry(metadata.configKey(), logLevel);
        }
        continue;
      }
    }
  }

重试器有如下几个控制参数: 

 8.发送Http请求

Feign真正发送HTTP请求是委托给feign.Client来做的

Feign默认底层通过JDK的java.net.HttpURLConnection实现了feign.Client接口类,在每次发送请求的时候,都会创建新的HttpURLConnection链接,

这也就是为什么默认情况下Feign的性能很差的原因。可以通过拓展该接口,使用Apache HttpClient或者OkHttp3等基于连接池的高性能Http客户端。

3.2 Feign如何进行服务调用

feign.ReflectiveFeign#newInstance: 获取代理对象

InvocationHandlerFactory: 控制反射方法调用

Client:实现类LoadBalancerFeignClient和Default

Feign在封装了相关的请求参数RequestTemplate后,发起服务远程请求调用

  Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
    // 拼接完整的request请求
    Request request = targetRequest(template);

    if (logLevel != Logger.Level.NONE) {
      logger.logRequest(metadata.configKey(), logLevel, request);
    }

    Response response;
    long start = System.nanoTime();
    try {
      // 发起请求,并获取返回结果Response
      response = client.execute(request, options);
    } catch (IOException e) {
      if (logLevel != Logger.Level.NONE) {
        logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
      }
      throw errorExecuting(request, e);
    }
    long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);

    boolean shouldClose = true;
    try {
      if (logLevel != Logger.Level.NONE) {
        response =
            logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime);
      }
      if (Response.class == metadata.returnType()) {
        if (response.body() == null) {
          return response;
        }
        if (response.body().length() == null ||
            response.body().length() > MAX_RESPONSE_BUFFER_SIZE) {
          shouldClose = false;
          return response;
        }
        // Ensure the response body is disconnected
        byte[] bodyData = Util.toByteArray(response.body().asInputStream());
        return response.toBuilder().body(bodyData).build();
      }
      if (response.status() >= 200 && response.status() < 300) {
        if (void.class == metadata.returnType()) {
          return null;
        } else {
          // 解析Response请求,获取最后的结果
          Object result = decode(response);
          shouldClose = closeAfterDecode;
          return result;
        }
      } else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) {
        Object result = decode(response);
        shouldClose = closeAfterDecode;
        // 响应返回结果
        return result;
      } else {
        throw errorDecoder.decode(metadata.configKey(), response);
      }
    } catch (IOException e) {
      if (logLevel != Logger.Level.NONE) {
        logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime);
      }
      throw errorReading(request, response, e);
    } finally {
      if (shouldClose) {
        ensureClosed(response.body());
      }
    }
  }

 从源码中可以很直观的看到真正发起服务请求调用的是feign.Client#execute 

 LoadBalancerFeignClient为 Ribbon负载均衡客户端实现类

1、LoadBalancerFeignClient#execute()是整个Feign Client的执行入口

2、LoadBalancerFeignClient#execute()方法主线逻辑:

  • 封装 RibbonReuqest请求对象
  • 从容器中获取 Ribbon请求配置参数信息IConfigClient
  • 根据RibbonRequest和IConfigClient构造 FeignLoadBalancer。

3、FeignLoadBalancer根据Ribbon的IRule规则从lloadbalancer获取一个服务实例server。

四、Feign源码解析

processOn参考流程图

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

视频教程

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值