实战系列-被面试官问到Feign原理

导语
  事情是这样的,昨天参加了某公司二面,被面试官问道了Spring Cloud的RESTFul远程调用。项目上用到的技术就是OpenFeign,面试官可能自己不是太了解,给他解释一番发现自己还有很多的细节也不是太清楚,下面就来结合OpenFeign的源码来分析一下

  Feign远程调用,核心就是通过一系列的封装和处理,将以JAVA注解的方式定义的远程调用API接口,最终转换成HTTP的请求形式,然后将HTTP的请求的响应结果,解码成JAVA Bean,放回给调用者。

  在使用的时候我们必不可少的两个注解@FeignClient和@EnableFeignClients两个注解,一个是开启Feign功能@EnableFeignClients ,一个是作为客户端应用注入@FeignClient。

@EnableFeignClients注解

  这个注解跟一般的注解无异,唯一不一样的地方就是使用@Import注解,我们知道@Import注解是向容器中注入一些其他的或者第三方的对象,可以理解为外部的操作通过Import的方式引入进来,既然是这样,那就要研究一下这个引入的类。FeignClientsRegistrar,从单词直译上可以看出来其实它是一个注册器。那么这个注册器到底有什么作用。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {

	/**
	 * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation
	 * declarations e.g.: {@code @ComponentScan("org.my.pkg")} instead of
	 * {@code @ComponentScan(basePackages="org.my.pkg")}.
	 * @return the array of 'basePackages'.
	 */
	String[] value() default {};

	/**
	 * Base packages to scan for annotated components.
	 * <p>
	 * {@link #value()} is an alias for (and mutually exclusive with) this attribute.
	 * <p>
	 * Use {@link #basePackageClasses()} for a type-safe alternative to String-based
	 * package names.
	 * @return the array of 'basePackages'.
	 */
	String[] basePackages() default {};

	/**
	 * Type-safe alternative to {@link #basePackages()} for specifying the packages to
	 * scan for annotated components. The package of each class specified will be scanned.
	 * <p>
	 * Consider creating a special no-op marker class or interface in each package that
	 * serves no purpose other than being referenced by this attribute.
	 * @return the array of 'basePackageClasses'.
	 */
	Class<?>[] basePackageClasses() default {};

	/**
	 * A custom <code>@Configuration</code> for all feign clients. Can contain override
	 * <code>@Bean</code> definition for the pieces that make up the client, for instance
	 * {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, {@link feign.Contract}.
	 *
	 * @see FeignClientsConfiguration for the defaults
	 * @return list of default configurations
	 */
	Class<?>[] defaultConfiguration() default {};

	/**
	 * List of classes annotated with @FeignClient. If not empty, disables classpath
	 * scanning.
	 * @return list of FeignClient classes
	 */
	Class<?>[] clients() default {};

}

  FeignClientsRegistrar ,这个类的描述是default ,表示只允许在同一个包中进行访问。它所在的包空间是org.springframework.cloud.openfeign.FeignClientsRegistrar。也就是说org.springframework.cloud.openfeign包下的所有内容都可以访问。那么它的具体内容是怎么样子呢?

class FeignClientsRegistrar
		implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {

  从代码中可以看到它继承了ResourceLoaderAware、EnvironmentAware以及ImportBeanDefinitionRegistrar。三个接口。这三个接口分别提供如下的一些操作

  • ResourceLoaderAware :设置资源加载扩展
  • EnvironmentAware:设置环境扩展
  • ImportBeanDefinitionRegistrar:通过注解信息,注册一个BeanDefinition,通过注解操作向容器中注入Bean

  两个验证方法,分别验证有没有回调类和回调工厂类这个主要是用来检测在@FeignClient参数中是否传入了fallback参数和fallbackFactory参数。
在这里插入图片描述
在这里插入图片描述

  接下来其实关系的是ImportBeanDefinitionRegistrar接口的两个实现方法

default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,
			BeanNameGenerator importBeanNameGenerator)

default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
	}

  进入这个方法实现之后,会看到分别实现了两个方法,默认的配置的注入,一个是对FeignClient的注入操作。首先来看看配置的注入
