• 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.cloudgroupId>
        <artifactId>spring-cloud-starter-openfeignartifactId>
        <version>3.1.3version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    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);
         }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    3. 背后的秘密

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

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

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Documented
    @Import(FeignClientsRegistrar.class)
    public @interface EnableFeignClients {
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

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

    class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
       @Override
    	public void registerBeanDefinitions(AnnotationMetadata metadata,           BeanDefinitionRegistry registry) {
    		registerDefaultConfiguration(metadata, registry);
    		registerFeignClients(metadata, registry);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这里我们可以看到其中注册了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);
    	}
    
    • 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
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

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

    三、创建代理对象

    1. 从Client开始

    @Override
    	public Object getObject() {
    		return getTarget();
    	}
    
    	/**
    	 * @param  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));
    	}
    
    • 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
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    从代码里可以看到,没有配置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));
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    3. Targeter接着浪

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

    虽然只有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 {
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    有了这个思路,直接去找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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    到这里,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);
    	}
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    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);
        }
    
    • 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

    到这里可以看到最终需要进入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;
      }
    
    • 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

    到这里就完成了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;
        }
      }
    
    • 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
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    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);
        }
    
        // 此处忽略部分非关键代码
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    接下来以默认的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;
          }
        }
      }
    
    • 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

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

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


    总结

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

  • 相关阅读:
    06_android gradle中的build依赖项简介(笔记篇)
    1161 Merging Linked Lists – PAT甲级真题
    Android 多线程、线程池
    ref和reactive
    Linux centos系统中添加磁盘
    一篇文章带你了解网页框架——Vue简单入门
    记录一下 cuda、torchinfo、gpustat 相关知识
    从零搭建开发脚手架 Logback使用janino实现条件判断
    [Vue项目实战]尚品汇 -- 初始化项目以及项目的配置与分析
    前端JavaScript入门到精通,javascript核心进阶ES6语法、API、js高级等基础知识和实战 —— JS进阶(三)
  • 原文地址:https://blog.csdn.net/weilaizhixing007/article/details/126686186