OpenFeign在分布式服务中运用非常广泛,它和RPC所要达到的效果一致,就是为了简化远程服务调用的操作,通过使用OpenFeign可以使得调用远程服务就像调用本地服务一样方便。

但是其和RPC在实现上还是不太一样,不一样的地方主要还是调用的方式。
OpenFeign是内部实现了Rest服务调用,从而一个本地服务调用远程服务的接口时,主要还是通过Rest服务调用的方式,那么对于服务端的要求就是其服务需要将这个接口通过rest暴露出来,不然OpenFeign将无法工作;
RPC则不需要服务端将该接口以rest服务的形式暴露出来,而是直接通过底层netty进行通信来互相沟通的。

前言

RPC

远程过程调用(Remote Procedure Call Protocol,简称RPC)。

RPC 框架说白了就是让你可以像调用本地方法一样调用远程服务提供的方法,而不需要关心底层的通信细节。简单地说就让远程服务调用更加简单、透明。
一个RPC框架大致需要动态代理、序列化、网络请求、网络请求接受(netty实现)、动态加载、反射这些知识点。
现在开源及各公司自己造的RPC框架层出不穷,唯有掌握原理是一劳永逸的。

业界主流的 RPC 框架整体上分为三类:

  1. 支持多语言的 RPC 框架,比较成熟的有 Google 的 gRPC、Apache(Facebook)的 Thrift;
  2. 只支持特定语言的 RPC 框架,例如新浪微博的 Motan;
  3. 支持服务治理等服务化特性的分布式服务框架,其底层内核仍然是 RPC 框架, 例如阿里的 Dubbo。

Rest

具象状态传输(Representational State Transfer, REST)。

REST 不是一种协议,它是一种架构。大部分REST的实现中使用了RPC的机制,大致由三部分组成:

  1. method:动词(get、post之类的)
  2. Host:URI(统一资源标识),服务器,端口
  3. Path:名词(路径,服务器里面的某个东西)路径的结尾是资源的形态(如html、text、image、pdf等)

即,对 host 里面的某个 Path 里面的东西做一些 get 或 post 操作。
传输层基于HTTP,相比于TCP,多了一层协议。

Feign

Feign是Netflix公司(第一代SpringCloud)研发的一个轻量级RESTful的伪HTTP服务客户端。
Feign内置了Ribbon逻辑,通过负载均衡算法可以从注册中心中寻找服务。
Feign 是 Netflix 公司产品,已经停止更新了。

Feign只是对HTTP调用组件进行了易用性封装,底层还是使用我们常见的OkHttp、HttpClient等组件(我们不生产水,我们只是水的搬运工)

Feign的目标之一就让这些HTTP客户端更好用,使用方式更统一(这和Spring出现的目的如出一辙),更像RPC。

调用过程

客户端组件 feign.Client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package feign;

import feign.Request.Options;

import java.io.IOException;

/**客户端接口
* Submits HTTP {@link Request requests}.
* Implementations are expected to be thread-safe.
*/
public interface Client {
//提交HTTP请求,并且接收response响应后进行解码
Response execute(Request request, Options options) throws IOException;
}

由于不同的feign.Client 实现类,内部完成HTTP请求的组件和技术不同,故,feign.Client 有多个不同的实现。

  • Client.Default类
    默认的feign.Client 客户端实现类,内部使用HttpURLConnnection 完成URL请求处理;
  • ApacheHttpClient类
    内部使用 Apache httpclient 开源组件完成URL请求处理的feign.Client 客户端实现类;
  • OkHttpClient类
    内部使用 OkHttp3 开源组件完成URL请求处理的feign.Client 客户端实现类。
  • LoadBalancerFeignClient类
    内部使用 Ribben 负载均衡技术完成URL请求处理的feign.Client 客户端实现类。

Java中常用http客户端

  • JDK原生HttpClient
    原生HttpClient是在Java 9中作为孵化模块引入的,然后在Java11中作为JEP 321的一部分正式可用,HTTPClient取代了JDK更早期的HttpUrlConnection类。
    • 使用 HttpURLConnection 发起 HTTP 请求最大的优点是不需要引入额外的依赖,但是使用起来非常繁琐,也缺乏连接池管理、域名机械控制等特性支持。
    • 在 Java 9 中,官方在标准库中引入了一个 high level、支持 HTTP/2 的 HttpClient
  • Apache HttpComponents项目中的HttpClient
    Apache HttpClient带有连接池的功能,具备优秀的HTTP连接的复用能力。
  • OkHttpClient
    使用OkHttp3 开源组件完成URL请求处理,OkHttp3 开源组件由Square公司开发。
  • Spring Boot中的WebClient

Spring Cloud OpenFeign

Feign的第一个目标是减少HTTP API的复杂性,希望能将HTTP调用做到像RPC一样易用。而Spring Cloud OpenFeign将其和Spring Cloud体系打通,让Feign更加方便的在Spring Cloud中使用。

OpenFeign是SpringCloud自己研发的,在Feign的基础上做了增强。
OpenFeign除了原有Ribbon逻辑外,还支持了Hystrix和Spring MVC注解。

OpenFeign 是SpringCloud在Feign的基础上支持了SpringMVC的注解,如@RequestMapping等。
OpenFeign 的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过JDK动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。

工作原理

Feign的工作原理,核心点围绕在被@FeignClient修饰的接口如何发送HTTP网络请求并获取响应。

问题探究

1. @FeignClient 如何根据接口生成实现(代理)类的?

Feign使用必须要有接口,满足JDK动态代理的使用条件,所以Feign使用的就是JDK自带的动态代理技术。