在这里插入图片描述

registerDefaultConfiguration()方法

  这个方法是私有方法传入的参数分别是注解的元数据,以及BeanDefinition的注册器。这个注册器的使用在Spring扩展机制以及Spring源码的分析中可以看到。 这里首先获取到一个默认参数,这里调用的时候获取的是EnableFeignClients注解中配置的信息,也就是说在EnableFeignClients注解中的那些配置项都有那些被配置了。
在这里插入图片描述
  会看到获取完注解配置信息之后,第一步先去看看是否有 defaultConfiguration 的配置,如果有的话就去判断是否是在其他地方被禁用了,如果没有的话就加入默认配置,如果有的话就加入默认不配置。最终将所有的配置通过registerClientConfiguration()方法进行注册。根据一般的Spring规则来看这里并不是方法的终结,一般应该是有一个do开头的方法才会是最后的调用逻辑。既然这样就进入到registerClientConfiguration()方法中。
在这里插入图片描述
  进入这个方法registerClientConfiguration(),会看到这里采用了建造者模式,通过获取到的基本信息要去组装一个BeanDefinition出来。这里卡看到调用了一个叫做FeignClientSpecification的类,通过这类去生成,也就是说这个就是最终需要定制生成的内容。到这里就没有啥可以分析了,后续的逻辑就是调用了Spring IOC的逻辑。

class FeignClientSpecification implements NamedContextFactory.Specification

  上面提到有两个方法另个一方法registerFeignClients(metadata, registry);

registerFeignClients()方法

  从方法名上可以看到它注入的就是FeignClient。具体方法内容如下
在这里插入图片描述
  首先来分析一下整个的执行过程:

  • 1、进入方法之后获取到一个类路径扫描器,将资源加载器添加到扫描器中。
  • 2、分别的获取到两个注解的注解的源性信息。
  • 3、最终调用获取组件的额方法将所有被注解标识的组件都获取到
  • 4、调用了registerFeignClient(registry, annotationMetadata, attributes)方法进行注入

  从上面的逻辑分析可以看到,其实这里面重要的一个方法就是registerFeignClient(registry, annotationMetadata, attributes),方法下面就来进入这个方法
  我们知道在Spring框架中BeanDefinition充当的角色就是整个的容器原料提供的角色,这个BeanDefinition并不是我们最终想要的或者说在应用中所需要的哪个Bean对象,而是为我们最终想要的对象提供一个最原始的数据。那么这里会看到。通过构建者,这里使用了我们的FactoryBean来构建出来一个特定的对象,而Spring的扩展机制就是通过FactoryBean来实现。这里会看到这个构建者传入的就是一个FeignClientFactoryBean,而他所继承的接口就是FactoryBean的接口。而我们最终想从中获取到的内容应该就被这个接口扩展过通过getObject获取到的内容。而这个内容就是Builder组装出来的内容。
在这里插入图片描述

  到这里整个的装配逻辑算是搞清楚了。从代码上可以很清楚的看到FeignClient和EnableFeignClients是怎么工作的。那么既然搞清楚这些了接下来就来看看整个的具体的内部流转逻辑。

内部流转逻辑

  我们都知道在Spring Cloud Feign中使用的基于RESTFul风格的调用既然是RESTFul风格的调用。那么就需要有一套基于RESTFul的调用逻辑。通过上面的逻辑可以知道,通过上面的注入操作,其实所有的被@FeignClient标注的内容已经全部进入到了IOC容器中。那么接下来的内容就是怎么样去使用这些内容。

  也就是是说其实@FeignClient为我们注册进入的时候就已经是存在一个需要的最简版本的BeanDefinition,而这个最简版本正好可以满足的通过动态代理进行调用的逻辑。也就是通过BeanDefinitionRegistry注入的内容。最终在我们当前运行的环境中应该有一个与远程客户端的IOC容器中相同的Bean定义内容。这个就是为什么可以在本地的应用中直接使用被@FeignClient标注的类进行调用。
  既然调用使用的是SpringIOC,那么就有一个内容需要注意,就是从BeanDefinition到实际调用Bean的过程中其实是需要经过一层代理机制的。也就是说到使用之前我们并不知道具体需要一个什么样的Bean对象。而这里在客户端调用的被@FeignClient标注的内容就可以理解为一个需要的Bean对象。

