程序项目代做,有需求私信(vue、React、Java、爬虫、电路板设计、嵌入式linux等)

Spring Cloud -- OpenFeign 核心原理

当今是微服务横行的时代,各个微服务之间相互调用是一件再平常不过的时候。在采用HTTP协议进行通信的微服务中,我们自己可能去封装一个HttpClient工具类去进行服务间的调用,封装一个HttpClient工具,我们就需要考虑一下这些事情:

  • 我们在发送一个HTTP请求时,我们需要选择请求方式GET、POST、DELETE等,我们需要构建请求参数、构建请求头信息等,那么作为一个工具类我们是不是也要提供各种参数的灵活配置;
  • 因为采用restful API 风格的HTTP请求参数和返回数据都是字符串的格式,那我们是否需要考虑序列化和反序列化问题;
  • 当同一个服务部署到多台服务器的时候,我们是不是应该采用轮询或者随机的方式去选择服务器,这也就是我们常说的负载均衡。从另一方面来说我们的核心是解决服务间的调用,但是我们在设计一个通用HttpClient工具的时候是否也应该支持负载均衡,以及如何和负载均衡高度解耦。

为此,大名鼎鼎的Feign应时而生,我们在学习Feign的实现的时候,我们应该带着这些问题去学习Feign的实现原理。

一、什么是Feign

Feign 是声明式 Web 服务客户端,它使编写 Web 服务客户端更加容易 Feign 不做任何请求处理,通过处理注解相关信息生成 Request,并对调用返回的数据进行解码,从而实现简化HTTP API 的开发。当然你也可以直接使用 Apache HttpClient 来实现Web服务器客户端,但是 Feign 的目的是尽量的减少资源和代码来实现和 HTTP API 的连接。通过自定义的编码解码器以及错误处理,你可以编写任何基于文本的 HTTP API。

如果要使用 Feign,需要创建一个接口并对其添加 Feign 相关注解,另外 Feign 还支持可插拔编码器和解码器,致力于打造一个轻量级 HTTP 客户端。

如果你想直接使用原生的Feign的话,你可以去参考Feign配置使用,下面就是Feign针对一个HTTP API的接口定义:

interface GitHub {
  // RequestLine注解声明请求方法和请求地址,可以允许有查询参数
  @RequestLine("GET /user/list")
  List<User> list();
}

目前由于Spring Cloud微服务的广泛使用,广大开发者更倾向于使用spring-cloud-starter-openfeign,Spring Cloud 添加了对 Spring MVC 注解的支持,在微服务中我们的接口定义有所变化:

@FeignClient(name="服务名",contextId="唯一标识")
interface GitHub {
  @GetMapping("/user/list")
  List<User> list();
}

二、Feign 和 Openfeign 的区别

Feign 最早是由 Netflix 公司进行维护的,后来 Netflix 不再对其进行维护,最终 Feign 由社区进行维护,更名为 Openfeign。

2.1 Starter OpenFeign

当然了,基于 SpringCloud 团队对 Netflix 的情有独钟,你出了这么好用的轻量级 HTTP 客户端,我这老大哥不得支持一下,所以就有了基于 Feign 封装的 Starter。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

这个包引入了如下依赖:

<dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter</artifactId>
      <version>2.2.0.RELEASE</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-openfeign-core</artifactId>
      <version>2.2.0.RELEASE</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
      <version>5.2.1.RELEASE</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-commons</artifactId>
      <version>2.2.0.RELEASE</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>io.github.openfeign</groupId>
      <artifactId>feign-core</artifactId>
      <version>10.4.0</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>io.github.openfeign</groupId>
      <artifactId>feign-slf4j</artifactId>
      <version>10.4.0</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>io.github.openfeign</groupId>
      <artifactId>feign-hystrix</artifactId>
      <version>10.4.0</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>io.github.openfeign</groupId>
      <artifactId>feign-java8</artifactId>
      <version>10.4.0</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
      <version>2.2.0.RELEASE</version>
      <scope>compile</scope>
      <optional>true</optional>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-archaius</artifactId>
      <version>2.2.0.RELEASE</version>
      <scope>compile</scope>
      <optional>true</optional>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-loadbalancer</artifactId>
      <version>2.2.0.RELEASE</version>
      <scope>compile</scope>
      <optional>true</optional>
    </dependency>
