前言:
上一篇文章说到被FeignClient注解修饰的接口会被加载成FeignClientFactoryBean类型,在Spring实例化时候会调用其getObject方法最后创建一个接口的代理类,然后封装了一个FeignInvocationHandler类,这个类实现了InvocationHandler,由动态原理可知在请求的时候会调用该对象的invoke方法,下面我会详细分析这个方法到底做了些什么事情。
源码分析:
先看看FeignInvocationHandler类的invoke方法,该方法的代码如下:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("equals".equals(method.getName())) {
try {
Object otherHandler =
args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
return equals(otherHandler);
} catch (IllegalArgumentException e) {
return false;
}
} else if ("hashCode".equals(method.getName())) {
return hashCode();
} else if ("toString".equals(method.getName())) {
return toString();
}
return dispatch.get(method).invoke(args);
}
dispatch.get(method)得到的类型是SynchronousMethodHandler,回调用其invoke方法,该方法代码如下:
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) {
try {
retryer.continueOrPropagate(e);
} catch (RetryableException th) {
Throwable cause = th.getCause();
if (propagationPolicy == UNWRAP && cause != null) {
throw cause;
} else {
throw th;
}
}
if (logLevel != Logger.Level.NONE) {
logger.logRetry(metadata.configKey(), logLevel);
}
continue;
}
}
首先创建了一个RequestTemplate对象,这个对象包含请求头信息以及请求方法等属性,接着获取一个Retryer对象,它是负载在远程接口不可调用时候进行重试的类,稍后我分析这个类的源码,接着一个while的死循环,try代码块中调用executeAndDecode方法,这个方法代码如下:
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
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);
} 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);
boolean shouldClose = true;
try {
if (logLevel != Logger.Level.NONE) {
response =
logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime);
}
if (Response.class == metadata.returnType()) {
if (response.body() == null) {
return response;
}
if (response.body().length() == null ||
response.body().length() > MAX_RESPONSE_BUFFER_SIZE) {
shouldClose = false;
return response;
}
// Ensure the response body is disconnected
byte[] bodyData = Util.toByteArray(response.body().asInputStream());
return response.toBuilder().body(bodyData).build();
}
if (response.status() >= 200 && response.status() < 300) {
if (void.class == metadata.returnType()) {
return null;
} else {
Object result = decode(response);
shouldClose = closeAfterDecode;
return result;
}
} else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) {
Object result = decode(response);
shouldClose = closeAfterDecode;
return result;
} else {
throw errorDecoder.decode(metadata.configKey(), response);
}
} catch (IOException e) {
if (logLevel != Logger.Level.NONE) {
logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime);
}
throw errorReading(request, response, e);
} finally {
if (shouldClose) {
ensureClosed(response.body());
}
}
}
这个方法又调用了targetRequest方法,该方法主要是获取所有的请求拦截器,然后循环遍历调用其apply方法,传入RequestTemplate对象,这个拦截器可以给RequestTemplate对象添加请求头信息,例如我的项目里面加入了一个自定义的拦截器,截图如下:
由我的截图可以看出我添加了一个自定义的请求拦截器,往RequestTemplate对象添加一个头信息,这就是拦截器的作用。
接着在targetRequest方法中创建了一个Request对象,成员属性主要有请求方式是GET/POST、请求的url、头信息、请求体参数,这些参数都来源于RequestTemplate对象。
好了targetRequest方法执行完成以后回到executeAndDecode方法,会调用client.execute(request, options),这个client实现类是LoadBalancerFeignClient,这里面的原理是Ribbon相关的源码,我会在分析Ribbon源码时候进行分析,现在我们只考虑返回Response对象以后如何处理的。
接着判断返回的类型是不是Response类型的,如果是直接返回里面的内容,然后判断是否是响应200的响应码,这里假定是200响应码,会调用decode方法进行解码,这个方法调用层次比较深我就不列出代码了,就说一下这里的逻辑是循环调用HttpMessageConverter类,这个类大概有10个截图如下:
这个类是处理我们返回的类比如我们返回的类型是一个Car对象类型,根据返回的媒体类型(text/html,applicaiton/json等)判断是否能处理这个Car类型,下面是我的一个测试Feign接口是这样定义的:
FeignClient(value = "exercise1")
public interface TestFeign {
@GetMapping(value = "/testFeign1")
public Car testFeign();
}
我的接口提供方是这样定义的:
@RestController
public class TestFeignController {
@RequestMapping("testFeign1")
public Car testFeign() {
Car car =new Car("tom",11);
return car;
}
}
可以看出接口提供方返回的是json类型,那么response中的媒体类型是application/json,我的Feign接口返回是Car类型,那么根据这两个参数能否判断出哪个HttpMessageConverter能处理吗?经过研究可知MappingJackson2HttpMessageConverter这个类可以处理返回的json数据,他可以把json数据转换为Car对象返回。
好了,decode方法执行完成以后就相当于把远程服务返回的数据封装成为我们Feign接口返回类型的对象,那么正常的逻辑就是执行完成了。
前面所说的都是能正常访问远程服务的情况下,还记得我前面截图的SynchronousMethodHandler的invoke方法中有catch代码块的逻辑吗?假如我们的远程服务挂了,会走到 retryer.continueOrPropagate(e)这段代码,这段代码是一个重试机制,我们看看里面的代码是什么样子的:
public void continueOrPropagate(RetryableException e) {
if (attempt++ >= maxAttempts) {
throw e;
}
long interval;
if (e.retryAfter() != null) {
interval = e.retryAfter().getTime() - currentTimeMillis();
if (interval > maxPeriod) {
interval = maxPeriod;
}
if (interval < 0) {
return;
}
} else {
interval = nextMaxInterval();
}
try {
Thread.sleep(interval);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
throw e;
}
sleptForMillis += interval;
}
这段代码的逻辑是:
定义一个attempt重试次数的变量,每次都加1,检查是否达到最大重试次数 maxAttempts,如果没有指定最大重试次数,默认是5次,如果达到最大重试次数,则直接抛出 RetryableException 异常。
计算下一次重试的时间间隔 interval:如果 RetryableException 中有设置 retryAfter,则计算 interval 为 retryAfter 时间减去当前时间的毫秒数。如果计算出的 interval 大于 maxPeriod,则将其截断为 maxPeriod。如果 interval 小于 0,则表示不需要进行重试,直接返回。
如果 RetryableException 中没有设置 retryAfter,则调用 nextMaxInterval() 方法计算默认的时间间隔。
使用 Thread.sleep(interval) 方法暂停当前线程,使得程序等待一段时间后再进行重试。如果在等待过程中被中断,则会抛出 InterruptedException 异常,并继续抛出 RetryableException 异常。
总体来说,这段代码的作用是在发生可重试异常时,判断是否需要进行重试,如果需要,计算下一次重试的时间间隔,并在间隔后进行重试。在重试过程中,如果线程被中断,则会继续抛出 RetryableException 异常。
总结:
以上就是我分析的Spring Cloud OpenFeign源码请求原理解析过程,下面我做个总结:
请求到达时候会调用FeignInvocationHandler的invoke方法,创建一个RequestTemplate对象设置请求头参数等信息,这些可能是在请求拦截器中设置的,接着调用远程服务获取响应对象,把响应对象解码成为我们Feign接口返回的类型,当服务不可用时候还可以进行重试机制,默认最大为5次,如果5次过后还是请求不到会抛出异常。
我是程序员老徐,下一节我会分析Spring Cloud Ribbon源码解析,还请大家持续关注我,我们下期见。