FeignInvocationHandler 调用处理器

  这里就要从Spring容器的动态代理机制说起。有这样一个逻辑,就是在不改变原有对象的基础上,对原有对象进行操作,要满足这样的操作就需要使用我们的代理机制。

在这里插入图片描述
  通过 JDK Proxy 生成动态代理类,核心步骤就是需要定制一个调用处理器,具体来说,就是实现JDK中位于java.lang.reflect 包中的 InvocationHandler 调用处理器接口,并且实现该接口的 invoke(…) 抽象方法。在Feign的实现中是由下面这类来进行实现的FeignInvocationHandler。而这个类是数据才feign-core模块中里面feign.ReflectiveFeign类的一个静态内部类。

static class FeignInvocationHandler implements InvocationHandler {

    private final Target target;
    private final Map<Method, MethodHandler> dispatch;

    FeignInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) {
      this.target = checkNotNull(target, "target");
      this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      if ("equals".equals(method.getName())) {
        try {
          Object otherHandler =
              args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
          return equals(otherHandler);
        } catch (IllegalArgumentException e) {
          return false;
        }
      } else if ("hashCode".equals(method.getName())) {
        return hashCode();
      } else if ("toString".equals(method.getName())) {
        return toString();
      }

      return dispatch.get(method).invoke(args);
    }

    @Override
    public boolean equals(Object obj) {
      if (obj instanceof FeignInvocationHandler) {
        FeignInvocationHandler other = (FeignInvocationHandler) obj;
        return target.equals(other.target);
      }
      return false;
    }

    @Override
    public int hashCode() {
      return target.hashCode();
    }

    @Override
    public String toString() {
      return target.toString();
    }
  }

  源码很简单,重点在于invoke(…)方法,虽然核心代码只有一行,但是其功能是复杂的:

  • (1)根据Java反射的方法实例,在dispatch 映射对象中,找到对应的MethodHandler 方法处理器;
  • (2)调用MethodHandler方法处理器的 invoke(…) 方法,完成实际的HTTP请求和结果的处理。

  补充说明一下:MethodHandler 方法处理器,和JDK 动态代理机制中位于 java.lang.reflect 包的 InvocationHandler 调用处理器接口,没有任何的继承和实现关系。MethodHandler 仅仅是Feign自定义的,一个非常简单接口。
  上面这方法其中最关键的一点就是,有一个非常重要Map类型成员 dispatch 映射,保存着远程接口方法到MethodHandler方法处理器的映射。
在这里插入图片描述
  默认的调用处理器 FeignInvocationHandler,在处理远程方法调用的时候,会根据Java反射的方法实例,在dispatch 映射对象中,找到对应的MethodHandler 方法处理器,然后交给MethodHandler 完成实际的HTTP请求和结果的处理。

MethodHandler 方法处理器

  看上去这个方法处理器很简单,但是我们需要有很多的不同的方法处理器,那么这个地方使用到的就是工厂设计模式,这里其实这个接口是被包括在一个InvocationHandlerFactory 的处理器工厂中这个工厂主要生产两种处理器一个是上面的调用处理器,另一个就是方法处理器。

 /**
   * Like {@link InvocationHandler#invoke(Object, java.lang.reflect.Method, Object[])}, except for a
   * single method.
   */
  interface MethodHandler {

    Object invoke(Object[] argv) throws Throwable;
  }

  这个方法处理器默认有两种处理方式一种是默认,一种是同步,既然这样的话可以大胆的猜测,默认采用的异步的方式。
在这里插入图片描述

SynchronousMethodHandler 同步处理

  进入类型之后最为关注的方法就是调用方法。invoke()调用方法
在这里插入图片描述

在这里插入图片描述
SynchronousMethodHandler的invoke(…)方法,调用了自己的executeAndDecode(…) 请求执行和结果解码方法。该方法的工作步骤:

  • (1)首先通 RequestTemplate 请求模板实例,生成远程URL请求实例 request;
  • (2)然后用自己的 feign 客户端client成员,excecute(…) 执行请求,并且获取 response 响应;
  • (3)对response 响应进行结果解码