View Code

这里面有两个非常重要的包:

  • 一个是spring-cloud-openfeign-core,这个包是SpringCloud支持Feign的核心包,Spring Cloud 添加了对 Spring MVC 注解的支持(通过SpringMvcContract实现),并支持使用 Spring Web 中默认使用的相同 HttpMessageConverters。另外,Spring Cloud同时集成了Ribbon和Eureka以及Spring Cloud LoadBalancer,以在使用 Feign 时提供负载均衡的 HTTP客户端。针对于注册中心的支持,包含但不限于 Eureka,比如 Consul、Naocs 等注册中心均支持。
  • 另一个包是feign-core,也就是feign的原生包,具体使用细节可以参考Feign配置使用。通俗点说,spring-cloud-openfeign-core就是通过一系列的配置创建Feign.builder()实例的过程。

在我们 SpringCloud 项目开发过程中,使用的大多都是这个 Starter Feign。

2.2 demo

为了方便大家理解,这里写出对应的生产方、消费方 Demo 代码,以及使用的注册中心。

生产者服务:添加 Nacos 服务注册发现注解以及发布出 HTTP 接口服务

@EnableDiscoveryClient 
@SpringBootApplication
public class NacosProduceApplication {
    public static void main(String[] args) {
        SpringApplication.run(NacosProduceApplication.class, args);
    }
    @RestController
    static class TestController {
        @GetMapping("/hello")
        public String hello(@RequestParam("name") String name) {
            return "hello " + name;
        }
    }
}

消费者服务:

定义 FeignClient 消费服务接口

@FeignClient(name= "nacos-produce",contextId="DemoFeignClient")
public interface DemoFeignClient {
    @GetMapping("/hello")
    String sayHello(@RequestParam("name") String name);
}

因为生产者使用 Nacos,所以消费者除了开启 Feign 注解,同时也要开启 Naocs 服务注册发现。

@RestController 
@EnableFeignClients
@EnableDiscoveryClient 
@SpringBootApplication
public class NacosConsumeApplication {
    public static void main(String[] args) {
        SpringApplication.run(NacosConsumeApplication.class, args);
    }

    @Autowired private DemoFeignClient demoFeignClient;

    @GetMapping("/test")
    public String test() {
        String result = demoFeignClient.sayHello("xxxxx");
        return result;
    }
}

三、Feign 的启动原理

下文中调试中使用的代码并不是demo中的代码,不过和demo使用的类似,只是业务系统更加复杂而已。

我们在 SpringCloud 的使用过程中,如果想要启动某个组件,一般都是 @Enable... 这种方式注入,Feign 也不例外,我们需要在类上标记此注解 @EnableFeignClients。

3.1 @EnableFeignClients

EnableFeignClients注解,用于扫描使用@FeignClient注解标注的接口, 而该功能是通过@Import(FeignClientsRegistrar.class)完成。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {

    String[] value() default {};

    String[] basePackages() default {};

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

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

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

}

FeignClientsRegistrar实现了ImportBeanDefinitionRegistrar, 用以完成相关Bean注册。

ImportBeanDefinitionRegistrar 负责动态注入 IOC Bean,分别注入 Feign 配置类、FeignClient。

class FeignClientsRegistrarimplements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware{
        @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata,
            BeanDefinitionRegistry registry) {
        registerDefaultConfiguration(metadata, registry);
        registerFeignClients(metadata, registry);
    }
       ...
}

3.2 registerDefaultConfiguration(metadata, registry)

    private void registerDefaultConfiguration(AnnotationMetadata metadata,
            BeanDefinitionRegistry registry) {
        Map<String, Object> defaultAttrs = metadata
                .getAnnotationAttributes(EnableFeignClients.class.getName(), true);

        if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
            String name;
            if (metadata.hasEnclosingClass()) {
                name = "default." + metadata.getEnclosingClassName();
            }
            else {
                name = "default." + metadata.getClassName();
            }
            registerClientConfiguration(registry, name,
                    defaultAttrs.get("defaultConfiguration"));
        }
    }

