SpringCloud关于远程调用Feign的知识点

文章详细介绍了Feign如何通过注解实现微服务间的远程调用,包括@FeignClient和@EnableFeignClients的使用。同时,讨论了JDK动态代理在Feign中的应用,以及Feign的工作流程,包括创建本地接口代理、封装Request对象和发送请求。此外,文章还提到了Feign与Ribbon的结合以及Client的实现策略。
摘要由CSDN通过智能技术生成

一、Feign实现远程调用的注解和配置

首先对于微服务之间的相互调用,如果微服务B可以单独启动起来,并注册到注册中心。那么我们在微服务A的启动类上加@EnableFeignClients注解,这就表明当前应用服务A中有的地方想要引用其他微服务(微服务B)中的接口。如下图所示,在启动类和接口中分别加上@EnableFeignClients注解和@FeignClient注解,就能实现远程微服务的调用:

 

 后端接收前端传输的消息,需要使用到@RequestMapping接收映射的URI路径,使用@RequestParam和@RequestVariable和@RequestBody接收参数,对于多个参数的URI,可以使用@RequestParam接收参数,对于单个参数可以不用加注解,直接在方法中使用同名参数接收即可。对于/***/{}的URI,需要使用@RequestVariable接收参数。对于对象的传递,使用@RequestBody接收。

对于Restful风格的资源处理,GET、PUT、POST、DELETE等价于资源处理的select,update,insert,delete四种方式。restful的四种注解,@GetMapping,@PostMapping,@PutMapping,@DeleteMapping等价于@RequestMapping(method = RequestMethod.GET/POST/PUT/DELETE)

@FeignClient标签中的一些属性的作用如下所示:

value:服务名(接口提供方的服务名)

name:指定FeignClient的名称,如果项目使用了Ribbon,name属性会作为微服务的名称,用于服务发现

url:url一般用于调试,可以手动指定@FeignClient调用的地址

decode404:当发生http 404错误时,如果该字段位true,会调用decoder进行解码,否则抛出FeignException

configuration:Feign配置类,可以自定义Feign的Encoder、Decoder、LogLevel、Contract

fallback:定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback指定的类必须实现@FeignClient标记的接口

fallbackFactory:工厂类,用于生成fallback类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码

path:定义当前FeignClient的统一前缀

对于注册发现的微服务架构而言,fegin接口下的方法对应的URL是通过value对应的微服务的ip+端口号的地址+URL映射路径访问对应功能的。如下图所示,通过service-product微服务的对应IP地址和端口加上/product/select的URL进而访问对应的功能。

二、JDK的动态代理

代理模式分为静态代理和动态代理两种方式。其中静态代理是指代理关系在编译期确定的代理模式,使用静态代理时,常见的作法是为每个业务类抽象一个接口,对应地创建一个代理类。动态代理是指代理关系在运行时确定的代理模式,动态代理的核心API是Proxy类和InvocationHandler接口,他的原理是利用反射机制在运行时生成代理类的字节码。动态代理的使用场景是当一个对象不能直接使用,可以在客户端和目标对象创建一个中介,这个中介就是代理。

三、Feign底层工作原理

Feign的工作过程主要分为三个步骤:

1:通过动态代理在本地实例化远程接口

2:封装Request对象并进行编程

3:发送请求并对获取结果进行编码

1、创建远程接口的本地代理实例

如果在SpringBoot的启动类上加上注解@EnableFeignCleints,这个注解就相当于Feign组件的一个入口,当使用@EnableFeignCleints后,程序启动后会进行包扫描,扫描所有被@FeignCleint注解修饰的接口,通过JDK底层的动态代理为远程接口创建代理实例,并注册到IOC容器中。

在@EnableFeignCleints注解中引入FeignClientsRegistrar类,这个类在启动时调用registerBeanDefinitions()方法,在这个方法里面加载Feign的各项配置,并注册实例化对象。方法的主要代码如下所示:

//获取扫描的包路径
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);
    }
}

我们可以看到在registerBeanDefinitions()方法中主要是先通过扫描基础包路径,然后找到被@FeignClient注解修饰的接口,放入Set中,接着遍历集合为每个接口逐一创建实例化对象。其中registerFeignClient(registry, annotationMetadata, attributes);是创建代理对象的方法。我们继续深入registerFeignClient()的代码,最终创建实际对象的核心代码是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也是基于代理模式来创建接口的实例化对象。

2、封装Request对象并进行编码

当调用远程微服务的方法时,底层通过JDK的动态代理交给Feign的代理类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(RequestTemplateinput())方法根据刚刚获取到的请求模板对象创建远程服务请求的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.Client接口中的excute()方法执行请求并获取响应结果。

对于Client接口,Feign提供了几个不同的实现:

(1)默认的实现Default()类,内部使用JDK中的HttpURLConnextion完成URL请求处理,没有使用连接池,http连接的复用能力弱,性能低,一般不会采用这种方式。

(2)当Feign配合Ribbon使用时提供默认实现类LoadBalanceFeignClient,内部使用Ribbon负载均衡技术完成URL请求处理的feign.Client客户端实现类。对于feign.Client发送请求并对获取结果进行解码的源码简化如下所示:

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);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值