1.背景
今天我们学习SpringCloud的客户端负载均衡Ribbon
我们今天继续使用之前eureka-server作为服务注册中心
使用Springboot和springcloud的版本如下
- springboot版本:2.3.5-release
- springcloud版本:Hoxton.SR9
2.feign远程的基本流程
Feign远程调用,核心就是通过一系列的封装和处理,将以JAVA注解的方式定义的远程调用API接口,最终转换成HTTP的请求形式,然后将HTTP的请求的响应结果,解码成JAVA Bean,放回给调用者。Feign远程调用的基本流程,大致如下图所示
从上图可以看到,Feign通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的 Request 请求。通过Feign以及JAVA的动态代理机制,使得Java 开发人员,可以不用通过HTTP框架去封装HTTP请求报文的方式,完成远程服务的HTTP调用
2.1 Feign的重要组件
在微服务启动时,Feign会进行包扫描,对加@FeignClient注解的接口,按照注解的规则,创建远程接口的本地JDK Proxy代理实例。然后,将这些本地Proxy代理实例,注入到Spring IOC容器中。当远程接口的方法被调用,由Proxy代理实例去完成真正的远程访问,并且返回结果。
- @EnableFeignClients
- @FeignClient
3.源码解读
@EnableFeignClients
从这个注解是入口,我们可以看到使用@Import这个注解,这是注解我们在使用springboot中经常遇到,主要作用就是将Bean加载在IOC容器中
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients
FeignClientsRegistrar
这个方式主要是加载一下默认配置,同时扫描@FeignClient的注解的类加载在IOC容器
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
//特别重要:加载一些默认配置
registerDefaultConfiguration(metadata, registry);
//特别重要:扫描@FeignClient注解加入到IOC
registerFeignClients(metadata, registry);
}
public void registerFeignClients(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
//特别重要:扫描@FeignClient的注解
scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
//加载在IOC容器
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
//特别重要:将FeignClient对象包装成FeignClientFactoryBean对象。偷天换日(Spring+mybaties也是这样的)
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientFactoryBean.class);
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
new String[] { alias });
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
FeignClientFactoryBean的分析
实现类Spring的FactoryBean的工程bean。
class FeignClientFactoryBean
implements FactoryBean<Object>, InitializingBean, ApplicationContextAware
该类主要有是三个方法
这个IOC容器在获取Bean的时候会调用getObject的方法,这里是代理的入口
@Override
public Object getObject() throws Exception {
return getTarget();
}
<T> T getTarget() {
// 二、实例化Feign上下文对象FeignContext
FeignContext context = applicationContext.getBean(FeignContext.class);
// 三、生成builder对象,用来生成feign
Feign.Builder builder = feign(context);
//特别重要:判断是否有url,没有则是负载均衡算法
if (!StringUtils.hasText(url)) {
if (!name.startsWith("http")) {
url = "http://" + name;
}
else {
url = name;
}
url += cleanPath();
// 四、生成负载均衡代理类
return (T) loadBalance(builder, context,
new HardCodedTarget<>(type, name, url));
}
}
生成代理类
protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
HardCodedTarget<T> target) {
//特别重要:获取feignClient,有三种情况
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?");
}
FeignClient用三种实现
1)没有整合ribbon、sleuth: 获取默认的Client:Default实例。
2)整合了ribbon,没有整合sleuth: 获取LoadBalanceFeignClient实例。
3)整合了ribbon 和 sleuth: 会获取TraceFeignClient实例,该实例是对LoadBalanceFeignClient的一种包装,实现方式通过BeanPostProcessor实现:FeignBeanPostProcessor中定义了包装逻辑:
HystrixTargeter或者DefaultTargeter
class HystrixTargeter implements Targeter {
@Override
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
FeignContext context, Target.HardCodedTarget<T> target) {
// 判断是否加入hystrix
if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) {
return feign.target(target);
}
}
public <T> T target(Target<T> target) {
return build().newInstance(target);
}
ReflectiveFeign
public Feign build() {
return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
}
}
public <T> T newInstance(Target<T> target) {
//特别重要:FeignInvocationHandler,HystrixInvocationHandler
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;
}
本次我们使用FeignInvocationHandler
FeignInvocationHandler
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return dispatch.get(method).invoke(args);
}
feign.SynchronousMethodHandler#invoke
@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) {
}
}
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
Request request = targetRequest(template);
//特别重要:执行最终的方法:LoadBalancerFeignClient
response = client.execute(request, options);
LoadBalancerFeignClient
这里使用到了RIbbon的负载均衡策略
@Override
public Response execute(Request request, Request.Options options) throws IOException {
try {
URI asUri = URI.create(request.url());
String clientName = asUri.getHost();
URI uriWithoutHost = cleanUrl(request.url(), clientName);
FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
this.delegate, request, uriWithoutHost);
IClientConfig requestConfig = getClientConfig(options, clientName);
return lbClient(clientName)
.executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
}
catch (ClientException e) {
IOException io = findIOException(e);
if (io != null) {
throw io;
}
throw new RuntimeException(e);
}
}
4.0 流程总结
由于Feign远程调用接口的JDK Proxy实例的InvokeHandler调用处理器有多种,导致Feign远程调用的执行流程,也稍微有所区别,但是远程调用执行流程的主要步骤,是一致的。这里主要介绍两类JDK Proxy实例的InvokeHandler调用处理器相关的远程调用执行流程:
(1)与 默认的调用处理器 FeignInvocationHandler 相关的远程调用执行流程;
(2)与 Hystrix调用处理器 HystrixInvocationHandler 相关的远程调用执行流程。
介绍过程中,还是以前面的DemoClient的JDK Proxy远程动态代理实例的执行过程为例,演示分析Feigh远程调用的执行流程。
1.1.1 与 FeignInvocationHandler 相关的远程调用执行流程
FeignInvocationHandler是默认的调用处理器,如果不对Feign做特殊的配置,则Feign将使用此调用处理器。结合前面的DemoClient的JDK Proxy远程动态代理实例的hello()远程调用执行过程,在这里,详细的介绍一下与 FeignInvocationHandler 相关的远程调用执行流程,大致如下图所示。
图6 与 FeignInvocationHandler 相关的远程调用执行流程
整体的远程调用执行流程,大致分为4步,具体如下:
第1步:通过Spring IOC 容器实例,装配代理实例,然后进行远程调用。
前文讲到,Feign在启动时,会为加上了@FeignClient注解的所有远程接口(包括 DemoClient 接口),创建一个本地JDK Proxy代理实例,并注册到Spring IOC容器。在这里,暂且将这个Proxy代理实例,叫做 DemoClientProxy,稍后,会详细介绍这个Proxy代理实例的具体创建过程。
然后,在本实例的UserController 调用代码中,通过@Resource注解,按照类型或者名称进行匹配(这里的类型为DemoClient接口类型),从Spring IOC容器找到这个代理实例,并且装配给@Resource注解所在的成员变量,本实例的成员变量的名称为 demoClient。
在需要代进行hello()远程调用时,直接通过 demoClient 成员变量,调用JDK Proxy动态代理实例的hello()方法。
第2步:执行 InvokeHandler 调用处理器的invoke(…)方法
前面讲到,JDK Proxy动态代理实例的真正的方法调用过程,具体是通过 InvokeHandler 调用处理器完成的。故,这里的DemoClientProxy代理实例,会调用到默认的FeignInvocationHandler 调用处理器实例的invoke(…)方法。
通过前面 FeignInvocationHandler 调用处理器的详细介绍,大家已经知道,默认的调用处理器 FeignInvocationHandle,内部保持了一个远程调用方法实例和方法处理器的一个Key-Value键值对Map映射。FeignInvocationHandle 在其invoke(…)方法中,会根据Java反射的方法实例,在dispatch 映射对象中,找到对应的 MethodHandler 方法处理器,然后由后者完成实际的HTTP请求和结果的处理。
所以在第2步中,FeignInvocationHandle 会从自己的 dispatch映射中,找到hello()方法所对应的MethodHandler 方法处理器,然后调用其 invoke(…)方法。
第3步:执行 MethodHandler 方法处理器的invoke(…)方法
通过前面关于 MethodHandler 方法处理器的非常详细的组件介绍,大家都知道,feign默认的方法处理器为 SynchronousMethodHandler,其invoke(…)方法主要是通过内部成员feign客户端成员 client,完成远程 URL 请求执行和获取远程结果。
feign.Client 客户端有多种类型,不同的类型,完成URL请求处理的具体方式不同。
第4步:通过 feign.Client 客户端成员,完成远程 URL 请求执行和获取远程结果
如果MethodHandler方法处理器实例中的client客户端,是默认的 feign.Client.Default 实现类性,则使用JDK自带的HttpURLConnnection类,完成远程 URL 请求执行和获取远程结果。
如果MethodHandler方法处理器实例中的client客户端,是 ApacheHttpClient 客户端实现类性,则使用 Apache httpclient 开源组件,完成远程 URL 请求执行和获取远程结果。
通过以上四步,应该可以清晰的了解到了 SpringCloud中的 feign 远程调用执行流程和运行机制。
实际上,为了简明扼要的介绍清楚默认的调用流程,上面的流程,实际上省略了一个步骤:第3步,实际可以分为两小步。为啥呢? SynchronousMethodHandler 并不是直接完成远程URL的请求,而是通过负载均衡机制,定位到合适的远程server 服务器,然后再完成真正的远程URL请求。换句话说,SynchronousMethodHandler实例的client成员,其实际不是feign.Client.Default类型,而是 LoadBalancerFeignClient 客户端负载均衡类型。 因此,上面的第3步,如果进一步细分话,大致如下:(1)首先通过 SynchronousMethodHandler 内部的client实例,实质为负责客户端负载均衡 LoadBalancerFeignClient 实例,首先查找到远程的 server 服务端;(2) 然后再由LoadBalancerFeignClient 实例内部包装的feign.Client.Default 内部类实例,去请求server端服务器,完成URL请求处理。
5.自己绘制的一个流程图
参考文章: