再学一下Feign的原理

时间:2021-09-19 01:14:00

简介

Feign是Spring Cloud Netflix组件中的一个轻量级Restful的HTTP服务客户端,它简化了服务间调用的方式。
Feign是一个声明式的web service客户端.它的出现使开发web service客户端变得更简单.使用Feign只需要创建一个接口加上对应的注解, 比如@FeignClient注解。
Feign是一种声明式、模板化的HTTP客户端。在Spring Cloud中使用Fegin,可以做到使用HTTP请求访问远程服务,就像调用本地方法一样,开发者完全无感知,更感知不到HTTP请求。
Feign的特性:

  • 可插拔的注解支持,包含Feign注解
  • 支持可插拔的HTTP编码器和解码器
  • 支持Hystrix和它的Fallback
  • 支持Ribbion的负载均衡
  • 支持HTTP请求和响应的压缩

快速入门

引入jar

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

编码

@FeignClient(
    url = "/test",
    name = "test-api"
)
public interface TestClient {
    @PostMapping(
        value = {"/create"}
    )
    void create(@RequestBody User user);
  }

启动注解扫描

@Configuration
@EnableFeignClients(basePackages = {"com.test"})

添加注解,启动FeignClient注解扫描,跟我们平时使用ComponentScan的道理都是相似的。

使用

    @Resource
    private TestClient testClient;
    
    public void create() {
        User user = new User();
        user.setName("test");
        testClient.create(user);
    }

完成这些操作之后,我们就可以正常的调用其他服务了,是不是很简单,很容易上手?
那么它到底是如何实现的呢?它都帮我们做了什么事情?接下来的内容才是我们需要了解和掌握的内容。

注册

在我们的业务代码中,我们只是定义了一个添加@FeignClient注解的接口,它是通过JDK的动态代理帮我们创建了接口的代理类,通过代理类完成HTTP的远程调用操作。
在上面的demo中,我们说了,想要调用其他服务,必须添加@EnableFeignClients注解,这个注解就是要扫描我们的代码所有标注了@FeignClient的接口,然后添加到容器当中。

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

	String[] value() default {};

	String[] basePackages() default {};

	Class<?>[] basePackageClasses() default {};

	Class<?>[] defaultConfiguration() default {};

	Class<?>[] clients() default {};

}

我们可以自己定义我们要扫描的包路径,加载所有的@FeignClient标识的所有接口。
在这个注解的上面还有一个注解,@Import,这个我想大家应该都知道,是为了导入其他的bean到容器当中,那么Feign的主要加载、扫描、添加注册信息等操作,都是在FeignClientsRegistrar中完成的。

FeignClientsRegistrar

spring框架已经成为了我们日常开发的企业级框架,或者说已经离不开spring的支持了。
FeignClientsRegistrar的处理逻辑就是在调用ApplicationContext的refresh方法的时候,需要初始化一些BeanPostProcessor,在这个过程中,需要处理一些配置类,比如添加了@Configuration注解的类,那么我们使用的Feign开启的时候,就需要这个配置注解。
FeignClientsRegistrar实现了ImportBeanDefinitionRegistrar接口,那么在容器启动时候就会调用它的registerBeanDefinitions方法,做一些前置处理,通常是把一些bean定义信息注册到容器当中,便于后面初始化这个bean。
现在我们主要看一下FeignClientsRegistrar的registerBeanDefinitions方法

registerFeignClients

public void registerFeignClients(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {
		Set<String> basePackages;

		Map<String, Object> attrs = metadata
				.getAnnotationAttributes(EnableFeignClients.class.getName());
		AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
				FeignClient.class);
		。。。省略 //主要是为了获取扫描的包路径,也就是basePackages

		for (String basePackage : basePackages) {
			//查找添加了@FeignClient注解的接口
			Set<BeanDefinition> candidateComponents = scanner
					.findCandidateComponents(basePackage);
			for (BeanDefinition candidateComponent : candidateComponents) {
				if (candidateComponent instanceof AnnotatedBeanDefinition) {
					// verify annotated class is an interface
					AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
					AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
					Assert.isTrue(annotationMetadata.isInterface(),
							"@FeignClient can only be specified on an interface");

					Map<String, Object> attributes = annotationMetadata
							.getAnnotationAttributes(
									FeignClient.class.getCanonicalName());

					String name = getClientName(attributes);
					//添加FeignClientSpecification的bean定义信息
					registerClientConfiguration(registry, name,
							attributes.get("configuration"));
					//添加FeignClientFactoryBean的bean定义信息
					//注册的是FactoryBean信息,那么在初始化这个bean的时候,就会调用到getObject方法,代理的逻辑也是在这里面处理的
					registerFeignClient(registry, annotationMetadata, attributes);
				}
			}
		}
	}

