• Feign 从注册到调用原理分析


    在这里插入图片描述

    在这里插入图片描述本文主要讲述 Feign 是如何注册到 Spring 容器、融合 Ribbon进行负载均衡、进行 RPC 调用。

    简单提一下项目中一般都是如何使用 Feign 的,首先声明一个@FeignClient定义 RPC 调用方法,然后像调用本地方法一样,调用远程服务的方法

    // 定义 FeignClient 
    @FeignClient(value = "service-order",path = "/order") 
    public interface OrderFeignService { 
    
    	@RequestMapping("/findOrderByUserId/{userId}") 
    	R findOrderByUserId(@PathVariable("userId") Integer userId); 
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    // 调用远程服务 
    @Autowired 
    OrderFeignService orderFeignService; 
    
    R findOrderByUserId(@PathVariable("id") Integer id) { 
    	//feign调用 
    	R result = orderFeignService.findOrderByUserId(id); 
    	return result; 
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这样一来,我们省去了自己去配置 RestTemplate 或其他 HTTPClient 的麻烦,但是简单方便的同时你是否会有一些疑惑:

    我们使用@Autowired注入的 OrderFeignService,那么它一定是一个 Spring Bean,它是什么时候,如何被注入到 Spring 容器的?

    • Feign 是如何执行 findOrderByUserId()的?
    • @RequestMapping 是 SpringMVC 的注解呀,怎么在 Feign 中也会生效呢?
    • Feign 是如何整合 Ribbon 的?
    • Ribbon 是如何获取注册中心的服务的?
    • Ribbon 是如何进行负载均衡的?

    理解了上面的这些问题,我们也就明白了 Feign 是如何进行调用的,那么带着这些问题我们来一步步分析

    @FeignClient 注册

    @EnableFeignClients 注解

    根据 SpringBoot 自动装配的思想,先猜想下一定会有@Enablexxx ,然后再有@Import(xxx.class),来进行 Feign 的自动注入

    要使用 Feign 首先要

    1. 引入 Feign 的 Maven 依赖,接着一定要在启动类上添加注解 @EnableFeignClients
    2. @EnableFeignClients @Import(FeignClientsRegistrar.class)
    3. 注册 @FeignClient

    在这里插入图片描述

    在这里插入图片描述

    FeignClientsRegistrar.class

    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
            this.registerDefaultConfiguration(metadata, registry);
            this.registerFeignClients(metadata, registry);
        }
    
    • 1
    • 2
    • 3
    • 4

    1. 扫描并注册 FeignClient 为 BeanDefinition

    public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { 
    	// ...,省略了部分源码 
    	扫描所有 @FeignClient 标注的类 
    	Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName()); 
    	AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter( FeignClient.class); 
    	// ... 
    	for (String basePackage : basePackages) { 
    	// ... 
    	Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(FeignClient.class.getCanonicalName()); 
    	// ... 
    	// 注册 FeignClient 为 BeanDefinition 
    	registerFeignClient(registry, annotationMetadata, attributes); 
    } } } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    2. 将 FeignClient 包裹成 FeignClientFactoryBean

    private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, 
    Map<String, Object> attributes) { 
    
    	String className = annotationMetadata.getClassName(); 
    	// 将 FeignClient 包裹成 FeignClientFactoryBean 
    	BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class); // ... 一堆definition.addPropertyValue() 
    	definition.addPropertyValue("fallbackFactory",attributes.get("fallbackFactory")); definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); 
    	// ... 
    	// 注册 
    	FeignClientFactoryBean BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[] { alias }); 
    	BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); 
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    3. FeignClientFactoryBean#getObject(),生成FeignClient 的代理对象

    在这里插入图片描述

    public Object getObject() throws Exception { 
    	return getTarget(); 
    } 
    /** * 根据指定的数据和上下文信息,生成一个 FeignClient 的代理对象 
    Client */ <T> T getTarget() { 
    	FeignContext context = applicationContext.getBean(FeignContext.class); 
    	// ... 
    	Client client = getOptional(context, Client.class); 
    	// ... 
    	Targeter targeter = get(context, Targeter.class); 
    	return (T) targeter.target(this, builder, context, new HardCodedTarget<>(type, name, url)); 
    } 
    // 最终会调用生成一个 FeignInvocationHandler 的代理对象 
    public <T> T target(Target<T> target) { 
    	return build().newInstance(target); 
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述

    4.生成FeignClient 的动态代理 FeignInvocationHandler

    public <T> T newInstance(Target<T> target) { 
    	// 解析请求为 
    	MethodHandler Map<String, MethodHandler> nameToHandler = targetoHandlersByName.apply(target); 
    	Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();    
    	List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>(); 
    	// ... 代码省略 
    	// 生成动态代理 
    	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
    1. FeignInvocationHandler 执行 invoke ,一步步调用,最终会调用 Client 的 execute()方法,执行远程调用

    总结:

    被@FeignClient 标记的 Interface 会在开启了@EnableFeignClients 之后,被 Spring 扫描到容器中,并且生成一个 FeignInvocationHandler 的动态代理

    FeignClient 调用

    在这里插入图片描述
    上面我们已经分析完了FeignClient 是如何创建动态代理的,在我们按着FeignInvocationHandler 的 invoke() 方法往下分析之前,先看下上图中步骤 2,这一步就是 @RequestMapping如何在 Feign 中生效的

    回头看下生成动态代理过程中有段代码

     // 解析请求为 MethodHandler 
     Map<String, MethodHandler> nameToHandler = targetoHandlersByName.apply(target); 
    
    • 1
    • 2

    在这里插入图片描述

    这里完成了 Contract 对 SpringMVC 的解析

    public Map<String, MethodHandler> apply(Target key) {
     // 这就用contract完成了方法上的SpringMVC注解的转换 
     // FeignClient标注的Interface 的每一个方法都会被解析成MethodMetadata 
     // 对各种SpringMVC的注解进行解析,将解析出来的header,method,path,body,form param,返回值等等,
     //放入了MethodMetadata中 
     List<MethodMetadata> metadata = contract.parseAndValidatateMetadata(key.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); 
       	} else if (md.bodyIndex() != null) { 
       	buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder); 
       	} else { 
       	buildTemplate = new BuildTemplateByResolvingArgs(md); 
       	} 
    	 // 在这里就创建了SynchronousMethodHandler,key就是方法名
    	 // SynchronousMethodHandler就是所有的方法被代理后实际处理的处理器 
    	 result.put(md.configKey(), factory.create(key, 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

    所以我们可以看到,Feign 之所以能用 SpringMVC 注解是因为专门对这层注解做了解析。

    FeignInvocationHandler#invoke()

    FeignInvocationHandler#invoke() --> executeAndDecode(template, options)

    Object executeAndDecode(RequestTemplate template, Options options) throws Throwable { 
    	// 处理所有Interceptor: 
    	RequestInterceptor Request request = targetRequest(template); 
    	// ... 
    	Response response; 
    	try { 
    		// 执行Client#execute() 
    		response = client.execute(request, options); 
    		// ... 
    	} 
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这里插入图片描述

    LoadBalancerFeignClient#execute()

    public Response execute(Request request, Request.Options options) throws IOException { 
    	try { 
    		// ... 封装 request 成 ribbonRequest 
    		FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest( this.delegate, request, uriWithoutHost); 
    		// 配置 Client 
    		IClientConfig requestConfig = getClientConfig(options, clientName); 
    		return lbClient(clientName).executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse(); 
    	} 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插入图片描述

    lbClient(clientName) 整合 Ribbon 实例化出 FeignLoadBalancer

    lbClient(String clientName) --> this.lbClientFactory.create(clientName)

    在这里插入图片描述

    public FeignLoadBalancer create(String clientName) { 
    	// FeignLoadBalancer 是 Feign 提供的 
    	FeignLoadBalancer client = this.cache.get(clientName); 
    	if (client != null) { 
    		return client; 
    	} 
    	// IClientConfig ILoadBalancer 是 Ribbon 提供的 
    	IClientConfig config = this.factory.getClientConfig(clientName); 
    	ILoadBalancer lb = this.factory.getLoadBalancer(clientName); 
    	ServerIntrospector serverIntrospector = this.factory.getInstance(clientName, ServerIntrospector.class); 
    	client = this.loadBalancedRetryFactory != null ? new RetryableFeignLoadBalancer(lb, config, serverIntrospector, this.loadBalancedRetryFactory) : new FeignLoadBalancer(lb, config, serverIntrospector); this.cache.put(clientName, client); 
    	// 该 client 本身封装了 Ribbon 的IClientConfig ILoadBalancer,在后面进行替换 url 和负载均衡发挥作用 
    	return client; 
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    FeignLoadBalancer如何负载均衡选择 Server

    public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
       LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig);
       try {
       // 这提交了一个匿名内部类进去,那么ServerOperation.call方法就一定会在submit方法里被调用 
           return command.submit(
               new ServerOperation<T>() {
                   @Override
                   public Observable<T> call(Server server) {
                       URI finalUri = reconstructURIWithServer(server, request.getUri());
                       S requestForServer = (S) request.replaceUri(finalUri);
                       try {
                           return Observable.just(AbstractLoadBalancerAwareClient.
                           this.execute(requestForServer, requestConfig));
                       } 
                       catch (Exception e) {
                           return Observable.error(e);
                       }
                   }
               }).toBlocking().single();
       } 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    在 submit() 中调用selectServer(),进行 url 替换,选择一个服务实例

    LoadBalancerCommand.java

    public Observable<T> submit(final ServerOperation<T> operation) { 
    	final ExecutionInfoContext context = new ExecutionInfoContext(); 
    	if (listenerInvoker != null) { 
    		try { 
    			listenerInvoker.onExecutionStart(); 
    		} catch (AbortExecutionException e) { 
    			return Observable.error(e);
    		} 
    } 
    	 // ribbon的重试参数 
    	 final int maxRetrysSame = retryHandler.getMaxRetriesOnSameServer(); 
    	 final int maxRetrysNext = retryHandler.getMaxRetriesOnNextServer(); 
    	 // Use the load balancer // selectServer 负载均衡选择实例 
    	 Observable<T> o = (server == null ? selectServer() : Observable.just(server)) 
    	 ......省略部分代码 
    	 // 选择出服务实例后,对operation进行回调,进行url的替换,然后发起真正的http请求 
    	 return operation.call(server)
    	 ... ......省略部分代码 
    	 // 选择一个服务实例 
     private Observable<Server> selectServer() { 
    	 return Observable.create(new OnSubscribe<Server>() { 
    		 @Override public void call(Subscriber<? super Server> next) { 
    		 try { 
    			 // 读取host信息,也就是服务名,然后调用负载均衡器chooseServer方法选择一个服务实例 
    			 Server server = loadBalancerContext.getServerFromLoadBalancer(loadBalancerURI, loadBalancerKey);   
    			 next.onNext(server); next.onCompleted(); 
    		 } catch (Exception e) { 
    			 next.onError(e); 
    		 } 
    	 } 
     }
     ); } 
    
    • 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
    • 构造了一个LoadBalancerCommand
    • 构造了一个ServerOperation,包含了发起http调用的逻辑,作为参数传入- -
      LoadBalancerCommand.submit方法,后面会进行回调
    • 在submit方法中,会调用selectServer方法,选择服务实例
      selectServer方法调用loadBalancerContext.getServerFromLoadBalancer,
      最终调用负载均衡器chooseServer方法选择一个服务实例,
    • 拿到服务实例后,将Server对象传入ServerOperation的call方法进行回调
      ServerOperation用server的信息替换host里的服务名,拿到真正的请求地址
    • 再调用子类也就是FeignLoadBalancer.execute方法执行http请求
    • 默认的connectTimeout和readTimeout都是1000毫秒
    • 响应结果封装为RibbonResponse

    执行 http 请求

    获取到了远程服务的真实地址,就可以采用 Http 方式调用了,问题是可提供Http 调用的方式有那么多(OkHttp 、ApacheHttpClient、RestTemplate 等等),Feign 怎么知道用哪个呢?

    答案在 FeignAutoConfiguration 里

    @Configuration(proxyBeanMethods = false) 
    @ConditionalOnClass(ApacheHttpClient.class) 
    @ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer") 
    @ConditionalOnMissingBean(CloseableHttpClient.class) 
    @ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true) 
    protected static class HttpClientFeignConfiguration { // ... } 
    
    @Configuration(proxyBeanMethods = false) 
    @ConditionalOnClass(OkHttpClient.class) 
    @ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer") 
    @ConditionalOnMissingBean(okhttp3.OkHttpClient.class) 
    @ConditionalOnProperty("feign.okhttp.enabled") 
    protected static class OkHttpFeignConfiguration { // ... } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    通过 @ConditionalOnClass 、@ConditionalOnProperty 等条件注解 来匹配用哪个 http 客户端,所以我们在使用过程中也是无感知的,只要我们引入 OkHttp 或者 ApacheHttpClient 相关的 Maven 依赖,就可以完成调用了

    至此,FeignClient 被注册,被动态代理,被执行,整个流程已经清晰了,上面的问题也应该都有了答案,还有一个:Ribbon 是如何知道我们用到的是哪个注册中心,是 Eureka 是 Nacos ?他又是如何获取注册中心上的服务的?

    Ribbon 获取注册中心服务路由

    既然我们在使用过程中没有自己指定,那么我猜想是不是 Ribbon 自己注册的时候自己会选择呢?
    我们就从Ribbon 的注册入手 RibbonClientConfiguration ,果然配置ILoadBalancer

    RibbonClientConfiguration 中会配置 ILoadBalancer,会返回一个 ZoneAwareLoadBalancer 实例

    @Bean
    @ConditionalOnMissingBean
    public ILoadBalancer ribbonLoadBalancer(IClientConfig config, ServerList<Server> serverList, 
    ServerListFilter<Server> serverListFilter, IRule rule, IPing ping,ServerListUpdater serverListUpdater) {
    	   return (ILoadBalancer)(this.propertiesFactory.isSet(ILoadBalancer.class, this.name) ? 
    	   (ILoadBalancer)this.propertiesFactory.get(ILoadBalancer.class, config, this.name) :
    	   new ZoneAwareLoadBalancer(config, rule, ping, serverList, serverListFilter, serverListUpdater));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    更新注册中心列表

    restOfInit() --> restOfInit() --> updateListOfServers --> servers = serverListImpl.getUpdatedListOfServers();

    public void updateListOfServers() {
            List<T> servers = new ArrayList<T>();
            if (serverListImpl != null) {
            // 更新注册中心服务列表 
                servers = serverListImpl.getUpdatedListOfServers();
                LOGGER.debug("List of Servers for {} obtained from Discovery client: {}",
                        getIdentifier(), servers);
    
                if (filter != null) {
                    servers = filter.getFilteredListOfServers(servers);
                    LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}",
                            getIdentifier(), servers);
                }
            }
            updateAllServerList(servers);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述

    ServerList serverListImpl

    获取服务列表是通过 serverListImpl 来获取的,serverListImpl 是一个interface,不同的注册中心服务商会实现 ServerLis, 比如 Nacos 的实现NacosServerList 。

    这样我们就不用指定,Ribbon 就自己知道是从哪个注册中心获取了。

  • 相关阅读:
    linux运行ant 报错 Unable to locate tools.jar【已解决】
    【Unity-Cinemachine相机】虚拟相机(Virtual Camera)的本质与基本属性
    epoll与非阻塞
    拓展(华为优秀网站)
    CADD课程学习(1)-- 药物设计基础知识
    [centos]centos镜像ISO下载地址
    云原生架构(微服务、容器云、DevOps、不可变基础设施、声明式API、Serverless、Service Mesh)
    神经网络如何识别图像,神经网络图像识别技术
    DSP 开发例程: led_flash
    中间件漏洞(redis)
  • 原文地址:https://blog.csdn.net/qq_22075913/article/details/126087660