上接:
SpringCloud OpenFeign_钱多多_qdd的博客-CSDN博客
前言
源码图:
一、Bean的动态装载
在spring系列里bean的动态装载是很常见的,感兴趣可以看看我的这篇文章:
Bean的动态装载_钱多多_qdd的博客-CSDN博客_动态加载bean
二、@FeignClient解析
从下面这个类开始切入,这个注解开启了FeignClient的解析过程。
@EnableFeignClients(basePackages = "com.gupaoedu.example.clients")
2.1 FeignClientsRegistrar
- registerDefaultConfiguration 方法内部从 SpringBoot 启动类上检查是否有@EnableFeignClients, 有该注解的话, 则完成 Feign 框架相关的一些配置内容注册
- registerFeignClients 方法内部从 classpath 中, 扫描获得 @FeignClient 修饰的类, 将类的内容解析为 BeanDefinition , 最终通过调用 Spring 框架中的
BeanDefinitionReaderUtils.resgisterBeanDefinition 将解析处理过的 FeignClient
BeanDeifinition 添加到 spring 容器中
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
//注册@EnableFeignClients中定义defaultConfiguration属性下的类,包装成FeignClientSpecification,注册到Spring容器。
registerDefaultConfiguration(metadata, registry);
//在@FeignClient中有一个属性:configuration,这个属性是表示各个FeignClient自定义的配置类,后面也会通过调用registerClientConfiguration方法来注册成FeignClientSpecification到容器。
//所以,这里可以完全理解在@EnableFeignClients中配置的是做为兜底的配置,在各个@FeignClient配置的就是自定义的情况。
registerFeignClients(metadata, registry);
}
这里面需要重点分析的就是 registerFeignClients 方法,这个方法主要是扫描类路径下所有的
@FeignClient注解,然后进行动态Bean的注入。它最终会调用 registerFeignClient 方法。
由上图我们可以看到此方法大概有5个步骤:
- 获取@EnableFeignClient的相关属性;
- 获取服务端类所在的路径basePackages(@EnableFeignClient("xx.xx.xx")配置的)。后续会遍历这些路径,并找到@FeignClient注解的类,将相关的类注册;
- 扫描basePackage下的所有类;
- 获取@FeignClient注解的相关属性;
- 注册;
下面我们来着重看第5步注册:
private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientFactoryBean.class);
validate(attributes);
definition.addPropertyValue("url", getUrl(attributes));
definition.addPropertyValue("path", getPath(attributes));
String name = getName(attributes);
definition.addPropertyValue("name", name);
String contextId = getContextId(attributes);
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);
String alias = contextId + "FeignClient";
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
// has a default, won't be null
boolean primary = (Boolean) attributes.get("primary");
beanDefinition.setPrimary(primary);
String qualifier = getQualifier(attributes);
if (StringUtils.hasText(qualifier)) {
alias = qualifier;
}
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
new String[] { alias });
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
在这个方法中,就是去组装BeanDefinition,也就是Bean的定义,然后注册到Spring IOC容器。
我们关注一下,BeanDefinitionBuilder是用来构建一个BeanDefinition的,它是通过
genericBeanDefinition 来构建的,并且传入了一个FeignClientFactoryBean的类,代码如下。
我们可以发现,FeignClient被动态注册成了一个FactoryBean,之前听过tom老师的源码的同学应该知道什么是FactoryBean吧。
Spring Cloud FengnClient实际上是利用Spring的代理工厂来生成代理类,所以在这里地方才会、把所有的FeignClient的BeanDefinition设置为FeignClientFactoryBean类型,而
FeignClientFactoryBean继承自FactoryBean,它是一个工厂Bean。
在Spring中,FactoryBean是一个工厂Bean,用来创建代理Bean。
工厂 Bean 是一种特殊的 Bean, 对于 Bean 的消费者来说, 他逻辑上是感知不到这个 Bean 是普通的 Bean 还是工厂 Bean, 只是按照正常的获取 Bean 方式去调用, 但工厂bean 最后返回的实例不是工厂Bean 本身, 而是执行工厂 Bean 的 getObject 逻辑返回的示例。
public static BeanDefinitionBuilder genericBeanDefinition(Class<?> beanClass) {
BeanDefinitionBuilder builder = new BeanDefinitionBuilder(new GenericBeanDefinition());
builder.beanDefinition.setBeanClass(beanClass);
return builder;
}
简单来说,FeignClient标注的这个接口,会通过FeignClientFactoryBean.getObject()这个方法获得一个代理对象。
2.2 FeignClientFactoryBean.getObject
getObject调用的是getTarget方法,它从applicationContext取出FeignContext,FeignContext继承了NamedContextFactory,它是用来来统一维护feign中各个feign客户端相互隔离的上下文。
FeignContext注册到容器是在FeignAutoConfiguration上完成的:
在初始化FeignContext时,会把configurations在容器中放入FeignContext中。configurations的来源就是在前面registerFeignClients方法中将@FeignClient的配置configuration。
- 接着,构建feign.builder,在构建时会向FeignContext获取配置的Encoder,Decoder等各种信息。
- FeignContext在上篇中已经提到会为每个Feign客户端分配了一个容器,它们的父容器就是spring容器配置完Feign.Builder之后,再判断是否需要LoadBalance,如果需要,则通过LoadBalance的方法来设置。
- 实际上他们最终调用的是Target.target()方法。
2.3 loadBalance
生成具备负载均衡能力的feign客户端,为feign客户端构建起绑定负载均衡客户端Client client = (Client)this.getOptional(context, Client.class);从上下文中获取一个Client,默认是LoadBalancerFeignClient。
——绿色②代码。
它是在FeignRibbonClientAutoConfiguration这个自动装配类中,通过Import实现的:
绿色①代码:
protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
HardCodedTarget<T> target) {
Client client = getOptional(context, Client.class);
if (client != null) {
builder.client(client);
Targeter targeter = get(context, Targeter.class);
return targeter.target(this, builder, context, target);
}
throw new IllegalStateException(
"No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?");
}
2.4 DefaultTarget.target
2.5 ReflectiveFeign.newInstance
这个方法是用来创建一个动态代理的方法,在生成动态代理之前,会根据Contract协议(协议解析规则,解析接口类的注解信息,解析成内部的MethodHandler的处理方式。
从实现的代码中可以看到熟悉的Proxy.newProxyInstance方法产生代理类。而这里需要对每个定义的接口方法进行特定的处理实现,所以这里会出现一个MethodHandler的概念,就是对应方法级别的InvocationHandler。
三、接口定义的参数解析
根据FeignClient接口的描述解析出对应的请求数据。
3.1 targetToHandlersByName.apply(target)
还是在ReflectiveFeign.newInstance方法里:
根据Contract协议规则,解析接口类的注解信息,解析成内部表现:
targetToHandlersByName.apply(target);会解析接口方法上的注解,从而解析出方法粒度的特定的配置信息,然后生产一个SynchronousMethodHandler 然后需要维护一个<method,MethodHandler>的map,放入InvocationHandler的实现FeignInvocationHandler中。
3.2 SpringMvcContract
当前Spring Cloud 微服务解决方案中,为了降低学习成本,采用了Spring MVC的部分注解来完成 请求协议解析,也就是说 ,写客户端请求接口和像写服务端代码一样:客户端和服务端可以通过SDK的方式进行约定,客户端只需要引入服务端发布的SDK API,就可以使用面向接口的编码方式对接服务。
扩展:Feign详解4-Contract 源码_zhangyingchengqi的博客-CSDN博客
四、OpenFeign调用过程
在前面的分析中,我们知道OpenFeign最终返回的是一个#ReflectiveFeign.FeignInvocationHandler的对象。
那么当客户端发起请求时,会进入到FeignInvocationHandler.invoke方法中,这个大家都知道,它是一个动态代理的实现。
而接着,在invoke方法中,会调用 this.dispatch.get(method)).invoke(args) 。
this.dispatch.get(method) 会返回一个SynchronousMethodHandler,进行拦截处理。
这个方法会根据参数生成完成的RequestTemplate对象,这个对象是Http请求的模版,代码如下:
4.1 动态生成Request
4.2 executeAndDecode
经过上述的代码,我们已经将restTemplate拼装完成,上面的代码中有一个executeAndDecode() 方法,该方法通过RequestTemplate生成Request请求对象,然后利用Http Client获取response,来获取响应信息。
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
//转化为Http请求报文
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);
}
long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
if (decoder != null)
return decoder.decode(response, metadata.returnType());
CompletableFuture<Object> resultFuture = new CompletableFuture<>();
asyncResponseHandler.handleResponse(resultFuture, metadata.configKey(), response,
metadata.returnType(),
elapsedTime);
try {
if (!resultFuture.isDone())
throw new IllegalStateException("Response handling not done");
return resultFuture.join();
} catch (CompletionException e) {
Throwable cause = e.getCause();
if (cause != null)
throw cause;
throw e;
}
}
4.3 Client.execute
默认采用JDK的 HttpURLConnection 发起远程调用。
五、拓展
六、 题目
6.1 请简述OpenFeign的底层原理
openfeign通过包扫描将所有被@FeignClient注解注释的接口扫描出来,并为每个接口注册一个FeignClientFactoryBean实例。FeignClientFactoryBean是一个FactoryBean,当Spring调用FeignClientFactoryBean的getObject方法时,openfeign返回一个Feign生成的动态代理对象,拦截接口的方法执行。
feign会为代理的接口的每个方法Method都生成一个MethodHandler。
当为接口上的@FeignClient注解的url属性配置服务提供者的url时,其实就是不与Ribbon整合,此时由SynchronousMethodHandler实现接口方法远程同步调用,使用默认的Client实现类Default实例发起http请求。
当接口上的@FeignClient注解的url属性不配置时,且会走负载均衡逻辑,也就是需要与Ribbon整合使用。这时候不再是使用默认的Client(Default)调用接口,而是使用LoadBalancerFeignClient调用接口,由LoadBalancerFeignClient实现与Ribbon的整合。
Spring Cloud OpenFeign原理可以理解为:
通过 @EnableFeignCleints 触发 Spring 应用程序对 classpath 中 @FeignClient 修饰类的扫描解析到 @FeignClient 修饰类后, Feign 框架通过扩展 Spring Bean Deifinition 的注册逻辑, 最终注册一个 FeignClientFacotoryBean 进入 Spring 容器
Spring 容器在初始化其他用到 @FeignClient 接口的类时, 获得的是 FeignClientFacotryBean 产生的一个代理对象 Proxy。
基于 java 原生的动态代理机制, 针对 Proxy 的调用, 都会被统一转发给 Feign 框架所定义的一个 InvocationHandler , 由该 Handler 完成后续的 HTTP 转换, 发送, 接收, 翻译HTTP响应的工作
请简述OpenFeign的底层原理?
Feign 能够让我们像调用一个本地 java 方法一样去调用基于 HTTP 协议的 Rest API 接口。 省去了我们编写 HTTP 连接,数据解析获取等一系列繁琐的操作。
Feign 的整体的工作流程:
- SpringBoot 应用启动时, 由针对 @EnableFeignClient 这一注解的处理逻辑触发程序扫描 classPath中所有被@FeignClient 注解的类, 这里以 DemoService 为例, 将这些类解析为 BeanDefinition 注册到 Spring 容器中
- Sping 容器在为某些用的 Feign 接口的 Bean 注入 DemoService 时, Spring 会尝试从容器中查找 DemoService 的实现类
- 由于我们从来没有编写过 DemoService 的实现类, 上面步骤获取到的 DemoService 的实现类必然是 feign 框架通过扩展 spring 的 Bean 处理逻辑, 为 DemoService 创建一个动态接口代理对象, 这里我们将其称为 DemoServiceProxy 注册到spring 容器中。
- Spring 最终在使用到 DemoService 的 Bean 中注入了 DemoServiceProxy 这一实例。
- 当业务请求真实发生时, 对于 DemoService 的调用被统一转发到了由 Feign 框架实现的 InvocationHandler 中, InvocationHandler 负责将接口中的入参转换为 HTTP 的形式, 发到服务端, 最后再解析 HTTP 响应, 将结果转换为 Java 对象, 予以返回。
Feign 的核心实现原理就是基于接口的动态代理。