Feign 客户端组件 feign.Client

 */
public interface Client {

  /**
   * Executes a request against its {@link Request#url() url} and returns a response.
   *
   * @param request safe to replay.
   * @param options options to apply to this request.
   * @return connected response, {@link Response.Body} is absent or unread.
   * @throws IOException on a network error connecting to {@link Request#url()}.
   */
  Response execute(Request request, Options options) throws IOException;

由于不同的feign.Client 实现类,内部完成HTTP请求的组件和技术不同,故,feign.Client 有多个不同的实现。这里举出几个例子:

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

在这里插入图片描述

Client.Default类

  这个类默认使用的自带的HttpURLConnnection 来实现网络请求。在JKD1.8中,虽然在HttpURLConnnection 底层,使用了非常简单的HTTP连接池技术,但是,其HTTP连接的复用能力,实际是非常弱的,性能当然也很低

ApacheHttpClient类

  ApacheHttpClient 客户端类的内部,使用 Apache HttpClient开源组件完成URL请求的处理。
  从代码开发的角度而言,Apache HttpClient相比传统JDK自带的URLConnection,增加了易用性和灵活性,它不仅使客户端发送Http请求变得容易,而且也方便开发人员测试接口。既提高了开发的效率,也方便提高代码的健壮性。
  从性能的角度而言,Apache HttpClient带有连接池的功能,具备优秀的HTTP连接的复用能力。关于带有连接池Apache HttpClient的性能提升倍数,具体可以参见后面的对比试验。
  ApacheHttpClient 类处于 feign-httpclient 的专门jar包中,如果使用,还需要通过Maven依赖或者其他的方式,倒入配套版本的专门jar包。

OkHttpClient类

  OkHttpClient 客户端类的内部,使用OkHttp3 开源组件完成URL请求处理。OkHttp3 开源组件由Square公司开发,用于替代HttpUrlConnection和Apache HttpClient。由于OkHttp3较好的支持 SPDY协议(SPDY是Google开发的基于TCP的传输层协议,用以最小化网络延迟,提升网络速度,优化用户的网络使用体验。),从Android4.4开始,google已经开始将Android源码中的 HttpURLConnection 请求类使用OkHttp进行了替换。也就是说,对于Android 移动端APP开发来说,OkHttp3 组件,是基础的开发组件之一。

LoadBalancerFeignClient 类

  LoadBalancerFeignClient 内部使用了 Ribben 客户端负载均衡技术完成URL请求处理。在原理上,简单的使用了delegate包装代理模式:Ribben负载均衡组件计算出合适的服务端server之后,由内部包装 delegate 代理客户端完成到服务端server的HTTP请求;所封装的 delegate 客户端代理实例的类型,可以是 Client.Default 默认客户端,也可以是 ApacheHttpClient 客户端类或OkHttpClient 高性能客户端类,还可以其他的定制的feign.Client 客户端实现类型。

Feign 远程调用的执行流程

  由于Feign远程调用接口的JDK Proxy实例的InvokeHandler调用处理器有多种,导致Feign远程调用的执行流程,也稍微有所区别,但是远程调用执行流程的主要步骤,是一致的。这里主要介绍两类JDK Proxy实例的InvokeHandler调用处理器相关的远程调用执行流程:

(1)与 默认的调用处理器 FeignInvocationHandler 相关的远程调用执行流程;
(2)与 Hystrix调用处理器 HystrixInvocationHandler 相关的远程调用执行流程。

在这里插入图片描述

总结

整体的远程调用执行流程,大致分为4步,具体如下:

第1步:通过Spring IOC 容器实例,装配代理实例,然后进行远程调用。