private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
            Object configuration) {
        BeanDefinitionBuilder builder = BeanDefinitionBuilder
                .genericBeanDefinition(FeignClientSpecification.class);
        builder.addConstructorArgValue(name);
        builder.addConstructorArgValue(configuration);
        registry.registerBeanDefinition(
                name + "." + FeignClientSpecification.class.getSimpleName(),
                builder.getBeanDefinition());
    }
View Code

1. 获取 @EnableFeignClients 注解上的属性以及对应 value。

2.使用BeanDefinitionBuilder构造器为FeignClientSpecification类生成BeanDefinition,这个BeanDefinition是对FeignClientSpecification Bean的定义,保存了FeignClientSpecification   Bean 的各种信息,如属性、构造方法参数等。其中@EnableFeignClients 注解上的defaultConfiguration属性就是作为构造方法参数传入的。而bean名称为 default. + @EnableFeignClients 修饰类(一般是启动类)全限定名称 + FeignClientSpecification

3.@EnableFeignClients defaultConfiguration 默认为 {},如果没有相关配置,默认使用 FeignClientsConfiguration 并结合 name 填充到 FeignClientSpecification,最终会被注册为 IOC Bean

总结下来,就是根据@EnableFeignClients中属性defaultConfiguration,为FeignClientSpecification类型生成BeanDefinition,并注入Spriing容器中。

3.3 registerFeignClients(metadata, registry)

public void registerFeignClients(AnnotationMetadata metadata,
            BeanDefinitionRegistry registry) {
        ClassPathScanningCandidateComponentProvider scanner = getScanner();
        scanner.setResourceLoader(this.resourceLoader);

        Set<String> basePackages;

        Map<String, Object> attrs = metadata
                .getAnnotationAttributes(EnableFeignClients.class.getName());
        AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
                FeignClient.class);
        final Class<?>[] clients = attrs == null ? null
                : (Class<?>[]) attrs.get("clients");
        if (clients == null || clients.length == 0) {
            scanner.addIncludeFilter(annotationTypeFilter);
            basePackages = getBasePackages(metadata);
        }
        else {
            final Set<String> clientClasses = new HashSet<>();
            basePackages = new HashSet<>();
            for (Class<?> clazz : clients) {
                basePackages.add(ClassUtils.getPackageName(clazz));
                clientClasses.add(clazz.getCanonicalName());
            }
            AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
                @Override
                protected boolean match(ClassMetadata metadata) {
                    String cleaned = metadata.getClassName().replaceAll("\\$", ".");
                    return clientClasses.contains(cleaned);
                }
            };
            scanner.addIncludeFilter(
                    new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
        }

        for (String basePackage : basePackages) {
            Set<BeanDefinition> candidateComponents = scanner
                    .findCandidateComponents(basePackage);
            for (BeanDefinition candidateComponent : candidateComponents) {
                if (candidateComponent instanceof AnnotatedBeanDefinition) {
                    // verify annotated class is an interface
                    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 = getClientName(attributes);
                    registerClientConfiguration(registry, name,
                            attributes.get("configuration"));

                    registerFeignClient(registry, annotationMetadata, attributes);
                }
            }
        }
    }
View Code

1. 扫描使用FeignClient注解标注的接口,获取basePackages扫描路径 。

2.根据获取到的扫描路径,然后根据扫描路径,获取该路径将其子路径下,使用FeignClient注解标记的接口。

3.遍历每一个FeignClient注解的类:

  • 收集接口FeignClient注解属性信息,并根据 configuration 属性去创建接口级的 FeignClientSpecification BeanDefinition,然后注入Spring容器。

  • 生成FeignClientFactoryBean 类型的BeanDefinition,并将 @FeignClient 的属性设置到 FeignClientFactoryBean 对象上,然后注入Spring容器。

其中需要注意,在将@FeignClient 的属性设置到 FeignClientFactoryBean 对象,会将@FeignClient的修饰的类的className作为type属性,传递给FeignClientFactoryBean,后续正是通过这个,创建对应的代理类。