registerFeignClient

private void registerFeignClient(BeanDefinitionRegistry registry,
			AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
		String className = annotationMetadata.getClassName();
		//feignclient注解的bean是FeignClientFactoryBean,一个FactoryBean
		BeanDefinitionBuilder definition = BeanDefinitionBuilder
				.genericBeanDefinition(FeignClientFactoryBean.class);
		validate(attributes);
		//设置url路径
		definition.addPropertyValue("url", getUrl(attributes));
		//urlpath
		definition.addPropertyValue("path", getPath(attributes));
		String name = getName(attributes);
		//设置服务名
		definition.addPropertyValue("name", name);
		String contextId = getContextId(attributes);
		//beanname的替代名
		definition.addPropertyValue("contextId", contextId);
		definition.addPropertyValue("type", className);
		definition.addPropertyValue("decode404", attributes.get("decode404"));
		//设置失败机制
		definition.addPropertyValue("fallback", attributes.get("fallback"));
		definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
		definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
		//设置bean的别名
		String alias = contextId + "FeignClient";
		//注解bean定义信息
		BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
	}

到这里呢, 所有的添加@FeignClient注解的接口已经把bean信息注册到了容器中,等待的就是bean的初始化动作.

bean初始化

在bean的初始化中,由于bean之间的相互依赖,一定会调用FeignClientFactoryBean的getObject方法.
再学一下Feign的原理
继续跟踪创建代理类的代码

Targeter targeter = get(context, Targeter.class);
		return (T) targeter.target(this, builder, context,
				new HardCodedTarget<>(type, name, url));

type就是我们定义的Feign接口, name和url就是注解中定义的属性.
接着就会调用到FeignBuilder的target方法

public <T> T target(Target<T> target) {
      return build().newInstance(target);
    }

调用build方法封装ReflectiveFeign数据,ReflectiveFeign继承了Feign,后面我们调用FeignClient的时候,就是调用它的内部类FeignInvocationHandler的invoke方法

public Feign build() {
      Client client = Capability.enrich(this.client, capabilities);
      Retryer retryer = Capability.enrich(this.retryer, capabilities);
      //Feign调用链路的拦截器
      List<RequestInterceptor> requestInterceptors = this.requestInterceptors.stream()
          .map(ri -> Capability.enrich(ri, capabilities))
          .collect(Collectors.toList());
      //... 省略
	  //设置SynchronousMethodHandler的内部工厂数据
      SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
          new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
              logLevel, decode404, closeAfterDecode, propagationPolicy, forceDecoding);
      ParseHandlersByName handlersByName =
          new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,
              errorDecoder, synchronousMethodHandlerFactory);
      //创建ReflectiveFeign
      return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
    }

接着调用ReflectiveFeign的newInstance方法,Feign接口的代理类的生成就是在这里产生的,每个方法对应的MethodHandler也是在这里注册好的.

public <T> T newInstance(Target<T> target) {
    //这里就会遍历元数据信息,生成每个方法名对应的MethodHandler,这里的MethodHandler就是通过SynchronousMethodHandler.Factory
    //创建的SynchronousMethodHandler
    //MethodHandler是Fegin定义个的方法处理器
    Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
    //method -> MethodHandler
    Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
    List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
	//遍历Feign接口中定义的所有方法
    for (Method method : target.type().getMethods()) {
      if (method.getDeclaringClass() == Object.class) {
        continue;
      } else if (Util.isDefault(method)) {
        DefaultMethodHandler handler = new DefaultMethodHandler(method);
        defaultMethodHandlers.add(handler);
        methodToHandler.put(method, handler);
      } else {
        methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
      }
    }
    //一个Feign接口创建一个对应的InvocationHandler
    InvocationHandler handler = factory.create(target, methodToHandler);
    //通过jdk动态代理创建代理对象
    T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
        new Class<?>[] {target.type()}, handler);
	//为默认的方法处理器绑定创建的代理对象
    for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
      defaultMethodHandler.bindTo(proxy);
    }
    return proxy;
  }

