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 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> Feign 从注册到调用原理分析 -> 正文阅读

[Java知识库]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); 
} 
// 调用远程服务 
@Autowired 
OrderFeignService orderFeignService; 

R findOrderByUserId(@PathVariable("id") Integer id) { 
	//feign调用 
	R result = orderFeignService.findOrderByUserId(id); 
	return result; 
} 

这样一来,我们省去了自己去配置 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. 扫描并注册 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); 
} } } 

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

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

在这里插入图片描述

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. FeignInvocationHandler 执行 invoke ,一步步调用,最终会调用 Client 的 execute()方法,执行远程调用

总结:

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

FeignClient 调用

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

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

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

在这里插入图片描述

这里完成了 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; 
 } 

所以我们可以看到,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); 
		// ... 
	} 
} 

在这里插入图片描述

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

在这里插入图片描述

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

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

在 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); 
		 } 
	 } 
 }
 ); } 
  • 构造了一个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 { // ... } 

通过 @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));
}

更新注册中心列表

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

在这里插入图片描述

ServerList serverListImpl

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

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

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-08-06 10:29:56  更:2022-08-06 10:33:09 
 
开发: 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/18 5:13:06-

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