总结下来,就是为一个@FeignClient创建一个FeignClientSpecification、FeignClientFactoryBean,FeignClientSpecification保存这个@FeignClient的configuration 属性信息,而FeignClientFactoryBean中收集了这个FeignClient其他的属性。

由于FeignClientFactoryBean 继承自 FactoryBean,也就是说,当我们定义 @FeignClient 修饰接口时,注册到 IOC 容器中 Bean 类型变成了 FeignClientFactoryBean,在 Spring 中,FactoryBean 是一个工厂 Bean,用来创建代理 Bean。工厂 Bean 是一种特殊的 Bean,对于需要获取 Bean 的消费者而言,它是不知道 Bean 是普通 Bean 或是工厂 Bean 的。工厂 Bean 返回的实例不是工厂 Bean 本身,该实例是由工厂 Bean 中 FactoryBean#getObject 逻辑所创建的。更多FactoryBean相关的信息,可以阅读我之前的博客

四、 FeignClient创建过程分析

上面说到 @FeignClient 修饰的接口最终填充到 IOC 容器的类型是 FeignClientFactoryBean,先来看下它是什么。

 4.1 FactoryBean 接口特征

1 .它会在类初始化时执行一段逻辑,依据InitializingBean 接口。

2.如果它被别的类 @Autowired 进行注入,返回的不是它本身,而是 FactoryBean#getObject 返回的类,依据 Spring FactoryBean 接口。

3.它能够获取 Spring 上下文对象,依据 Spring ApplicationContextAware 接口。

先来看它的初始化逻辑都执行了什么:

@Override
public void afterPropertiesSet() {
    Assert.hasText(contextId, "Context id must be set");
    Assert.hasText(name, "Name must be set");
}

没有特别的操作,只是使用断言工具类判断两个字段不为空。ApplicationContextAware 也没什么说的,获取上下文对象赋值到对象的局部变量里,重点以及关键就是 FactoryBean#getObject 方法。

@Override
public Object getObject() throws Exception {
    return getTarget();
}

4.2 FeignClientFactoryBean#getTarget

getTarget 源码方法还是挺长的,这里采用分段的形式展示:

<T> T getTarget() {
   // 从 IOC 容器获取 FeignContext
    FeignContext context = applicationContext.getBean(FeignContext.class);
   // 通过 context 创建 Feign 构造器
    Feign.Builder builder = feign(context);
  ...
}

这里提出一个疑问?FeignContext 什么时候、在哪里被注入到 Spring 容器里的?

用了 SpringBoot 怎么会不使用自动装配的功能呢,FeignContext 就是在 FeignAutoConfiguration 中被成功创建。

在FeignAutoConfiguration中,向Spring容器注入FeignContext :

并设置其配置为configurations ,而configurations 是通过@Autowired注入,即List<FeignClientSpecification>集合。

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Feign.class)
@EnableConfigurationProperties({ FeignClientProperties.class,
        FeignHttpClientProperties.class })
@Import(DefaultGzipDecoderConfiguration.class)
public class FeignAutoConfiguration {

    @Autowired(required = false)
    private List<FeignClientSpecification> configurations = new ArrayList<>();

    @Bean
    public HasFeatures feignFeature() {
        return HasFeatures.namedFeature("Feign", Feign.class);
    }

    @Bean
    public FeignContext feignContext() {
        FeignContext context = new FeignContext();
        context.setConfigurations(this.configurations);
        return context;
    }
       ...
}

4.3 FeignClientFactoryBean#feign

    protected Feign.Builder feign(FeignContext context) {
        FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
        Logger logger = loggerFactory.create(this.type);

        // @formatter:off
        Feign.Builder builder = get(context, Feign.Builder.class)
                // required values
                .logger(logger)
                .encoder(get(context, Encoder.class))
                .decoder(get(context, Decoder.class))
                .contract(get(context, Contract.class));
        // @formatter:on

        configureFeign(context, builder);

        return builder;
    }

feign 方法里日志工厂、编码、解码等类均是通过FeignClientFactoryBean#get(...) 方法得到。

    protected <T> T get(FeignContext context, Class<T> type) {
        T instance = context.getInstance(this.contextId, type);
        if (instance == null) {
            throw new IllegalStateException(
                    "No bean found of type " + type + " for " + this.contextId);
        }
        return instance;
    }

