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>

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

一个是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());

    }

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

                }

            }

        }

    }

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集合。

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

    }


1、可以看到每个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原理 (图解)):

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容