04、SpringCloud之Feign组件学习笔记

举报
长路 发表于 2022/11/28 20:06:50 2022/11/28
【摘要】 本节配套案例代码:Gitee仓库、Github仓库所有博客文件目录索引:博客目录索引(持续更新)学习视频:动力节点最新SpringCloud视频教程|最适合自学的springcloud+springcloudAlibabaPS:本章节中部分图片是直接引用学习课程课件,如有侵权,请联系删除。在之前调用生产者的服务是使用RestTemplate,这种是HTTP请求调用,而对于服务调用的终极方案是使用O

@[toc]

前言

本节配套案例代码:Gitee仓库Github仓库

所有博客文件目录索引:博客目录索引(持续更新)

学习视频:动力节点最新SpringCloud视频教程|最适合自学的springcloud+springcloudAlibaba

PS:本章节中部分图片是直接引用学习课程课件,如有侵权,请联系删除。

一、认识Feign

在之前调用生产者的服务是使用RestTemplate,这种是HTTP请求调用,而对于服务调用的终极方案是使用OpenFeign。

1.1、OpenFeign简介

openfeign文档

Feign is a declarative web service client. It makes writing web service clients easier. To use Feign create an interface and annotate it. It has pluggable annotation support including Feign annotations and JAX-RS annotations. Feign also supports pluggable encoders and decoders. Spring Cloud adds support for Spring MVC annotations and for using the same HttpMessageConverters used by default in Spring Web. Spring Cloud integrates Eureka, Spring Cloud CircuitBreaker, as well as Spring Cloud LoadBalancer to provide a load-balanced http client when using Feign.

是一个web客户端,想要使用Feign只需要使用一个接口以及注解,feign能够支持插件式注解,并且提供编解码器,在springcloud中也有对springmvc的注解支持,并且对于Feign本身就自带了负载均衡器

Feign 是一个远程调用的组件 (接口,注解) http 调用的

Feign 集成了 ribbon ,ribbon 里面集成了 eureka。

feign底层默认是ribbon,采用的是轮询算法。

二、实战

2.1、案例1:使用feign来进行远程调用服务

背景

版本:SpringBoot:2.3.12.RELEASE;Spring-Cloud:Hoxton.SR12。

说明:创建两个服务:order以及user。在user服务中会去使用feign来进行调用order服务。

1、创建Order服务

image-20220716190234245

创建SpringBoot项目,选择依赖如下:

image-20220716183718090

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.12.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
    <java.version>1.8</java.version>
    <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

①配置文件:applicaion.yaml

server:
  port: 8081
spring:
  application:
    name: order-service
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka

②在启动器上添加开启eurekaclient注解

@EnableEurekaClient  //开启服务注册

③创建一个服务,用于对外提供服务

package com.changlu.orderservice.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

/**
 * @Description:
 * @Author: changlu
 * @Date: 6:45 PM
 */
@RestController
public class OrderController {

    @GetMapping("/doOrder")
    public String doOrder() {
        return "牛奶泡芙";
    }

}

2、创建User服务(使用feign来进行远程调用order服务)

image-20220716184733404

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.12.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
    <java.version>1.8</java.version>
    <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>

<dependency>           	
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

image-20220716191109246

①配置文件:applicaion.yaml

server:
  port: 8081
spring:
  application:
    name: order-service
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka

②在启动器上添加开启eurekaclient注解以及开启openfeign注解(扫描包)

@EnableEurekaClient  //开启服务注册
@EnableFeignClients //开启feign客户端扫描

③创建对应的feign接口(对应order服务的controller方法)

UserOrderFeign.java

package com.changlu.userservice.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * @Description: 远程调用器
 * @Author: changlu
 * @Date: 6:52 PM
 */
@FeignClient(value = "order-service")//value表示服务名
public interface UserOrderFeign {

    /**
     * 你需要调用哪个controller  就写它的方法签名
     * 方法签名(就是包含一个方法的所有的属性)
     */
    @GetMapping("/doOrder")
    public String doOrder();

}

④创建controller,进行依赖注入发起远程调用

UserController.java

package com.changlu.userservice.controller;

import com.changlu.userservice.feign.UserOrderFeign;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Description:
 * @Author: changlu
 * @Date: 6:50 PM
 */