//FeignContext方法
public <T> T getInstance(String name, Class<T> type) { //根据name获取context实例 AnnotationConfigApplicationContext context = getContext(name); //根据type类型从子容器获取Bean实例 if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, type).length > 0) { return context.getBean(type); } return null; }

这里涉及到 Spring 父子容器的概念,默认子容器FeignContext#contexts为空,获取不到服务名对应 context 则使用FeignContext#createContext新建。

    private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>();
protected AnnotationConfigApplicationContext getContext(String name) {
        if (!this.contexts.containsKey(name)) {
            synchronized (this.contexts) {
                if (!this.contexts.containsKey(name)) {
                    this.contexts.put(name, createContext(name));
                }
            }
        }
        return this.contexts.get(name);
    }

子容器context创建完之后,如果@FeignClient中配置有configuration。会向子容器中注入一个 configuration属性指定的类型的 Bean。因此我们可以通过configuration对每个@FeignClient做定制化配置、比如Encoder、Decoder、FeignLoggerFactory等等。

//这里的name是@FeignContent中的contentId值
  protected AnnotationConfigApplicationContext createContext(String name) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();

         //@FeignClient没有配置configuration属性 不会执行  this.configurations 保存的是FeignClientConfiguration类型的列表,也就是之前我们介绍到的注入Spring容器中的FeignClient配置
        if (this.configurations.containsKey(name)) {
            for (Class<?> configuration : this.configurations.get(name)
                    .getConfiguration()) {
                context.register(configuration);
            }
        }
        for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
            if (entry.getKey().startsWith("default.")) {
                // @EnableFeignClient没有配置defaultConfiguration属性 不会执行
                for (Class<?> configuration : entry.getValue().getConfiguration()) {
                    context.register(configuration);
                }
            }
        }


         // 注入默认配置类FeignClientsConfiguration,会注入默认的feignEncoder、feignDecoder等
        context.register(PropertyPlaceholderAutoConfiguration.class,
                this.defaultConfigType);
        context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(
                this.propertySourceName,
                Collections.<String, Object>singletonMap(this.propertyName, name)));
        if (this.parent != null) {
            // Uses Environment from parent as well as beans
            //设置父容器、子容器不存在去父容器查找 
            context.setParent(this.parent);
            // jdk11 issue
            // https://github.com/spring-cloud/spring-cloud-netflix/issues/3101
            context.setClassLoader(this.parent.getClassLoader());
        }
        context.setDisplayName(generateDisplayName(name));
        context.refresh();
        return context;
    }

关于父子类容器对应关系,以及提供 @FeignClient 服务对应子容器的关系(每一个服务对应一个子容器实例)

需要注意的是上图中的Product1、Product2、Product3并不是说就有三个微服务。而是说有三个@FeignClien服务,三个服务可以对应一个微服务,比如下面这种:

Client 1
@FeignClient(name = "optimization-user",contextId="1")
public interface UserRemoteClient {
    @GetMapping("/user/get")
    public User getUser(@RequestParam("id") int id);
}

Client 2
@FeignClient(name = "optimization-user",,contextId="2")
public interface UserRemoteClient2 {
    @GetMapping("/user2/get")
    public User getUser(@RequestParam("id") int id);
}

Client 3
@FeignClient(name = "optimization-user",,contextId="3")
public interface UserRemoteClient2 {
    @GetMapping("/user2/get")
    public User getUser(@RequestParam("id") int id);
}

回到FeignContext#getInstance 方法,子容器此时已加载对应 Bean,直接通过 getBean 获取 FeignLoggerFactory。

如法炮制,Feign.Builder、Encoder、Decoder、Contract 都可以通过子容器获取对应 Bean。

protected Feign.Builder feign(FeignContext context) {
        FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
        Logger logger = loggerFactory.create(this.type);

        // @formatter:off
        Feign.Builder builder = get(context, Feign.Builder.class)
                // required values
                .logger(logger)
                .encoder(get(context, Encoder.class))
                .decoder(get(context, Decoder.class))
                .contract(get(context, Contract.class));
        // @formatter:on

        configureFeign(context, builder);

        return builder;
    }

