Feign的工作原理

  • Post author:
  • Post category:其他




Feign的简单介绍

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的各种配置。



版权声明:本文为weixin_43111776原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。