IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> RPC client之OpenFeign -> 正文阅读

[网络协议]RPC client之OpenFeign



前言

本文我们一起看看SpringCloud中的OpenFeign组件,探讨下其整体结构和带给我的启发。内容涵盖启用Feign,创建代理对象,到最终响应处理的整个过程。


一、啥是OpenFeign

OpenFeign是是一个基于Http协议的RPC组件,简化在基于SpringCloud微服务环境下完成服务间调用的开发。那跟Feign有啥区别呢? 早期SpringCloud版本下的RPC组件是Feign,后来停止更新后退出了OpenFeign。此外相比Feign,OpenFeign更加open,支持处理SpringMVC中的@RequestMapping注解。目前项目,大家一般都用的大多是OpenFeign。

二、启用FeignClient

1. Maven依赖

别问我为啥不贴Gradle,就是不会用,因为Maven用顺手了也就没再折腾。

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

2. 代码声明

关键代码就是这个 @EnableFeignClients(basePackages = {“com.test.spi”}), 到这里就启用了OpenFeign。要问到底是咋启用的,说明小伙你很不错,别着急往后看。

@Slf4j
@EnableFeignClients(basePackages = {"com.test.spi"})
@SpringBootApplication
public class TestApplication {
     public static void main(String[] args) {
     	SpringApplication.run(TestApplication.class, args);
     }
}

3. 背后的秘密

首先看@EnableFeignClients到底是咋定义的?

其中最关键的是@Import。如果你看过前面写的Spring系列,应该知道有个叫ConfigurationClassParser的类,其中processImports方法完成了对@Import注解的处理,创建一个ImportBeanDefinitionRegistrar接口类的实例。

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

}

ImportBeanDefinitionRegistrar接口内部提供了register方法来完成BeanDefinition的注册。因此FeignClientsRegistrar也实现了该接口;

class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
   @Override
	public void registerBeanDefinitions(AnnotationMetadata metadata,           BeanDefinitionRegistry registry) {
		registerDefaultConfiguration(metadata, registry);
		registerFeignClients(metadata, registry);
	}
}

这里我们可以看到其中注册了DefaultConfiguration和FeginClient对应的BeanDefinition。咱们顺着这条线继续。

private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata,
			Map<String, Object> attributes) {
		String className = annotationMetadata.getClassName();
		Class clazz = ClassUtils.resolveClassName(className, null);
		ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory
				? (ConfigurableBeanFactory) registry : null;
		String contextId = getContextId(beanFactory, attributes);
		String name = getName(attributes);
		
		FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();
		factoryBean.setBeanFactory(beanFactory);
		factoryBean.setName(name);
		factoryBean.setContextId(contextId);
		factoryBean.setType(clazz);
		factoryBean.setRefreshableClient(isClientRefreshEnabled());
		BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> {
			factoryBean.setUrl(getUrl(beanFactory, attributes));
			factoryBean.setPath(getPath(beanFactory, attributes));
			factoryBean.setDecode404(Boolean.parseBoolean(String.valueOf(attributes.get("decode404"))));
			Object fallback = attributes.get("fallback");
			if (fallback != null) {
				factoryBean.setFallback(fallback instanceof Class ? (Class<?>) fallback
						: ClassUtils.resolveClassName(fallback.toString(), null));
			}
			Object fallbackFactory = attributes.get("fallbackFactory");
			if (fallbackFactory != null) {
				factoryBean.setFallbackFactory(fallbackFactory instanceof Class ? (Class<?>) fallbackFactory
						: ClassUtils.resolveClassName(fallbackFactory.toString(), null));
			}
			return factoryBean.getObject();
		});
		definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
		definition.setLazyInit(true);
		validate(attributes);

		AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
		beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
		beanDefinition.setAttribute("feignClientsRegistrarFactoryBean", factoryBean);

		// has a default, won't be null
		boolean primary = (Boolean) attributes.get("primary");

		beanDefinition.setPrimary(primary);

		String[] qualifiers = getQualifiers(attributes);
		if (ObjectUtils.isEmpty(qualifiers)) {
			qualifiers = new String[] { contextId + "FeignClient" };
		}

		BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers);
		BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);

		registerOptionsBeanDefinition(registry, contextId);
	}