configureFeign 方法主要进行一些配置赋值,比如超时、重试、404 配置等,就不再细说了。

到这里有必要总结一下创建 Spring 代理工厂的前半场代码 :

1. 注入@FeignClient 服务时,其实注入的是 FactoryBean#getObject 返回代理工厂对象。

2.通过 IOC 容器获取 FeignContext 上下文。

3.,创建 Feign.Builder 对象时会创建 Feign 服务对应的子容器。

4.从子容器中获取日志工厂、编码器、解码器等 Bean 为 Feign.Builder 设置配置,比如超时时间、日志级别等属性,每一个服务都可以个性化设置。

4.4 动态生成代理

接下来是最最最重要的地方了,继续FeignClientFactoryBean#getTarget

<T> T getTarget() {
        FeignContext context = this.applicationContext.getBean(FeignContext.class);
        Feign.Builder builder = feign(context);
        //判断@FeignClient url属性是否存在
        if (!StringUtils.hasText(this.url)) {
            if (!this.name.startsWith("http")) {
                this.url = "http://" + this.name;
            }
            else {
                this.url = this.name;
            }
            this.url += cleanPath();
           //type就是@FeignClient注解修饰的接口类型
          //name:@FeignClient name属性,ribbon通过这个到注册中心获取服务信息
            return (T) loadBalance(builder, context,
                    new HardCodedTarget<>(this.type, this.name, this.url));
        }
        //存在的话,就不使用负载均衡以及注册中心了
               ...
}

因为我们在 @FeignClient 注解是使用 name 而不是 url,所以会执行负载均衡策略的分支。FeignClientFactoryBean#loadBalance:

    protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
            HardCodedTarget<T> target) {
//从 Client client
= getOptional(context, Client.class); if (client != null) { builder.client(client); Targeter targeter = get(context, Targeter.class); return targeter.target(this, builder, context, target); } throw new IllegalStateException( "No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?"); }

Client: Feign 发送请求以及接收响应等都是由 Client 完成,该类默认 Client.Default,另外支持 HttpClient、OkHttp 等客户端,如:

Feign.builder().client(new OkHttpClient())

代码中的 Client、Targeter 在自动装配时注册,配合上文中的父子容器理论,这两个 Bean 在父容器中存在,所以子容器也可以获取到。FeignClientFactoryBean#getOptional,getOptional和get的区别在于一个是可选,一个是必须的,get中如果从子容器获取不到指定的bean实例,会抛出异常,而getOptional不会:

    protected <T> T getOptional(FeignContext context, Class<T> type) {
        return context.getInstance(this.contextId, type);
    }

因为我们并没有对 Hystix 进行设置,所以Targeter#target走入Feign#target分支:

    public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
            FeignContext context, Target.HardCodedTarget<T> target) {
        if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) {
            return feign.target(target);
        }
        feign.hystrix.HystrixFeign.Builder builder = (feign.hystrix.HystrixFeign.Builder) feign;
        String name = StringUtils.isEmpty(factory.getContextId()) ? factory.getName()
                : factory.getContextId();
        SetterFactory setterFactory = getOptional(name, context, SetterFactory.class);
        if (setterFactory != null) {
            builder.setterFactory(setterFactory);
        }
        Class<?> fallback = factory.getFallback();
        if (fallback != void.class) {
            return targetWithFallback(name, context, target, builder, fallback);
        }
        Class<?> fallbackFactory = factory.getFallbackFactory();
        if (fallbackFactory != void.class) {
            return targetWithFallbackFactory(name, context, target, builder,
                    fallbackFactory);
        }

        return feign.target(target);
    }

Feign#target中首先会创建反射类 ReflectiveFeign,其中ReflectiveFeign是Feign的实现类:

然后调用ReflectiveFeign#newInstance(target)执行创建实例类:

    public <T> T target(Target<T> target) {
      return build().newInstance(target);
    }
    public Feign build() {
      SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
          new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
              logLevel, decode404, closeAfterDecode, propagationPolicy);
      ParseHandlersByName handlersByName =
          new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,
              errorDecoder, synchronousMethodHandlerFactory);
      return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
    }