@RestController
public class UserController {

    @Autowired
    private UserOrderFeign userOrderFeign;

    @GetMapping("/userDoOrder")
    public String userDoOrder() {
        System.out.println("用户来访问接口:/userDoOrder");
        //发起远程调用
        String res = userOrderFeign.doOrder();
        return res;
    }

}

测试案例

我们启动之前的eureka服务,接着启动自己的编写的两个服务:

image-20220716194832652

image-20220716194843869

此时我们来访问:http://localhost:8082/userDoOrder

image-20220716194922189

可以看到远程调用成功!

2.2、超时配置处理

超时异常复现

远程调用openfeign的默认超时时长为1s,在当前的版本背景当中。

我们修改一下Order服务中的处理时长为2s,来看看是否会出现超时异常:

@GetMapping("/doOrder")
public String doOrder() {
    //这里睡眠两秒
    try {
        TimeUnit.SECONDS.sleep(2);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "牛奶泡芙";
}

接着我们重启下order服务,然后去访问网址:http://localhost:8082/userDoOrder

image-20220716195146279

看下报错信息:

image-20220716195241753

方案:配置超时时长,解决报错异常

在User服务中修改配置文件:

image-20220716195458782

ribbon:  #feign 默认调用 1s 超时
  ReadTimeout: 3000   #修改调用时长为 5s
  ConnectTimeOut: 5000  # 修改连接时长为5s

此时再访问地址:http://localhost:8082/userDoOrder

image-20220716195601239

可以看到能够得到访问结果。

2.3、案例2:参数传递案例(单独日期特别处理)

常见请求体+参数案例

我们在Order服务中添加请求参数的一些请求接口:

image-20220716202703538

ParamController.java

package com.changlu.orderservice.controller;

import com.changlu.orderservice.domain.Order;
import org.springframework.web.bind.annotation.*;

import java.util.Date;

/**
 * url    /doOrder/热干面/add/油条/aaa
 * get传递一个参数
 * get传递多个参数
 * post传递一个对象
 * post传递一个对象+一个基本参数
 */
@RestController
public class ParamController {

    //url参数:PathVariable
    @GetMapping("testUrl/{name}/and/{age}")
    public String testUrl(@PathVariable("name") String name, @PathVariable("age") Integer age) {
        System.out.println(name + ":" + age);
        return "ok";
    }

    //请求参数:?xx=xx
    @GetMapping("oneParam")
    public String oneParam(@RequestParam(required = false) String name) {
        System.out.println(name);
        return "ok";
    }

    //请求参数:?xx=xx&xx=xx
    @GetMapping("twoParam")
    public String twoParam(@RequestParam(required = false) String name, @RequestParam(required = false) Integer age) {
        System.out.println(name);
        System.out.println(age);
        return "ok";
    }

    //请求体参数
    @PostMapping("oneObj")
    public String oneObj(@RequestBody Order order) {
        System.out.println(order);
        return "ok";
    }

    //请求体 + 请求参数url中的xx=xx
    @PostMapping("oneObjOneParam")
    public String oneObjOneParam(@RequestBody Order order,@RequestParam("name") String name) {
        System.out.println(name);
        System.out.println(order);
        return "ok";
    }
}

image-20220716202851553

接着我们来在user服务中添加对应的feign的接口方法,然后再UserController的某个接口方法里来进行远程调用:

UserOrderFeign.java·:

package com.changlu.userservice.feign;

import com.changlu.userservice.domain.Order;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

import java.util.Date;

/**
 * @Description: 远程调用器
 * @Author: changlu
 * @Date: 6:52 PM
 */
@FeignClient(value = "order-service")//value表示服务名
public interface UserOrderFeign {

    /**
     * 你需要调用哪个controller  就写它的方法签名
     * 方法签名(就是包含一个方法的所有的属性)
     */
    @GetMapping("/doOrder")
    public String doOrder();


    //*****请求参数测试案例*****
    //url参数:PathVariable
    @GetMapping("testUrl/{name}/and/{age}")
    public String testUrl(@PathVariable("name") String name, @PathVariable("age") Integer age);

    //请求参数:?xx=xx
    @GetMapping("oneParam")
    public String oneParam(@RequestParam(required = false) String name);

    //请求参数:?xx=xx&xx=xx
    @GetMapping("twoParam")
    public String twoParam(@RequestParam(required = false) String name, @RequestParam(required = false) Integer age);

    //请求体参数
    @PostMapping("oneObj")
    public String oneObj(@RequestBody Order order);

    //请求体 + 请求参数url中的xx=xx
    @PostMapping("oneObjOneParam")
    public String oneObjOneParam(@RequestBody Order order,@RequestParam("name") String name);

}

UserController.java

package com.changlu.userservice.controller;

import com.changlu.userservice.domain.Order;
import com.changlu.userservice.feign.UserOrderFeign;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;

/**
 * @Description:
 * @Author: changlu
 * @Date: 6:50 PM
 */
@RestController
public class UserController {
    @GetMapping("/testParam")
    public String testParam() {
        //第一个:url路径携带参数
        String s1 = userOrderFeign.testUrl("changlu", 18);
        System.out.println(s1);

        //第二个:1个请求参数
        String s2 = userOrderFeign.oneParam("changlu");
        System.out.println(s2);

        //第三个:两个请求参数
        String s3 = userOrderFeign.twoParam("changlu", 666);
        System.out.println(s3);

        Order order = Order.builder()
                .name("泡芙")
                .price(1000.0)
                .time(new Date())
                .id(1)
                .build();
        //第四个:请求体
        String s4 = userOrderFeign.oneObj(order);
        System.out.println(s4);

        //第五个:第一个请求体+参数
        String s5 = userOrderFeign.oneObjOneParam(order, "changlu");
        System.out.println(s5);
        return "ok";
    }
}

测试一下:

image-20220716203131939

image-20220716203141235

单独日期传递【特别注意】

Order服务:

//ParamController
//时间请求参数
@GetMapping("testTime")
public String testTime(@RequestParam Date date){
    System.out.println(date);
    return "ok";
}

User服务:

//接口:UserOrderFeign
@GetMapping("testTime")
public String testTime(@RequestParam Date date);

//控制器类:UserController
/**
     * Sun Mar 20 10:24:13 CST 2022
     * Mon Mar 21 00:24:13 CST 2022  +- 14个小时
     * 1.不建议单独传递时间参数
     * 2.转成字符串(推荐,比较好的方案,在服务端可以进行format转为date对象)   2022-03-20 10:25:55:213 因为字符串不会改变
     * 3.jdk LocalDate 年月日 没问题    LocalDateTime 会丢失秒
     * 4.改feign的源码
     *
     * @return
     */
@GetMapping("/testTime")
public String testTime(){
    //错误示例1:使用new Date();  
    userOrderFeign.testTime(new Date());
    //正确方案:服务端的请求接口不应该使用Date来进行接收,尽可能使用字符串、LocalDate来进行传递
    return "ok";
}

错误案例Order服务打印结果(时间不准确):Sun Jul 17 10:26:20 CST 2022

2.4、日志处理

需求:若是我们想要知道每一个请求的详细请求参数,那么我们可以来进行打开日志配置以及来注入相应的日志等级。

操作1:在启动器或者配置类中注入一个日志等级。

/**
     * 打印fein日志信息 级别
     * @return
     */
@Bean
public Logger.Level level(){
    return Logger.Level.FULL;
}

操作2:在配置文件yaml中打开配置。

# 日志等级选择
logging:
  level:
    com.changlu.userservice.feign.UserOrderFeign: debug  # 我需要答应这个接口下面的日志

接着我们来测试一下:访问网址http://localhost:8082/testTime

此时就可以看到feign发起请求的一系列参数如下:

image-20220716204313563

三、手写feign简易实现

本质:feign本质就是进行发送http请求,并且再此基础上具备负载均衡的功能,我们来对其进行复现一下。

我们来接着二实战中User服务中UserOrderFeign接口,来对该接口中的doOrder方法进行代理:

image-20220716205352394

①首先我们来增强RestTemplate方法,另起能够具备负载均衡的效果。

在对应的启动器类UserserviceApplication中添加该bean:

@Bean
@LoadBalanced
public RestTemplate restTemplate() {
    return new RestTemplate();
}

②之后我们在test测试包中来进行代理类编写

package com.changlu.userservice;

import com.changlu.userservice.controller.UserController;
import com.changlu.userservice.feign.UserOrderFeign;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.client.RestTemplate;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

@SpringBootTest
class UserserviceApplicationTests {

    @Autowired
    private RestTemplate restTemplate;

    @Test
    void contextLoads() {
        UserOrderFeign feign = (UserOrderFeign)Proxy.newProxyInstance(UserController.class.getClassLoader(), new Class[]{UserOrderFeign.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                String res = null;
                if (method.getName().equals("doOrder")) {
                    //1、获取到对应的path路径
                    // 能去拿到对方的ip和port 并且拿到这个方法上面的注解里面的值 那么就完事了
                    GetMapping annotation = method.getAnnotation(GetMapping.class);
                    String[] paths = annotation.value();
                    String path = paths[0];
                    //2、根据对应的的方法来获取到相应的class字节码
                    Class<?> aclass = method.getDeclaringClass();
                    //3、获取到对应feign类的注解,取得对应的服务名
                    FeignClient fannoation = aclass.getAnnotation(FeignClient.class);
                    String serviceName = fannoation.value();
                    //4、拼接服务地址
                    String url = "http://" + serviceName + path;
                    //5、发送请求(使用resttemplate来进行发送请求)
                    res = restTemplate.getForObject(url, String.class);
                }
                return res;
            }
        });
        System.out.println(feign.doOrder());
    }

}