到这里我们看到了熟悉的手法,那就是FactoryBean,FeignClientFactoryBean,其中设置了各种配置,获取实例的方法在factory.getObject()。在下一节,咱们详细看看getObject方法。

三、创建代理对象

1. 从Client开始

@Override
	public Object getObject() {
		return getTarget();
	}

	/**
	 * @param <T> the target type of the Feign client
	 * @return a {@link Feign} client created with the specified data and the context
	 * information
	 */
	<T> T getTarget() {
		FeignContext context = beanFactory != null ? beanFactory.getBean(FeignContext.class)
				: applicationContext.getBean(FeignContext.class);
		Feign.Builder builder = feign(context);

		if (!StringUtils.hasText(url)) {

			if (LOG.isInfoEnabled()) {
				LOG.info("For '" + name + "' URL not provided. Will try picking an instance via load-balancing.");
			}
			if (!name.startsWith("http")) {
				url = "http://" + name;
			}
			else {
				url = name;
			}
			url += cleanPath();
			return (T) loadBalance(builder, context, new HardCodedTarget<>(type, name, url));
		}
		if (StringUtils.hasText(url) && !url.startsWith("http")) {
			url = "http://" + url;
		}
		String url = this.url + cleanPath();
		Client client = getOptional(context, Client.class);
		if (client != null) {
			if (client instanceof FeignBlockingLoadBalancerClient) {
				// not load balancing because we have a url,
				// but Spring Cloud LoadBalancer is on the classpath, so unwrap
				client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
			}
			if (client instanceof RetryableFeignBlockingLoadBalancerClient) {
				// not load balancing because we have a url,
				// but Spring Cloud LoadBalancer is on the classpath, so unwrap
				client = ((RetryableFeignBlockingLoadBalancerClient) client).getDelegate();
			}
			builder.client(client);
		}

		applyBuildCustomizers(context, builder);

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

从代码里可以看到,没有配置url则进入FeignBlockingLoadBalancerClient,此处的URL最终需要从注册中心里获取。如果配置了url,一般来说是网关的url,负载均衡由网关完成。
此外,如果自己在applicationContext中注册了FeignBlockingLoadBalancerClient或者RetryableFeignBlockingLoadBalancerClient,框架也会自动识别并使用已提供的client。作为一个框架, 不能强制要求用户用什么(面向抽象),还得考虑用户可能用什么(支持上下文感知),支持替换(松耦合)。至此,我明白自己写不来框架是有原因的。

2. 最爱的Customizer

再说这个 applyBuildCustomizers, 允许整一堆的customizer,框架层面专门定义了一个接口FeignBuilderCustomer,灵活性不要不要的。

private void applyBuildCustomizers(FeignContext context, Feign.Builder builder) {
		Map<String, FeignBuilderCustomizer> customizerMap = context.getInstances(contextId,
				FeignBuilderCustomizer.class);

		if (customizerMap != null) {
			customizerMap.values().stream().sorted(AnnotationAwareOrderComparator.INSTANCE)
					.forEach(feignBuilderCustomizer -> feignBuilderCustomizer.customize(builder));
		}
		additionalCustomizers.forEach(customizer -> customizer.customize(builder));
	}

3. Targeter接着浪

Targeter targeter = get(context, Targeter.class);

虽然只有1行代码,但从逻辑上说完成了上下文感知,也就是从上下文中获取Targeter对象。代码中直接可以找到的有2种,DefaultTargeter 和 FeignCircuitBreakerTargeter。后者从名字可以看出是一个支持熔断的targeter。但是Targeter从哪里来呢?

这个问题严格说之前没写过,算是个盲区。首先我们找到了创建实例的地方。结尾的几个字母说明了问题AutoConfiguration。这就得说说SpringBoot的AutoConfiguration机制了。 所以这玩意是在某个spring.factories中配置了,进而导致该Configuration中的Bean被自动装配到了容器中。

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Feign.class)
@EnableConfigurationProperties({ FeignClientProperties.class, FeignHttpClientProperties.class,
		FeignEncoderProperties.class })
