Feign源码解析4:调用过程
背景
前面几篇分析了Feign的初始化过程,历经艰难,可算是把@FeignClient注解的接口对应的代理对象给创建出来了。今天看下在实际Feign调用过程中的一些源码细节。
我们这里Feign接口如下:
@FeignClient(value = "echo-service-provider") // 指向服务提供者应用
public interface EchoService {
@GetMapping("/echo/{message}")
String echo(@PathVariable("message") String message);
}
调用代码:
http://localhost:8080/feign/echo/ddd
@GetMapping("/feign/echo/{message}")
public String feignEcho(@PathVariable String message) {
return echoService.echo(message);
}
完整解析
动态代理
这里的echoService此时的类型其实是jdk动态代理,内部有一个字段,就是实现了jdk的InvocationHandler接口:
实际逻辑如下,会根据被调用的method找到一个合适的handler:
import feign.InvocationHandlerFactory.MethodHandler
static class FeignInvocationHandler implements InvocationHandler{
private final Map<Method, MethodHandler> dispatch;
}
一般获取到的都是如下类型:
这个类有如下核心属性:
都是和http请求息息相关的,如重试、请求拦截器、响应拦截器、logger、logger级别、options(包含了超时时间等参数)。
重试
接下来看实际请求的大体框架:
上面主要是一个while循环,内部会执行请求,如果请求报错,抛出了RetryableException类型的异常,此时就会由重试组件(Retryer retryer),判断是否要重试,如果要的话,就会continue,就会再次执行请求。
但如果重试组件认为不需要重试或重试次数已经超过,就会抛出异常,此时就走不到continue部分了,会直接向上层抛异常。
注意,这个重试接口实现了Cloneable,因为每次请求的时候,都要有一个对应的重试对象来记录当前请求的重试状态(比如重试了几次了),正因为有状态,所以得每次clone一个新的。
Retryer retryer = this.retryer.clone();
默认情况下,不做任何配置,都是不重试的,此时的retryer类型为一个内部类,意思是NEVER_RETRY:
这里再拓展一点,什么情况下,Feign会抛出RetryableException呢?
其实就是在执行上图的正常执行部分,遇到了java.io.IOException的时候,就会抛这种RetryableException。
static FeignException errorExecuting(Request request, IOException cause) {
return new RetryableException(
-1,
format("%s executing %s %s", cause.getMessage(), request.httpMethod(), request.url()),
request.httpMethod(),
cause,
null, request);
}
还有一种情况是啥呢?是服务端返回的http header中包含了Retry-After
这个header的时候:
这个header一般是在503的时候返回:
根据模版创建请求
大家看看我们的接口,用了path variable,所以,在真正进行请求前,必须先完成一些准备工作,比如把path variable替换为真实的值:
@GetMapping("/echo/{message}")
String echo(@PathVariable("message") String message);
另外,大家看代码,还有个参数传给下层:
这个就是控制请求超时参数的。这个options从哪里来呢,从我们的传参来。我们可以在方法中加一个参数:
@GetMapping("/echo/{message}")
String echo(@PathVariable("message") String message, Request.Options options);
这个的优先级,我觉得应该是最高的。
这样就能支持,每个接口用不一样的超时时间。
executeAndDecode概览
生成绝对路径请求
先说说1处:
Request targetRequest(RequestTemplate template) {
// 使用请求拦截器
for (RequestInterceptor interceptor : requestInterceptors) {
interceptor.apply(template);
}
return target.apply(template);
}
请求拦截器的类型:
public interface RequestInterceptor {
/**
* Called for every request. Add data using methods on the supplied {@link RequestTemplate}.
*/
void apply(RequestTemplate template);
}
这里是对模版进行修改,我看注释,有如下场景(增加全局的header):
接下来,再看看模版如何转化为请求:
return target.apply(template);
结果,其实也没干啥,就是模版里只有接口的相对路径,此处要拼接为完整路径:
client.execute接口
实际执行请求,靠client对象,它的默认类型为:
它是自动装配的:
它内部包裹了实际发http请求的库,上面的代码中,默认用的是feign自带的(位于feign-core依赖):
new Client.Default(null, null)
比如,假设我们想用httpclient (在classpath中包含),就会触发自动装配:
也支持okhttp(feign.okhttp.enabled为true):
这里看看FeignBlockingLoadBalancerClient的几个构造参数:
public FeignBlockingLoadBalancerClient(Client delegate, LoadBalancerClient loadBalancerClient, LoadBalancerClientFactory loadBalancerClientFactory) {
this.delegate = delegate;
this.loadBalancerClient = loadBalancerClient;
this.loadBalancerClientFactory = loadBalancerClientFactory;
}
第一个是上面提到的http客户端,第二个、第三个呢,其实是负责负载均衡的客户端(不只是要负责客户端负载均衡算法,还要负责从nacos这些地方获取服务对应的实例)
client获取服务实例
这个execute方法实际有两部分,一部分是如下,根据service名称,获取服务实例(包含了真实的ip、端口),再一部分才是对服务实例发起请求。
下面的1处,是获取一些listener,然后通知listener,我们已经开始请求了,这里的listener的类型是:
org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycle
包含了好些个生命周期方法,如onStart方法:
void onStart(Request<RC> request);
然后是2处,利用loadBalancerClient,根据服务名,如我们这里的echo-service-provider,获取到一个实例(类型为org.springframework.cloud.client.ServiceInstance)。
3处会判断,如果没获取到实例,此时就会报503了,服务实例不存在。
这里面,获取实例的部分,比较复杂,我们得单独开一篇来讲。
发起真实请求
根据服务实例,组装真实的url进行请求。
这个等负载均衡部分写完了,再讲解这部分,这块的逻辑也还好,无非是对httpclient这些的封装。
总结
我们把整体的feign调用的脉络梳理了一遍,下篇继续loadbalancer的部分,那里也是有点坑的。
用讲师的话来结个尾:啥也不是,散会!