image-20220716205540644

测试成功!

四、feign的源码分析

4.1、OpenFeign的原理分析

使用动态代理jdk (invoke) 及cglib 子类继承的 :

源码简述

1、给接口创建代理对象(启动扫描)

2、代理对象执行进入 invoke 方法

3、在 invoke 方法里面做远程调用

具体流程

1、通过注解扫描来获取到调用服务名称以及对应的接口url。

image-20220716205940726

2、拼接:``http://服务名/doOrder,首先会通过ribbon从注册中心中根据服务名获取对应的访问地址,最终根据负载均衡策略确定得到一个服务,最终得到的对应的url地址:http://ip:port/doOrder`

3、最终发起请求,来进行远程调用。

4.2、如何扫描注解@FeignClient?

在启动器上我们添加了一个开启Feign扫描的注解:

@EnableFeignClients //开启feign客户端扫描

//看下对应的注解源码
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({FeignClientsRegistrar.class})  //FeignClientsRegistrar是Feign的注册类
public @interface EnableFeignClients {
    String[] value() default {};

    String[] basePackages() default {};

    Class<?>[] basePackageClasses() default {};

    Class<?>[] defaultConfiguration() default {};

    Class<?>[] clients() default {};
}

进入 FeignClientsRegistrar 这个类 去查看里面的东西