Feign接口的每个方法有 @RequestMapping ,意味着这些方法可以映射到不同的远端HTTP路径,所以给整个Feign接口做代理时,代理类的方法必须知道对应到哪个远端HTTP路径,虽然我们可以在 java.lang.reflect.InvocationHandler#invoke 的方法入参 Method 中去解析 @RequestMapping 拿url(注意,大多数开源框架很忌讳在运行时高频使用JDK的反射,这样影响执行效率,Dubbo的Provider端也不是用反射来调用本地方法的),所以在Feign使用JDK动态代理技术时,需要提前将接口带 @RequestMapping 方法解析出来
为了探究这块的具体实现,我们移步原生Feign的feign-core包的核心类ReflectiveFeign

ReflectiveFeign借助于JDK动态代理,根据我们的业务接口生成对应的代理类,这个代理类会根据调用的方法来直接找到对应已经提前准备好的 MethodHandler,直接调用即可完成Feign的使命,根据上面的使用方法,我们不难猜到 MethodHandler 里面有HTTP调用的相关信息(这些信息之前是在接口方法定义的 @RequestMapping 或 @RequestLine 之中),而且MethodHandler#invoke会完成真正的HTTP调用并将结果反序列化成原接口方法的返回值对象

2. 生成的实现(代理)类是如何适配各种HTTP组件的?

这个问题应该由Feign来回答,而不是Spring Cloud OpenFeign,Feign的feign-core模块中有一个Client接口专门用来给各个HTTP组件提供接入接口

Slf4j适配各种日志组件的方案类似——SLF4J漫谈

3. 生成的实现(代理)类如何实现HTTP请求应答序列化和反序列化的?

  • 原生的Feign
    原生的Feign允许添加额外的解码器,官方给出了Consumer的例子:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Example {
    public static void main(String[] args) {
    // 这里假定ProviderDemoService中有一个返回MyResponse的方法
    MyResponse response = Feign.builder()
    .decoder(new GsonDecoder())
    .client(new OkHttpClient())
    .target(ProviderDemoService.class, "https://xxxx");
    }
    }
    为了能做到这一点,原生Feign提供了 Decoder 和 Encoder 两个接口(本文只重点关注解码部分):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    public interface Decoder {

    /**
    * Decodes an http response into an object corresponding to its
    * {@link java.lang.reflect.Method#getGenericReturnType() generic return type}. If you need to
    * wrap exceptions, please do so via {@link DecodeException}.
    *
    * @param response the response to decode
    * @param type {@link java.lang.reflect.Method#getGenericReturnType() generic return type} of the
    * method corresponding to this {@code response}.
    * @return instance of {@code type}
    * @throws IOException will be propagated safely to the caller.
    * @throws DecodeException when decoding failed due to a checked exception besides IOException.
    * @throws FeignException when decoding succeeds, but conveys the operation failed.
    */
    Object decode(Response response, Type type) throws IOException, DecodeException, FeignException;
    }

    public interface Encoder {

    /**
    * Converts objects to an appropriate representation in the template.
    *
    * @param object what to encode as the request body.
    * @param bodyType the type the object should be encoded as. {@link #MAP_STRING_WILDCARD}
    * indicates form encoding.
    * @param template the request template to populate.
    * @throws EncodeException when encoding failed due to a checked exception.
    */
    void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException;
    }
  • Spring Cloud OpenFeign
    换成Spring Cloud OpenFeign的话,就得和Spring的Web体系打通了,这里就不得不提一个构造类即 FeignClientsConfiguration
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    // 注意:为了演示方便,对其进行了代码裁剪
    @Configuration(proxyBeanMethods = false)
    public class FeignClientsConfiguration {

    @Autowired
    // 这里将Spring Web的消息转换器机制注入进来
    private ObjectFactory<HttpMessageConverters> messageConverters;

    @Bean
    @ConditionalOnMissingBean
    // 构造解码Decoder的Spring Bean
    public Decoder feignDecoder() {
    // 这里的SpringDecoder实现了Feign的Decoder接口,并且将Spring Web的消息转换器设置到SpringDecoder来使用
    return new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)));
    }

    @Bean
    @ConditionalOnMissingBean
    // 构造编码Encoder的Spring Bean
    public Encoder feignEncoder(ObjectProvider<AbstractFormWriter> formWriterProvider) {
    return springEncoder(formWriterProvider);
    }

    private Encoder springEncoder(ObjectProvider<AbstractFormWriter> formWriterProvider) {
    AbstractFormWriter formWriter = formWriterProvider.getIfAvailable();

    if (formWriter != null) {
    return new SpringEncoder(new SpringPojoFormEncoder(formWriter), this.messageConverters);
    }
    else {
    return new SpringEncoder(new SpringFormEncoder(), this.messageConverters);
    }
    }
    }

4. 生成的实现(代理)类是如何注入到Spring容器中的?

Spring Cloud OpenFeign如何将动态生成的代理类和Spring容器打通?

1
2
3
4
5
6
7
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
// 注解内容省略
}

就是@EnableFeignClients@Import 提前加载Spring Bean的方式,触发了 FeignClientRegistrar 的初始化。
FeignClientRegistrar 由于实现了 ImportBeanDefinitionRegistrar 接口,我们知道在处理@Configuration类时可以通过Import注册其他Spring Bean定义的能力,而还不知道哪些接口使用了 @FeignClient,所以在 FeignClientRegistrar 需要做的就是扫描某些路径(该路径由配置Spring扫描路径包括@EnableFeignClients中配置的路径)的接口类,识别对应的 @FeignClient,给这些接口类创建代理对象。而为了把这些代理对象注入到Spring 容器中,所以还得借助 FactoryBean 的能力。