SpringCloud学习笔记(五)Feign

目录

Feign简介

Feign的简单使用

Feign原理

Feign设计

Feign源码分析

咋知道哪个接口需要生成实现类?

谁给我生成的实现类?

实现类的InvocationHandler是怎么定义的?啥功能?

调用接口方法时,实现类咋干活的?干了啥?

请求信息咋生成的request信息?

咋把request发出去的?结果咋解析的?

http客户端用的哪个?能不能改?

咋选择的服务器?

request信息能不能人为改变?咋改变?


Feign简介

我们之前学习的时候对微服务的调用采用了RestTemplate+Ribbon的方式,方便吗?其实还好,但是,程序员的眼中从来没有方便,只有更方便…… Feign是 Netflflix 公司开源的轻量级 Rest 客户端 ( https://github.com/OpenFeign/feign ) ,使用 Feign 可以非常方
便、简单的实现 Http 客户端, 使用 Feign 只需要定义一个接口,然后在接口上添加注解即可
  • Feign是Netflix开发的声明式、模板化的HTTP客户端, Feign可以帮助我们更快捷、优雅地调用HTTP API。
  • 在Spring Cloud中,使用Feign非常简单——创建一个接口,并在接口上添加一些注解,代码就完成了。Feign支持多种注解,例如Feign自带的注解或者JAX-RS注解等。
  • Spring Cloud对Feign进行了增强,使Feign支持了Spring MVC注解,并整合了Ribbon和Eureka,从而让Feign的使用更加方便。
  • Spring Cloud Feign是基于Netflix feign实现,整合了Spring Cloud Ribbon和Spring Cloud Hystrix(后面会学习),除了提供这两者的强大功能外,还提供了一种声明式的Web服务客户端定义的方式。
  • Spring Cloud Feign帮助我们定义和实现依赖服务接口的定义。在Spring Cloud feign的实现下,只需要创建一个接口并用注解方式配置它,即可完成服务提供方的接口绑定,简化了在使用Spring Cloud Ribbon时自行封装服务调用客户端的开发量。
  • Spring Cloud Feign具备可插拔的注解支持,支持Feign注解、JAX-RS注解和Spring MVC的注解。

Feign的简单使用

我们先用一用,看到底多方便。

还看我们的项目。

1、在consumer模块的pom文件添加一个依赖。

        
        
            org.springframework.cloud
            spring-cloud-starter-openfeign
        
2、新建 ProductClientService 接口,使用 @FeignClient(" 服务名称 ") 注解标识,来指定调用哪个服务。requestmapping里面的是要访问的服务提供者的访问路径和HTTP动词。
package com.haogenmin.consumer.service;

import com.haogenmin.model.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import java.util.List;

/**
 * @author :HaoGenmin
 * @Title :ProductClientService
 * @date :Created in 2020/6/29 14:14
 * @description:
 */
@FeignClient(value = "microservice-product")
public interface ProductClientService {
    @RequestMapping(value = "/provider/products", method = RequestMethod.POST)
    public boolean add(Product product);

    @RequestMapping(value = "/provider/products/{id}", method = RequestMethod.GET)
    public Product get(Long id);

    @RequestMapping(value = "/provider/products",method = RequestMethod.GET)
    public List list();
}

3、修改controller,这时候之前配置的RestTemplate就没用了。

package com.haogenmin.consumer.controller;


import com.haogenmin.consumer.service.ProductClientService;
import com.haogenmin.model.Product;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.util.List;


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

    @Autowired
    private ProductClientService productClientService;

    @RequestMapping(value = "/products", method = RequestMethod.POST)
    public boolean add(Product product) {
        return productClientService.add(product);
    }

    @RequestMapping(value = "/products/{id}", method = RequestMethod.GET)
    public Product get(@PathVariable("id") Long id) {
        return productClientService.get(id);
    }

    @RequestMapping(value = "/products",method = RequestMethod.GET)
    public List list() {
        return productClientService.list();
    }



}

4、最后主启动类上面扫描一下我们的Feign客户端这个包。

package com.haogenmin.consumer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 * @author :HaoGenmin
 * @Title :ConsumerApplication
 * @date :Created in 2020/6/23 19:31
 * @description:
 */
@EnableFeignClients(basePackages= {"com.haogenmin.consumer.service"})
@EnableEurekaClient
@SpringBootApplication
public class ConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class,args);
    }
}

测试:

以上的过程如下:

  • 启动时,程序会进行包扫描,扫描所有包下所有@FeignClient注解的类,并将这些类注入到spring的IOC容器中。当定义的Feign中的接口被调用时,通过JDK的动态代理来生成RequestTemplate。
  • RequestTemplate中包含请求的所有信息,如请求参数,请求URL等。
  • RequestTemplate声场Request,然后将Request交给client处理,这个client默认是JDK的HTTPUrlConnection,也可以是OKhttp、Apache的HTTPClient等。
  • 最后client封装成LoadBaLanceClient,结合ribbon负载均衡地发起调用。

Feign原理

Feign封装了Http调用流程,更适合面向接口化的变成习惯。

在服务调用的场景中,我们经常调用基于Http协议的服务,而我们经常使用到的框架可能有HttpURLConnection、Apache HttpComponnets、OkHttp3 、Netty等等,这些框架在基于自身的专注点提供了自身特性。而从角色划分上来看,他们的职能是一致的提供Http调用服务。具体流程如下:

SpringCloud学习笔记(五)Feign_第1张图片

Feign设计

 

SpringCloud学习笔记(五)Feign_第2张图片

要理解以上内容,需要理解动态代理的知识。

1、我们在自己写的接口上面加了注解以及注解里面的参数信息。Feign根据这些信息,生成了一个代理类,这个代理类可以把接口方法的参数,转成request,然后用ribbon找到服务器,用http客户端把请求发出去,再解析请求。等等等等。

2、代理类咋做的呢?我们知道动态代理真正执行的是InvocationHandler里面的invoke方法。实际上,Feign实现了专门用于FeignClient的InvocationHandler,但是,它是构建了一个接口方法到方法处理器的映射表(Method==>MethodHandler),当我们执行接口的method方法的时候,我们通过这个映射表找到对应的MethodHandler,MethodHandler是真正的执行者。

3、MethodHandler就厉害了,它根据参数信息,注解信息,把请求参数等相关信息创建了一个requestTemplate出来。然后执行请求并解析数据。这一切都是MethodHandler做的,当然,它用到了encoder和decoder以及httpClient。

4、Feign定义了拦截器接口,在请求发送之前可以拦截一下,对其进行自定义修改请求信息。

5、httpclient有很多种,默认的性能比较低,生产环境按需修改,而我们使用的一般是一个httpclient的包装类,为啥?因为我们配置的是应用名啊,我们得需要先找到对应的服务器才能发请求。所以这个httpclient是这个包装Client类的一个属性引用,在访问请求的时候,这个包装类先用ribbon根据负载策略找到合适的服务器IP端口。然后,用httpclient进行最终的请求。

如果大家觉得我说的太简单,看不懂。请看这一篇。我这里主要是针对源码进行分析。

原理参考:

https://www.cnblogs.com/crazymakercircle/p/11965726.html

https://www.jianshu.com/p/8c7b92b4396c

Feign源码分析

首先,我们知道springcloud肯定根据我们的接口注解信息生成了实现类,那么我们现在要探寻的问题有这么几个:

咋知道哪个接口需要生成实现类?

谁给我生成的实现类?

实现类的InvocationHandler是怎么定义的?啥功能?

调用接口方法时,实现类咋干活的?干了啥?

请求信息咋生成的request信息?

咋把request发出去的?结果咋解析的?

http客户端用的哪个?能不能改?

咋选择的服务器?

request信息能不能人为改变?咋改变?

带着问题我们来看看代码:

咋知道哪个接口需要生成实现类?

首先我们知道启动类上面注解了@EnableFeignClients,目的是为了扫描FeignClient接口,所以,能扫描到的接口中,有@FeignClient注解的就是我们要实现代理的接口。

从下图中我们发现除了启动类,只在FeignClientsRegistrar类中引用了EnableFeignClients。看名字就知道这是Feign客户端的注册器。

SpringCloud学习笔记(五)Feign_第3张图片

当应用启动时会首先调用FeignClientsRegistrar的registerBeanDefinitions()方法。

SpringCloud学习笔记(五)Feign_第4张图片

主要看下registerFeignClients()方法。

SpringCloud学习笔记(五)Feign_第5张图片

我们再看一看当找不到clients属性时候,怎么扫描包的:

SpringCloud学习笔记(五)Feign_第6张图片

所以我们@EnableFeignClients(basePackages= {"com.haogenmin.consumer.service"})这个注解就是这个时候被处理的。然后,就把我们的ProductClientService接口注册进去了。

再看一看是怎么注册到容器中的。

SpringCloud学习笔记(五)Feign_第7张图片

到此,应该明白,因为我们的注解,那些接口被扫描,打上了FeignClient的标记,然后加入了Spring容器,就知道哪些借口是FeignClient的类型了。

谁给我生成的实现类?

在spring容器启动时会调用FeignClientFactoryBean的getObject()方法(只有在其他bean注入feign client时才会调用)。