image-20220716211350955

public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    this.registerDefaultConfiguration(metadata, registry);
    //扫描注解来进行一个注册(@FeignClient)
    this.registerFeignClients(metadata, registry);
}

//AnnotationMetadata即注解的元信息
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet();
    //获取到启动类上的feign注解
    Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
    Class<?>[] clients = attrs == null ? null : (Class[])((Class[])attrs.get("clients"));
    if (clients != null && clients.length != 0) {
        Class[] var12 = clients;
        int var14 = clients.length;

        for(int var16 = 0; var16 < var14; ++var16) {
            Class<?> clazz = var12[var16];
            candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
        }
    } else {
        ClassPathScanningCandidateComponentProvider scanner = this.getScanner();
        scanner.setResourceLoader(this.resourceLoader);
        scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
        Set<String> basePackages = this.getBasePackages(metadata);
        Iterator var8 = basePackages.iterator();
		
        //循环遍历得到对应的包名
        while(var8.hasNext()) {
            String basePackage = (String)var8.next();
            candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
        }
    }

    Iterator var13 = candidateComponents.iterator();

    while(var13.hasNext()) {
        BeanDefinition candidateComponent = (BeanDefinition)var13.next();
        if (candidateComponent instanceof AnnotatedBeanDefinition) {
            AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition)candidateComponent;
            AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
            Assert.isTrue(annotationMetadata.isInterface(), "@FeignClient can only be specified on an interface");
            //拿到接口上的注解
            Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(FeignClient.class.getCanonicalName());
            String name = this.getClientName(attributes);
            this.registerClientConfiguration(registry, name, attributes.get("configuration"));
            //将创建的代理对象registry交给spring
            this.registerFeignClient(registry, annotationMetadata, attributes);
        }
    }

}

