再学一下Feign的原理

简介

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方法.
在这里插入图片描述
继续跟踪创建代理类的代码

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接口的相关注册和初始化动作就结束了.

消费

在这里插入图片描述
懒的画图了,从网上复制了一份, 基本上可以完整的概括了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客户端也有很多的实现
在这里插入图片描述
在这里插入图片描述
其中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的结合,实现负载均衡.

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
SpringCloud微服务课程说明 1 网站架构演变过程 1 传统架构 1 分布式架构 1 SOA架构 1 微服务架构 2 微服务架构产生的原因 2 漫谈微服务架构 2 什么是微服务 2 微服务架构特征 3 微服务架构如何拆分 3 微服务架构与SOA架构区别 3 SpringCloud微服务框架 3 为什么选择SpringCloud 4 SpringCloud简介 4 服务治理SpringCloud Eureka 5 什么是服务治理 5 服务注册与发现 5 搭建注册中心 6 常用注册中心框架 6 注册中心环境搭建 6 注册服务提供者 8 服务消费者 11 高可用注册中心 14 Eureka高可用原理 14 Eureka集群环境搭建 14 Eureka详解 17 使用Eureka闭源了闭源了怎么办? 21 使用Consul来替换Eureka 21 使用Zookeeper来替换Eureka 25 Consistency(一致性), 数据一致更新,所有数据变动都是同步的 Availability(可用性), 好的响应性能 Partition tolerance(分区容忍性) 可靠性 28 客户端负载均衡器 29 客户端负载均衡器 29 Ribbon与Nginx区别 29 RestTemplate 31 请求类型 31 负载均衡器源码分析 33 负载均衡器重试机制 33 服务保护机制SpringCloud Hystrix 33 微服务高可用技术 33 服务降级、熔断、限流概念 34 服务学崩效应 34 服务降级 34 服务熔断 35 服务隔离 35 服务限流 36 Hystrix简单介绍 36 Hystrix环境搭建 37 Maven依赖信息 37 开启Hystrix断路器 37 服务降级处理 38 Hystrix仪表盘 39 Turbine集群监控 39 声明式服务调用SpringCloud Feign 39 feign介绍 39 环境搭建 40 Maven依赖信息 40 feign客户端接口 40 feign继承特性 41 创建springcloud-2.0-parent 42 Ribbon配置 45 配置Feign客户端超时时间 45 Api网关服务SrpingCloud Zuul 45 网关分类 46 网关设计 46 网关框架 48 网关作用 49 网关与过滤器区别 49 Nginx与Zuul的区别 49 Zuul网关 49 动态网关 53 网关集群 55 分布式配置中心SrpingCloud config 57 SpringCloud分布式配置中心 57 Config架构 58 Git环境搭建 58 服务端详解 58 Maven依赖信息 58 application.yml配置 59 项目启动 60 客户端详解 60 Maven依赖信息 60 bootstrap.yml 61 读取配置文件 62 动态刷新数据 62 actuator端点刷新数据 63 Maven依赖信息 63 Bootstrap.xml新增 63 include: "*" 63 生效前提 63 private String itmayieduInfo; 63 当配置更改时,标有@RefreshScope的Bean将得到特殊处理来生效配置 64 手动刷新接口 64 分布式服务跟踪SpringCloud sleuth 64 什么是Sleuth 64 环境搭建 64 构建server-zipkin 64 会员服务 65 订单服务 65 Swagger2API接口管理 65 课题引入 65 传统的API文档编写存在以下几个痛点: 66 Swagger具有以下优点 66 Swagg er 2.0 集成配置 66 Maven依赖信息 66 SwaggerConfig 68 Zull整合Swagger管理微服务所有API 68 会员和订单引入Maven依赖 68 ZuulGateway网关 69

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值