SpringCloud学习笔记(五)Feign_第8张图片

看一下getTarget的注释,根据给定的数据和上下文信息,创建一个FeignClient客户端。从@FeignClient注解上是否指定URL,feign的处理分成了两部分,如果未指定URL,则使用负载均衡去发送请求,指定URL,只会向指定的URL发送请求。

SpringCloud学习笔记(五)Feign_第9张图片

SpringCloud学习笔记(五)Feign_第10张图片

我们再来看看loadBalance函数做了什么?

SpringCloud学习笔记(五)Feign_第11张图片

SpringCloud学习笔记(五)Feign_第12张图片

SpringCloud学习笔记(五)Feign_第13张图片

Targeter默认为DefaultTargeter,client为LoadBalancerFeignClient。再看下DefaultTargeter.target()方法。

SpringCloud学习笔记(五)Feign_第14张图片

feign.target()方法。

SpringCloud学习笔记(五)Feign_第15张图片

这里看到,调用的其实是ReflectiveFeign的newInstance方法。

这个build里面的参数一大堆:

  • decoder:将http请求的response转换成对象
  • encoder:将http请求的对象转换成http request body
  • contract:校验Feign Client上的注解及value值是否合法
  • retryer:定义http请求如果失败了是否应该重试以及重试间隔、方式等等
  • RequestInterceptor:feign发起请求前的拦截器,可以全局定义basic auth、发起请求前自动添加header等等。

先看看这个newInstance方法做了啥。

SpringCloud学习笔记(五)Feign_第16张图片

由此,我们知道了,在spring依赖注入的时候,生成了接口的代理,处理的类是ReflectiveFeign 。

实现类的InvocationHandler是怎么定义的?啥功能?

从这里我们知道handler是这个工厂制造出来的。看一看。发现是构造函数传进来的。回到之前Feign中发现:

SpringCloud学习笔记(五)Feign_第17张图片

SpringCloud学习笔记(五)Feign_第18张图片

你发现兜了一大圈,有回来了。搞了半天FeignInvocationHandler就是ReflectiveFeign的一个静态内部类。

于是我们发现,FeignInvocationHandler是定义好的,传入参数生产出实例,为每个接口生成代理对象。功能就是,当我们调用接口的方法时,调用的其实是FeignInvocationHandler的invoke来执行的。

到这,在启动时候的注入过程就完了。

简单总结下启动时Feign所做的处理:

  • 获取@EnableFeignClients注解配置的扫描包路径,如果没配置,默认为启动类的包路径。
  • 获得扫描包路径下@FeignClient修饰的类
  • 校验@FeignClient修饰的类,包括类必须是interface,以及@FeignClient的fallback及fallbackFactory配置的必须是接口的实现类等
  • 将@FeignClient修饰的类交由spring管理,声明为bean,其他bean注入FeignClient时注入的其实是当前FeignClient的代理类,这个代理类包装在Targeter内部,Targeter被注入到引用的bean中。

这样做的好处是:在程序中使用Feign Client时就可以像其他spring 管理的bean一样直接注入即可。

调用接口方法时,实现类咋干活的?干了啥?

接下来看一下FeignInvocationHandler的invoke方法。

SpringCloud学习笔记(五)Feign_第19张图片

看到,它有一个属性,是传进来的Method与MethodHandler的映射。当我们调用接口的方法时,实际上处理的是对应的MethodHandler。

那么我们看一下MethodHandler是哪个?他又做了啥?

SpringCloud学习笔记(五)Feign_第20张图片

从上图发现,MethodHandler是apply方法得来的,看一看。

SpringCloud学习笔记(五)Feign_第21张图片

SpringCloud学习笔记(五)Feign_第22张图片

到这,发现SynchronousMethodHandler就是最终执行的MethodHandler。

SpringCloud学习笔记(五)Feign_第23张图片

以上是实现类工作的过程,在这里请求被处理,响应被解析。当然,我们不想到这就结束,还想进一步看看,请求咋发出去的?谁发出去的……

请求信息咋生成的request信息?

那我们先看一看RequestTemplate咋生成的。

找到在这里:

SpringCloud学习笔记(五)Feign_第24张图片

进入这个类瞅一眼,发现又是ReflectiveFeign的静态内部类:

SpringCloud学习笔记(五)Feign_第25张图片