4.3、如何创建代理对象去执行调用?

在启动时,在ReflectiveFeign的newInstance方法中,给接口创建了代理对象

public class ReflectiveFeign extends Feign {
    
	public <T> T newInstance(Target<T> target) {
        Map<String, MethodHandler> nameToHandler = this.targetToHandlersByName.apply(target);
        Map<Method, MethodHandler> methodToHandler = new LinkedHashMap();
        List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList();
        Method[] var5 = target.type().getMethods();
        int var6 = var5.length;

        for(int var7 = 0; var7 < var6; ++var7) {
            Method method = var5[var7];
            if (method.getDeclaringClass() != Object.class) {
                if (Util.isDefault(method)) {
                    DefaultMethodHandler handler = new DefaultMethodHandler(method);
                    defaultMethodHandlers.add(handler);
                    methodToHandler.put(method, handler);
                } else {
                    methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
                }
            }
        }

        InvocationHandler handler = this.factory.create(target, methodToHandler);
        //创建代理对象
        T proxy = Proxy.newProxyInstance(target.type().getClassLoader(), new Class[]{target.type()}, handler);
        Iterator var12 = defaultMethodHandlers.iterator();

        while(var12.hasNext()) {
            DefaultMethodHandler defaultMethodHandler = (DefaultMethodHandler)var12.next();
            defaultMethodHandler.bindTo(proxy);
        }

        return proxy;
    }
}

ReflectiveFeign 类中的 invoke 方法帮我们完成调用:

image-20220716212219953

当我们去进行远程调用的时候可以debug到对应的断点:

image-20220716212610473

SynchronousMethodHandlerinvoke中给每一个请求创建了一个requestTemplate 对 象,去执行请求:

final class SynchronousMethodHandler implements MethodHandler {
    
    //执行请求走对应的invoke方法
    public Object invoke(Object[] argv) throws Throwable {
        //可以看到构建了一个RequestTemplate
        RequestTemplate template = this.buildTemplateFromArgs.create(argv);
        Options options = this.findOptions(argv);
        Retryer retryer = this.retryer.clone();

        while(true) {
            try {
                //交给ribbon来进行请求调用
                return this.executeAndDecode(template, options);
            } catch (RetryableException var9) {
                ...
            }
        }
    }
}

Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
        Request request = this.targetRequest(template);
        if (this.logLevel != Level.NONE) {
            this.logger.logRequest(this.metadata.configKey(), this.logLevel, request);
        }
        long start = System.nanoTime();
        Response response;
        try {
            //这里client指的是LoadBalancerFeignClient,就是对应的负载均衡客户端请求工具
            response = this.client.execute(request, options);
            response = response.toBuilder().request(request).requestTemplate(template).build();
        } catch (IOException var12) {
            if (this.logLevel != Level.NONE) {
                this.logger.logIOException(this.metadata.configKey(), this.logLevel, var12, this.elapsedTime(start));
            }

            throw FeignException.errorExecuting(request, var12);
        }
}

此时就会走LoadBalancerFeignClient中的execute():

public class LoadBalancerFeignClient implements Client {
    
    public Response execute(Request request, Options options) throws IOException {
        try {
            //整理得到对应的url
            URI asUri = URI.create(request.url());
            String clientName = asUri.getHost();
            URI uriWithoutHost = cleanUrl(request.url(), clientName);
            RibbonRequest ribbonRequest = new RibbonRequest(this.delegate, request, uriWithoutHost);
            IClientConfig requestConfig = this.getClientConfig(options, clientName);
            //执行处理根据负载均衡处理器
            return ((RibbonResponse)this.lbClient(clientName).executeWithLoadBalancer(ribbonRequest, requestConfig)).toResponse();
        } catch (ClientException var8) {
            IOException io = this.findIOException(var8);
            if (io != null) {
                throw io;
            } else {
                throw new RuntimeException(var8);
            }
        }
    }
}