InvocationHandler 这里返回的对象还是ReflectiveFeign的内部类FeignInvocationHandler

  static class FeignInvocationHandler implements InvocationHandler {

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

首先它实现了jdk代理的InvocationHandler 接口,包含2个成员变量,一个目标类, 一个是解析后的方法到MethodHandler的映射关系,便于后面调用的时候,通过dispatch查询对应方法的MethodHandler.
到这里Feign接口的相关注册和初始化动作就结束了.

消费

再学一下Feign的原理
懒的画图了,从网上复制了一份, 基本上可以完整的概括了FeignClient的调用流程了.

FeignInvocationHandler

了解jdk动态代理的都知道, 想要实现动态代理,首先要实现InvocationHandler接口, 调用的时候就会执行invoke方法,接下来我们仔细看下invoke方法的执行流程.

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      //... 省略
      //通过初始化好的dispatch,根据对应调用的method查找对应的methodHandler, 从上述的分析过程中,我们看到使用的是
      //SynchronousMethodHandler
      return dispatch.get(method).invoke(args);
    }

SynchronousMethodHandler

追踪到对应的handler上看下

public Object invoke(Object[] argv) throws Throwable {
	//封装RequestTemplate 对象, 它包含http相关请求的url,body,method,等等一下参数
    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) {
          //...
        }
        //....
        continue;
      }
    }
  }

接下来的就是使用Http客户端调用远程服务

  Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
    Request request = targetRequest(template);

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

    Response response;
    long start = System.nanoTime();
    try {
      response = client.execute(request, options);
      // ensure the request is set. TODO: remove in Feign 12
      response = response.toBuilder()
          .request(request)
          .requestTemplate(template)
          .build();
    } catch (IOException e) {
      if (logLevel != Logger.Level.NONE) {
        logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
      }
      throw errorExecuting(request, e);
    }
   //...
  }

http客户端也有很多的实现
再学一下Feign的原理
再学一下Feign的原理
其中Okhttp和httpclient是自动装配的, 我们只需要配置一下,就可以启用对应的http客户端,缺省配置的情况下,默认使用ApacheHttpClient.

OkHttp

public feign.Response execute(feign.Request input, feign.Request.Options options)
      throws IOException {
    okhttp3.OkHttpClient requestScoped;
    if (delegate.connectTimeoutMillis() != options.connectTimeoutMillis()
        || delegate.readTimeoutMillis() != options.readTimeoutMillis()
        || delegate.followRedirects() != options.isFollowRedirects()) {
      requestScoped = delegate.newBuilder()
          .connectTimeout(options.connectTimeoutMillis(), TimeUnit.MILLISECONDS)
          .readTimeout(options.readTimeoutMillis(), TimeUnit.MILLISECONDS)
          .followRedirects(options.isFollowRedirects())
          .build();
    } else {
      requestScoped = delegate;
    }
    Request request = toOkHttpRequest(input);
    Response response = requestScoped.newCall(request).execute();
    return toFeignResponse(response, input).toBuilder().request(input).build();
  }

这里就是OkHttp的远程服务调用代码, 没有太多的逻辑.

重试

重试默认使用的Retryer.Default
我们先看下它的默认参数

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

最大尝试次数是5次, 最大时间间隔是1s, 100ms的重试时间间隔, 每次的interval是动态计算下次尝试需要等待的时间

public void continueOrPropagate(RetryableException e) {
      //超过最大重试次数直接抛出异常
      if (attempt++ >= maxAttempts) {
        throw e;
      }

      long interval;
      if (e.retryAfter() != null) {
        interval = e.retryAfter().getTime() - currentTimeMillis();
        if (interval > maxPeriod) {
          interval = maxPeriod;
        }
        if (interval < 0) {
          return;
        }
      } else {
       //根据时间间隔和重试次数计算下次重试需要等待的时间
        interval = nextMaxInterval();
      }
      try {
        Thread.sleep(interval);
      } catch (InterruptedException ignored) {
        Thread.currentThread().interrupt();
        throw e;
      }
      sleptForMillis += interval;
    }

Feign的初始化和调用基本原来,先分享到这里,下一期分享一下和ribbon的结合,实现负载均衡.