找到它的create方法。

 @Override
    public RequestTemplate create(Object[] argv) {
        // 获取methodMetada的template,这个RequestTemplate是可变的,跟随每次调用参数而变。
      RequestTemplate mutable = RequestTemplate.from(metadata.template());
      if (metadata.urlIndex() != null) {
        //处理@PathVariable在URL上插入的参数  
        int urlIndex = metadata.urlIndex();
        checkArgument(argv[urlIndex] != null, "URI parameter %s was null", urlIndex);
        mutable.target(String.valueOf(argv[urlIndex]));
      }
        //处理调用方法的param参数,追加到URL ?后面的参数
      Map varBuilder = new LinkedHashMap();
      for (Entry> entry : metadata.indexToName().entrySet()) {
        int i = entry.getKey();
        Object value = argv[entry.getKey()];
        if (value != null) { // Null values are skipped.
          if (indexToExpander.containsKey(i)) {
            value = expandElements(indexToExpander.get(i), value);
          }
          for (String name : entry.getValue()) {
            varBuilder.put(name, value);
          }
        }
      }
        //处理query参数以及body内容  
      RequestTemplate template = resolve(argv, mutable, varBuilder);
      if (metadata.queryMapIndex() != null) {
        // add query map parameters after initial resolve so that they take
        // precedence over any predefined values
        // 当  RequestTemplate处理完参数后,再处理@QueryMap注入的参数,以便优先于任意值。
        Object value = argv[metadata.queryMapIndex()];
        Map queryMap = toQueryMap(value);
        template = addQueryMapQueryParameters(queryMap, template);
      }

      if (metadata.headerMapIndex() != null) {
        //处理RequestTemplate的header内容  
        template =
            addHeaderMapHeaders((Map) argv[metadata.headerMapIndex()], template);
      }

      return template;
    }

可以看到,第一步是根据调用时的参数等构造了RequestTemplate的param、body、header等内容。

再看executeAndDecode方法。

SpringCloud学习笔记(五)Feign_第26张图片

到这,我们发现,我们关于请求的各种信息,终于生成了一个request请求。

咋把request发出去的?结果咋解析的?

继续看executeAndDecode方法,发现在这个地方发送了请求,并处理了响应结果。

SpringCloud学习笔记(五)Feign_第27张图片

http客户端用的哪个?能不能改?

接下来,是比较重要的一点。我们用的哪个HTTP客户端执行的请求呢?回顾到第一步,我们知道这个Client是LoadBalancerFeignClient我们看看这个客户端的执行方法。

SpringCloud学习笔记(五)Feign_第28张图片

SpringCloud学习笔记(五)Feign_第29张图片

这个值在配置类里面生成bean的时候传进去了,就是:

SpringCloud学习笔记(五)Feign_第30张图片

Feign 默认底层通过JDK 的 java.net.HttpURLConnection 实现了feign.Client接口类,在每次发送请求的时候,都会创建新的HttpURLConnection 链接,这也就是为什么默认情况下Feign的性能很差的原因。可以通过拓展该接口,使用Apache HttpClient 或者OkHttp3等基于连接池的高性能Http客户端。所以,大家使用的时候,记得改一下这个客户端。

咋选择的服务器?

这部分是ribbon处理的,ribbon的内容很多,见上一篇详解。

其实,到这里大致的流程都已经看完了,除了encoder,decoder,和日志记录之外的过程,这里都在源码里走了一遍了。另外我们还想知道,请求的信息可否自定义添加。

request信息能不能人为改变?咋改变?

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

SpringCloud学习笔记(五)Feign_第31张图片

我们可以在apply方法中对request模板进行自定义操作。具体用法,可以参考官方的几个实现类。

SpringCloud学习笔记(五)Feign_第32张图片

对以上过程总结:

  • 代理类先调用到FeignInvocationHandler的invoke方法,而这个invoke方法相当于直接调用了SynchronousMethodHandler的invoke方法。
  • SynchronousMethodHandler的invoke方法主要是构造了RequestTemplate以及出现异常重试的Retryer,最后根据构造的RequestTemplate发起了http请求以及decode。
  • 构造RequestTemplate时,根据传入的参数动态构建URL中的参数(@PathVarible)以及URL ?追加的参数,还有body等等,最后再处理@QueryMap注入的参数,以保证优先级最高。
  • 发起http请求时,没有负载均衡时,默认是通过JDK的HttpURLConnection发送请求,另一种就是LoadBalancerFeignClient各种实现类,如Apache的HTTPClient,以及OKhttp等,这些实现也是通过ribbon动态指定服务器IP地址,以达到负载均衡的作用。
  • 最后将response处理成需要的返回值类型,以及根据状态码进行decode。

然而,我们说的这个过程是Feign没有配置熔断器的时候的过程。配置熔断器的话,在最初的时候得到的Target就不是默认的那个了。回头有时间再看……

 

 

 

 

你可能感兴趣的:(SpringCloud)