接着会走AbstractLoadBalancerAwareClient的executeWithLoadBalancer:

 public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
        LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig);

        try {
            return command.submit(
                new ServerOperation<T>() {
                    //此时Server对象会取得最终的ip地址,例如:ip地址:port
                    @Override
                    public Observable<T> call(Server server) {
                        URI finalUri = reconstructURIWithServer(server, request.getUri());
                        S requestForServer = (S) request.replaceUri(finalUri);
                        try {
                            //这里才是真正进行发送请求操作
                            return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
                        } 
                        catch (Exception e) {
                            return Observable.error(e);
                        }
                    }
                })
                .toBlocking()
                .single();
        } catch (Exception e) {
            Throwable t = e.getCause();
            if (t instanceof ClientException) {
                throw (ClientException) t;
            } else {
                throw new ClientException(e);
            }
        }
        
    }

最终在FeignLoadBalancer中的execute方法中完成远程方法访问:

public FeignLoadBalancer.RibbonResponse execute(FeignLoadBalancer.RibbonRequest request, IClientConfig configOverride) throws IOException {
    Options options;
    if (configOverride != null) {
        RibbonProperties override = RibbonProperties.from(configOverride);
        options = new Options((long)override.connectTimeout(this.connectTimeout), TimeUnit.MILLISECONDS, (long)override.readTimeout(this.readTimeout), TimeUnit.MILLISECONDS, override.isFollowRedirects(this.followRedirects));
    } else {
        options = new Options((long)this.connectTimeout, TimeUnit.MILLISECONDS, (long)this.readTimeout, TimeUnit.MILLISECONDS, this.followRedirects);
    }
	//最终这里完成了远程方法调用
    Response response = request.client().execute(request.toRequest(), options);
    return new FeignLoadBalancer.RibbonResponse(request.getUri(), response);
}

其实本质就是发送的HttpURLConnection

image-20220716214656542

public Response execute(Request request, Options options) throws IOException {
    //发起HttpURLConnection请求
    HttpURLConnection connection = this.convertAndSend(request, options);
    //转转响应体
    return this.convertResponse(connection, request);
}

Response convertResponse(HttpURLConnection connection, Request request) throws IOException {
    //获取到状态码
    int status = connection.getResponseCode();
    String reason = connection.getResponseMessage();
    if (status < 0) {
        throw new IOException(String.format("Invalid status(%s) executing %s %s", status, connection.getRequestMethod(), connection.getURL()));
    } else {
        Map<String, Collection<String>> headers = new LinkedHashMap();
        Iterator var6 = connection.getHeaderFields().entrySet().iterator();

        while(var6.hasNext()) {
            Entry<String, List<String>> field = (Entry)var6.next();
            if (field.getKey() != null) {
                headers.put(field.getKey(), field.getValue());
            }
        }

        Integer length = connection.getContentLength();
        if (length == -1) {
            length = null;
        }

        InputStream stream;
        if (status >= 400) {
            stream = connection.getErrorStream();
        } else {
            stream = connection.getInputStream();
        }

        return Response.builder().status(status).reason(reason).headers(headers).request(request).body(stream, length).build();
    }
}

4.4、feign调用问题快速定位

只要是 feign 调用出了问题,看 feign 包下面的 Client 接口下面的 108 行。

image-20220716215351377

我们可以根据对应响应的状态码来进行定位:

200:成功         400:请求参数错误   401:没有权限 
403:权限不够     404:路径不匹配     405:方法不允许 
500:提供者报错了  302:资源重定向

五、OpenFeign总结

OpenFeign 主要基于接口和注解实现了远程调用。

源码总结:面试

1、OpenFeign 用过吗?它是如何运作的?

在主启动类上加上@EnableFeignClients 注解后,启动会进行包扫描,把所有加了 @FeignClient(value=”xxx-service”)注解的接口进行创建代理对象通过代理对象,使用 ribbon 做了负载均衡和远程调用

2、如何创建的代理对象?

当 项 目 在 启 动 时 , 先 扫 描 , 然 后 拿 到 标 记 了 @FeignClient 注 解 的 接 口 信 息 , 由 ReflectiveFeign 类的 newInstance 方法创建了代理对象 JDK 代理

3、OpenFeign 到底是用什么做的远程调用?

使用的是 HttpURLConnection (java.net)
  1. OpenFeign 怎么和 ribbon 整合的?
在代理对象执行调用的时候

image-20220716215854451

参考文章

[1]. 微服务RPC框架-Feign

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。