public class FeignAutoConfiguration {

}

有了这个思路,直接去找jar中的META-INF文件夹。最终在
spring-cloud-openfeign-core-3.1.3.jar中的META-INF/spring.factories中找到如下结果

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.openfeign.hateoas.FeignHalAutoConfiguration,\
org.springframework.cloud.openfeign.FeignAutoConfiguration,\
org.springframework.cloud.openfeign.encoding.FeignAcceptGzipEncodingAutoConfiguration,\
org.springframework.cloud.openfeign.encoding.FeignContentGzipEncodingAutoConfiguration,\
org.springframework.cloud.openfeign.loadbalancer.FeignLoadBalancerAutoConfiguration

到这里,Targeter对象算是找到了。注意到这里,我们仅仅是流程上知道了什么对象在哪里创建,具体的创建细节我们没有讨论。毕竟一开始不能太深,否则容易沉沦于细节迷失方向。今天咱们的重点是,全局视角,流程打通。

4. 最终实例化

接下来以DefaultTargeter为例看看咱们可用的Feign对象具体是咋创建出来的。

DefaultTargeter
从这里可以看到实际调用的是FeignBuilder对象的target方法

class DefaultTargeter implements Targeter {

	@Override
	public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
			Target.HardCodedTarget<T> target) {
		return feign.target(target);
	}

}

Feign.Builder

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

    public Feign build() {
      Client client = Capability.enrich(this.client, capabilities);
      Retryer retryer = Capability.enrich(this.retryer, capabilities);
      List<RequestInterceptor> requestInterceptors = this.requestInterceptors.stream()
          .map(ri -> Capability.enrich(ri, capabilities))
          .collect(Collectors.toList());
      Logger logger = Capability.enrich(this.logger, capabilities);
      Contract contract = Capability.enrich(this.contract, capabilities);
      Options options = Capability.enrich(this.options, capabilities);
      Encoder encoder = Capability.enrich(this.encoder, capabilities);
      Decoder decoder = Capability.enrich(this.decoder, capabilities);
      InvocationHandlerFactory invocationHandlerFactory =
          Capability.enrich(this.invocationHandlerFactory, capabilities);
      QueryMapEncoder queryMapEncoder = Capability.enrich(this.queryMapEncoder, capabilities);

      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);
      return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
    }

到这里可以看到最终需要进入ReflectiveFeign#newInstance()

ReflectiveFeign

public <T> T newInstance(Target<T> target) {
    Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
    Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
    List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();

    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)));
      }
    }
    InvocationHandler handler = factory.create(target, methodToHandler);
    T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
        new Class<?>[] {target.type()}, handler);

    for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
      defaultMethodHandler.bindTo(proxy);
    }
    return proxy;
  }

到这里就完成了Feign的实例化。整个过程涉及的关键对象有Client,Customizer和Targeter。有个FeignClient的代理对象,终于可以进入实际的RPC环节了。

四、RPC过程

在开始之前先聊几句背景知识,基于Feign的RPC实际上是一次基于Http协议的网络通信。从工程实现的角度,底层必然依赖成熟的HTTP通信框架(Apache HttpClien、okhttp)等。因此,此处在架构要分层设计,OpenFeign 应该做http协议和底层通信之外的其他事情。这就包括定义和实现方法和参数注解标准,最终将调用转换为一次网络请求,并将通信层提供的响应报文转换为目标类型的返回值。在OpenFeign中,这些都是由MethodHandler完成。

1. 构造MethodHandler