ReflectiveFeign#newInstance 方法对 @FeignClient 修饰的接口中 SpringMvc 等配置进行解析转换,对接口类中的方法进行归类,生成动态代理类

 public <T> T newInstance(Target<T> target) {
    //将装饰了@FeignClient的接口方法封装为方法处理器,包括Spring MVC注解逻辑处理
    Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
    //接口方法对应的MethodHandler
    Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
   //添加JDK8以后出现的接口中默认方法 
    List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
 
    //1.如果是object 方法跳过  2.default方法添加defaultMethodHandlers 3、否则添加methodToHandler
    for (Method method : target.type().getMethods()) {
      if (method.getDeclaringClass() == Object.class) {
        continue;
      } else 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)));
      }
    }
    //根据targert、methodToHandler创建InvocationHandler 
    InvocationHandler handler = factory.create(target, methodToHandler);
    //根据JDK Proxy创建动态代理类
    T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
        new Class<?>[] {target.type()}, handler);

    for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
      defaultMethodHandler.bindTo(proxy);
    }
    return proxy;
  }

可以看出 Feign 创建动态代理类的方式和 Mybatis Mapper 处理方式是一致的,因为两者都没有实现类 。

根据 ReflectiveFeign#newInstance 方法按照行为大致划分,共做了四件事 处理:

1. 将@FeignClient 接口方法封装为 MethodHandler 包装类,每一个方法对应一个MethodHandler,MethodHandler的实现类是SynchronousMethodHandler

    public MethodHandler create(Target<?> target,
                                MethodMetadata md,
                                RequestTemplate.Factory buildTemplateFromArgs,
                                Options options,
                                Decoder decoder,
                                ErrorDecoder errorDecoder) {
      return new SynchronousMethodHandler(target, client, retryer, requestInterceptors, logger,
          logLevel, md, buildTemplateFromArgs, options, decoder,
          errorDecoder, decode404, closeAfterDecode, propagationPolicy, forceDecoding);
    }

可以看到每个MethodHandler都包含了客户端、日志、请求模板、编码解码器等参数,通过这些参数就可以构建相应接口的Http请求。

2. 遍历接口中所有方法,过滤 Object 方法,并将默认方法以及 FeignClient 方法分类。

3. 创建动态代理对应的 InvocationHandler ,默认InvocationHandler 的实现类为ReflectiveFeign.FeignInvocationHandler,然后利用Proxy.newProxyInstance创建 Proxy 实例。

4. 接口内 default 方法绑定动态代理类。

其中FeignInvocationHandler实现如下:

  static class FeignInvocationHandler implements InvocationHandler {

    private final Target target;
    private final Map<Method, MethodHandler> dispatch;

    FeignInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) {
      this.target = checkNotNull(target, "target");
      this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);
    }

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

dispath 是缓存的method 以及 method对应的MethodHandler

我们调用的远程接口用的是SynchronousMethodHandler实现,该类将方法参数、方法返回值、参数集合、请求类型、请求路径进行解析存储。

到这里我们已经很清楚Feign 的工作方式了。前面那么多封装铺垫,封装个性化配置等等,最终确定收尾的是创建动态代理类。

也就是说在我们调用 feign接口时,会被 ReflectiveFeign.FeignInvocationHandler#invoke 拦截,最后会被SynchronousMethodHandler#invoke方法处理。

4.5 调试运行

既然已经明白了调用流程,那就正儿八经的试一哈,就会发现我们通过FeignClient服务发送的请求,最终会被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:构建 Request 模版类。Options:存放连接、超时时间等配置类。Retryer:失败重试策略类。

跟踪代码发现,首先根据请求模板RequestTemplate构建Request实例,然后调用的SynchronousMethodHandler持有的Client的实例的execute方法。

Client默认是通过FeignRibbonClientAutoConfiguration进行注入的:

@ConditionalOnClass({ ILoadBalancer.class, Feign.class })
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.ribbon.enabled",
        matchIfMissing = true)