  上面说到,Feign在启动时,会为加上了@FeignClient注解的所有远程接口创建一个本地JDK Proxy代理实例,并注册到Spring IOC容器。在这里,暂且将这个Proxy代理实例,叫做 DemoClientProxy,稍后,会详细介绍这个Proxy代理实例的具体创建过程。
  然后,在UserController 调用代码中,通过@Resource注解,按照类型或者名称进行匹配,当然也可以使用其他方式。由于使用的SpringIOC所以还可以使用@Autowire 。从Spring IOC容器找到这个代理实例,并且装配给@Resource注解所在的成员变量。也就是之前提到的被使用的FeginDemo
  例如,在需要代理进行hello()远程调用时,直接通过 feginDemo 成员变量,调用JDK Proxy动态代理实例的hello()方法。
第2步:执行 InvokeHandler 调用处理器的invoke(…)方法
  前面讲到,JDK Proxy动态代理实例的真正的方法调用过程,具体是通过 InvokeHandler 调用处理器完成的。故,这里的DemoClientProxy代理实例,会调用到默认的FeignInvocationHandler 调用处理器实例的invoke(…)方法。
  通过前面 FeignInvocationHandler 调用处理器的详细介绍,已经知道,默认的调用处理器 FeignInvocationHandle,内部保持了一个远程调用方法实例和方法处理器的一个Key-Value键值对Map映射。FeignInvocationHandle 在其invoke(…)方法中,会根据Java反射的方法实例,在dispatch 映射对象中,找到对应的 MethodHandler 方法处理器,然后由后者完成实际的HTTP请求和结果的处理。
  所以在第2步中,FeignInvocationHandle 会从自己的 dispatch映射中,找到hello()方法所对应的MethodHandler 方法处理器,然后调用其 invoke(…)方法。

第3步:执行 MethodHandler 方法处理器的invoke(…)方法

  通过前面关于 MethodHandler 方法处理器的非常详细的组件介绍,大家都知道,feign默认的方法处理器为 SynchronousMethodHandler,其invoke(…)方法主要是通过内部成员feign客户端成员 client,完成远程 URL 请求执行和获取远程结果。
feign.Client 客户端有多种类型,不同的类型,完成URL请求处理的具体方式不同。

第4步:通过 feign.Client 客户端成员,完成远程 URL 请求执行和获取远程结果

  如果MethodHandler方法处理器实例中的client客户端,是默认的 feign.Client.Default 实现类性,则使用JDK自带的HttpURLConnnection类,完成远程 URL 请求执行和获取远程结果。

  如果MethodHandler方法处理器实例中的client客户端,是 ApacheHttpClient 客户端实现类性,则使用 Apache httpclient 开源组件,完成远程 URL 请求执行和获取远程结果。

通过以上四步,应该可以清晰的了解到了 SpringCloud中的 feign 远程调用执行流程和运行机制。

补充
  实际上,为了简明扼要的介绍清楚默认的调用流程,上面的流程,实际上省略了一个步骤:第3步,实际可以分为两小步。为啥呢?
  SynchronousMethodHandler 并不是直接完成远程URL的请求,而是通过负载均衡机制,定位到合适的远程server 服务器,然后再完成真正的远程URL请求。
  换句话说,SynchronousMethodHandler实例的client成员,其实际不是feign.Client.Default类型,而是 LoadBalancerFeignClient 客户端负载均衡类型。
   因此,上面的第3步,如果进一步细分话,大致如下:

  • (1)首先通过 SynchronousMethodHandler 内部的client实例,实质为负责客户端负载均衡LoadBalancerFeignClient 实例,首先查找到远程的 server 服务端;
  • (2) 然后再由LoadBalancerFeignClient 实例内部包装的feign.Client.Default 内部类实例,去请求server端服务器,完成URL请求处理。

  最后,说明下,默认的与 FeignInvocationHandler 相关的远程调用执行流程,在运行机制以及调用性能上,满足不了生产环境的要求,为啥呢? 大致原因有以下两点:
(1) 没有远程调用过程中的熔断监测和恢复机制;
(2) 也没有用到高性能的HTTP连接池技术。

  后续分享中,介绍用到熔断监测和恢复机制 Hystrix 技术的远程调用执行流程,在该流程中,远程接口的JDK Proxy动态代理实例所使用的调用处理器,叫做 HystrixInvocationHandler 调用处理器。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章