Feign的简单介绍
Feign组件主要用于微服务项目中,用来简化服务之间的远程调用,相信大家对他的使用方法已经相当熟悉了。那么Feign到底是如何实现服务的远程调用的呢?又是如何发送请求的呢?发送的请求又是什么样子的呢?本文主要就针对这三个对Feign的原理做一个简单的分析。
上面这张图是我按照自己的理解简单的画了一下Feign的主要工作流程,下面将结合Feign底层的源码对工作原理进行详细的分析。
Feign的工作原理
根据上图我将流程分为主要的三个步骤:通过动态代理在本地实例化远程接口->封装Request对象并进行编码->发送请求并对获取结果进行解码。我写了一个简单Feign调用的Demo,代码如下,以此作为背景再结合底层源码来分析。
@FeignClient("product")
public interface ProductClient {
/**
* 根据商品id获取商品信息
* @param id 商品id
* @return 商品信息
*/
@GetMapping("/product/getProductInfo/{id}")
Result<ProductInfoDTO> getProductInfo(@PathVariable("id") Integer id);
}
1.创建远程接口的本地代理实例
要使用Feign就必须在启动类上加上注解
@EnableFeignCleints
,这个注解就相当于Feign组件的一个入口,当使用
@EnableFeignCleints
后,程序启动后,会进行包扫描,扫描所有被
@FeignCleint
注解修饰的接口,通过JDK底层的动态代理来为远程接口创建代理实例,并且注册到IOC容器中。我们先来看看
@EnableFeignCleints
这个注解。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
//一系列属性
......
}
@EnableFeignCleints
注解引入了
FeignClientsRegistrar
类,这个类会在启动时调用
registerBeanDefinitions()
方法,而这个方法里面只有两行调用其他方法的代码,分别是用来加载Feign的各项配置的方法
registerDefaultConfiguration(metadata, registry)
和注册实例化对象的方法
registerFeignClients(metadata, registry)
。我们重点来看一下
registerFeignClients()
方法,方法的主要的逻辑就是先扫描基础包路径然后,然后找出被@ FeignClient注解修饰的接口,放入Set中,接着遍历集合为每个接口逐一创建实例化对象,主要代码如下:
//获取扫描的包路径
Set<String> basePackages;
basePackages.add(ClassUtils.getPackageName(clazz));
clientClasses.add(clazz.getCanonicalName());
......
//遍历包路径搜索目标接口
for (String basePackage : basePackages) {
Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);
for (BeanDefinition candidateComponent : candidateComponents) {
Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(
FeignClient.class.getCanonicalName());
registerClientConfiguration(registry, name,attributes.get("configuration"));
//为接口创建代理对象的方法
registerFeignClient(registry, annotationMetadata, attributes);
}
}
可以看到创建对象是交给
registerFeignClient()
方法来处理的,该方法会在创建对象前进行一系列的准备工作,比如IOC中对象对应的key值统一使用@FeignClient中的value值+”FeignClient”,本例中对应为productFeignClient等。而对于创建对象的所调用的方法一层层深入进去会发现,最终实际创建对象的核心代码是
feign.ReflectiveFeign
类中的
newInstance()
方法,其代码如下:
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;
看到这段代码相信大家都很熟悉了吧,这不正是JDK底层典型的动态代理嘛,可见Feign也是基于代理模式来创建接口的实例化对象的。而代理工具类InvocationHandler,Feign也默认提供了两种实现,稍后会提到。
2.封装Request对象并进行编码
当调用
ProductClient
接口中的
getProductInfo()
方法时,底层通过JDK动态代理交由Feign的代理类
FeignInvocationHandler
进行处理,(Feign底层实现了两个调用处理器分别为
FeignInvocationHandler
和
HystrixInvocationHandler
,默认的调用处理器为
FeignInvocationHandler
,当和Hystrix结合使用时才会使用
HystrixInvocationHandler
,本篇基于默认的
FeignInvocationHandler
进行分析)根据方法的对象在映射集合中找到对应的
MethodHandler
方法处理器。
static class FeignInvocationHandler implements InvocationHandler {
private final Map<Method, MethodHandler> dispatch;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
......
//找到对应的方法处理器
return dispatch.get(method).invoke(args);
}
}
Feign中为
MethodHandler
接口提供了默认的实现类
SynchronousMethodHandler
,其中中核心的逻辑就是根据具体的配置先创建
RequestTemplate
请求模板对象,然后调用Target接口中的
apply(RequestTemplate input)
方法根据刚刚获取到的请求模板对象创建远程服务请求的Request对象。部分核心代码如下:
final class SynchronousMethodHandler implements MethodHandler {
......
public Object invoke(Object[] argv) throws Throwable {
//创建请求模板对象,同时使用Encode接口进行编码操作
RequestTemplate template = buildTemplateFromArgs.create(argv);
Retryer retryer = this.retryer.clone();
//Feign默认提供的重试机制
while (true) {
try {
//发送请求和获取响应结果的核心方法
return executeAndDecode(template);
} catch (RetryableException e) {
retryer.continueOrPropagate(e);
if (logLevel != Logger.Level.NONE) {
logger.logRetry(metadata.configKey(), logLevel);
}
continue;
}
}
}
Object executeAndDecode(RequestTemplate template) throws Throwable {
//多个方法层层调用这里简单贴出主要逻辑
for (RequestInterceptor interceptor : requestInterceptors) {
interceptor.apply(template);
}
Request request = target.apply(new RequestTemplate(template));
if (logLevel != Logger.Level.NONE) {
logger.logRequest(metadata.configKey(), logLevel, request);
}
......
}
}
经过这一系列的操作后,
Request
对象就封装好了,我们来简单看一下这个对象的具体结构:
//请求方法,本例中对应为GET
private final String method;
//要访问的接口地址,本例中对应为/product/getProductInfo/22
private final String url;
//请求头部信息,本例对应的键值对为token=xingren
private final Map<String, Collection<String>> headers;
//请求体,一般POST请求下将复杂对象放入请求体,会被转换成字节数组进行传输
private final byte[] body;
//字符集
private final Charset charset;
3.feign.Client发送请求并对获取结果进行解码
这一步骤Feign的代码量比较多,而且用了大量的try-catch和if-else,整段拿出来展示的话,可读性比较低,我在这里将源码分段进行分析。首先是调用
feign.Client
接口中的
excute()
方法执行请求并获取响应结果。对于
Client
接口,Feign提供了几个不同的实现:(1)默认的实现
Default()
类,内部使用JDK中的
HttpURLConnnection
完成URL请求处理,没有使用连接池,http连接的服用能力弱,性能低,一般不会采用这种方式。(2)当Feign配合Ribbon使用时提供的默认实现类
LoadBalancerFeignClient
,内部使用 Ribben 负载均衡技术完成URL请求处理的
feign.Client
客户端实现类。除此之外,我们也可以使用其他的http客户端来替代Feign底层默认的实现类。比如
ApacheHttpClient
,相比传统JDK自带的URLConnection,增加了易用性和灵活性,它不仅使客户端发送Http请求变得容易,而且也方便开发人员测试接口。既提高了开发的效率,也方便提高代码的健壮性。使用起来也很简单,只需引入依赖,然后增加配置
feign.httpclient.enabled=true
即可。这一步骤,源码简化之后如下:
Response response;
try {
response = client.execute(request, options);
response.toBuilder().request(request).build();
} catch (IOException e) {
//一系列的日志处理
}
其次是对响应对象的null值处理、响应种类的判断以及响应对象消息体的处理,最后会将body转化成字节数组:
//判断返回的响应对象的类型是否正确
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;
}
//转换成字节数组
byte[] bodyData = Util.toByteArray(response.body().asInputStream());
return response.toBuilder().body(bodyData).build();
}
最后一步是针对不同的http状态码来做不同的处理,如果http状态码为成功,再判断是否有返回值,如果有返回值就对响应对象进行解码操作,无返回值直接返回null;如果是404调用解码方法,最后Decoder底层会执行emptyValueOf()方法返回空的Response对象;如果状态码为其他类型,则返回异常。
if (response.status() >= 200 && response.status() < 300) {
if (void.class == metadata.returnType()) {
return null;
} else {
return decode(response);
}
} else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) {
return decode(response);
} else {
throw errorDecoder.decode(metadata.configKey(), response);
}
总结
本文对Feign主要的工作流程做了一些简单的分析,其实关于Feign的原理还有很多细节值得深入探究。另外关于Feign的相关配置本文都省略了,后续将陆续更新Feign使用中的坑以及Feign的各种配置。