@Configuration(proxyBeanMethods = false)
@AutoConfigureBefore(FeignAutoConfiguration.class)
@EnableConfigurationProperties({ FeignHttpClientProperties.class })
// Order is important here, last should be the default, first should be optional
// see
// https://github.com/spring-cloud/spring-cloud-netflix/issues/2086#issuecomment-316281653
@Import({ HttpClientFeignLoadBalancedConfiguration.class,
        OkHttpFeignLoadBalancedConfiguration.class,
        DefaultFeignLoadBalancedConfiguration.class })
public class FeignRibbonClientAutoConfiguration {
    ...
}

如果前面的两个配置类的条件没有满足,feign.Client 的 IOC 容器实例没有装配,则:

1. 创建一个 Client.Default 默认客户端实例,该实例的内部,使用HttpURLConnnection 完成URL请求处理;

2. 创建一个 LoadBalancerFeignClient 负载均衡客户端实例,将 Client.Default 实例包装起来,然后返回LoadBalancerFeignClient 客户端实例,作为 feign.Client 类型的Spring IOC 容器实例。

@Configuration
class DefaultFeignLoadBalancedConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
            SpringClientFactory clientFactory) {
        return new LoadBalancerFeignClient(new Client.Default(null, null), cachingFactory,
                clientFactory);
    }

}

LoadBalancerFeignClient 也是一个feign.Client 客户端实现类。内部先使用 Ribbon 负载均衡算法计算server服务器,然后使用包装的 delegate 客户端实例,去完成 HTTP URL请求处理。

当然我们还可以上面两种,具体配置可以参考Feign、httpclient、OkHttp3 结合使用

  • ApacheHttpClient 类:内部使用 Apache httpclient 开源组件完成HTTP URL请求处理的feign.Client 客户端实现类;
  • OkHttpClient类:内部使用 OkHttp3 开源组件完成HTTP URL请求处理的feign.Client 客户端实现类。

我们调用feign接口的某一个方法,最终调用的LoadBalancerFeignClient.execute()方法。

4.6 LoadBalancerFeignClient#execute

public Response execute(Request request, Request.Options options) throws IOException {
        try {
             // URL 处理  
            URI asUri = URI.create(request.url());
            String clientName = asUri.getHost();
            URI uriWithoutHost = cleanUrl(request.url(), clientName);
            FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
                    this.delegate, request, uriWithoutHost);
            // 获取调用服务配置
            IClientConfig requestConfig = getClientConfig(options, clientName);

            // 创建负载均衡客户端,执行请求
            return lbClient(clientName)
                    .executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
        }
        catch (ClientException e) {
            IOException io = findIOException(e);
            if (io != null) {
                throw io;
            }
            throw new RuntimeException(e);
        }
    }

从上面的代码可以看到,lbClient(clientName) 创建了一个负载均衡的客户端,它实际上就是生成的如下所述的类:

public class FeignLoadBalancer extends
        AbstractLoadBalancerAwareClient<FeignLoadBalancer.RibbonRequest, FeignLoadBalancer.RibbonResponse> 

熟悉ribbon的朋友应该知道AbstractLoadBalancerAwareClient 就是Ribbon负载均衡调用的父类。具体的负载均衡实现策略,可以移步微服务通信之ribbon实现原理。至此我们可以得出结论:feign集成负载均衡是通过将FeignLoadBalancer作为调用feign接口的实际执行者,从而达到负载均衡的效果。可以看到这里与Ribbon高度的解耦,相当于我们获取了服务名、调用地址、调用参数后,最终交由一个执行器去调用。执行器并不关心参数从何而来,这里基于Ribbon提供的执行器实现只是根据传递的服务名找到了一个正确的实例去调用而已。

五、总结

到这里Feign的介绍就结束了,我们使用一张Feign远程调用的基本流程总结一下 Feign 调用链(图片来自Feign原理 (图解)):

参考文章:

【1】花一个周末,掌握 SpringCloud OpenFeign 核心原理(大部分内容转载该文)

【2】Feign配置使用

【3】Spring Cloud Feign 如何自定义编码器、解码器和客户端

【4】微服务通信之feign集成负载均衡

【5】Feign原理 (图解)

 

posted @ 2021-04-17 16:43  大奥特曼打小怪兽  阅读(1793)  评论(1编辑  收藏  举报
如果有任何技术小问题,欢迎大家交流沟通,共同进步