OpenFeign通过Contract来获取每个FeignClient中方法的MethodMetadata,并基于此创建对应RequestTemplate.Factory,进而创建MethodHandler。最终将method.configKey和MethodHandler作为键值对存储在Map中。细节代码如下:

ReflectiveFeign.ParseHandlersByName

static final class ParseHandlersByName {

    private final Contract contract;
    private final Options options;
    private final Encoder encoder;
    private final Decoder decoder;
    private final ErrorDecoder errorDecoder;
    private final QueryMapEncoder queryMapEncoder;
    private final SynchronousMethodHandler.Factory factory;

    ParseHandlersByName(
        Contract contract,
        Options options,
        Encoder encoder,
        Decoder decoder,
        QueryMapEncoder queryMapEncoder,
        ErrorDecoder errorDecoder,
        SynchronousMethodHandler.Factory factory) {
      this.contract = contract;
      this.options = options;
      this.factory = factory;
      this.errorDecoder = errorDecoder;
      this.queryMapEncoder = queryMapEncoder;
      this.encoder = checkNotNull(encoder, "encoder");
      this.decoder = checkNotNull(decoder, "decoder");
    }

    public Map<String, MethodHandler> apply(Target target) {
      List<MethodMetadata> metadata = contract.parseAndValidateMetadata(target.type());
      Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>();
      for (MethodMetadata md : metadata) {
        BuildTemplateByResolvingArgs buildTemplate;
        if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) {
          buildTemplate =
              new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
        } else if (md.bodyIndex() != null || md.alwaysEncodeBody()) {
          buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
        } else {
          buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder, target);
        }
        if (md.isIgnored()) {
          result.put(md.configKey(), args -> {
            throw new IllegalStateException(md.configKey() + " is not a method handled by feign");
          });
        } else {
          result.put(md.configKey(),
              factory.create(target, md, buildTemplate, options, decoder, errorDecoder));
        }
      }
      return result;
    }
  }

2. 执行RPC

显然首先得知道需要哪个MethodHandler,这个dispatch过程在InvocationHandler中完成,为了不与前面实例构造过程重复,该细节在此处体现。下面代码为创建Proxy对象时,使用的FeignInvocationHandler定义。

FeignInvocationHandler

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 {
      // 此处忽略部分非关键代码 
      // 最关键的一行
      return dispatch.get(method).invoke(args);
    }

    // 此处忽略部分非关键代码
  }

接下来以默认的MethodHandler实现类SynchronousMethodHandler进行介绍。

SynchronousMethodHandler中比较关键的代码如下

@Override
  public Object invoke(Object[] argv) throws Throwable {
    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) {
          Throwable cause = th.getCause();
          if (propagationPolicy == UNWRAP && cause != null) {
            throw cause;
          } else {
            throw th;
          }
        }
        if (logLevel != Logger.Level.NONE) {
          logger.logRetry(metadata.configKey(), logLevel);
        }
        continue;
      }
    }
  }

以上为整个调用过程,先构造RequestTemplate,然后执行executeAndDecode,接下来根据Retriery的定义和响应结果确定重试逻辑。返回的Response其body要么是原始字节,要么是结构化的字符串(如JSON或者XML)通过Decoder对象转换为我们需要的对象。

至此,一个超精简版的RPC过程结束,后续会尝试对OpenFeign提供的扩展进行更进一步的分享。


总结

以上就是今天要讲的内容,本文介绍了OpenFeign在SpringBoot环境下如何启用@FeignClient注解,对应的BeanDefinition是如何注册的,BeanInstance是如何生成的,并简单分析了基于Feign的RPC调用过程。从内容上来看,基本覆盖了FeignClient的骨架内容。实际上,每个步骤在横向环节上,也有诸多值得注意的细节,姑且留给后续分享,希望本文能帮助你对OpenFeign有一个初步的了解,对RPC框架的内部有些参考。

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2022-09-24 21:27:49  更:2022-09-24 21:28:58 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 13:09:12-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码