掘金 后端 ( ) • 2024-05-06 09:08

highlight: a11y-dark

傻笑表情


前言

本文将从Spring父子容器开始,一步一步推进到Openfeign的底层原理,让我们在学习Openfeign的底层原理时,不至于被一个又一个陌生的概念所困扰。

本文也将尽量减少怼着源码无效输出,更有效的方式是找出 平时我们接触Feign客户端最多的一些东西是怎么体现在源码中的

Springcloud版本是3.1.1,思维导图如下。

Springcloud-Openfeign原理简析脑图

注意,Feign是Springcloud的一个Http客户端组件,而Openfeign是在Feign的基础上增加支持了SpringMVC的注解,现在都是使用的Openfeign,所以提到Feign其实就是指Openfeign。

正文

一. Spring父子容器

Openfeign的底层实现中,处处都有父子容器的使用,但是处处又都不明显,所以这里先回味一下Spring父子容器,我盲猜应该还是有人没太关注过这个概念的。

1. 概念速览

Spring的容器,是有父子概念的,子容器可以获取父容器的bean,但是反过来父容器不能获取子容器中的bean

2. 使用场景

视野先狭窄一点,考虑同一个工程不同的包目录下有两个名字一样的类,那么默认情况下,这两个类不能同时都注册beanSpring容器中的,Spring会报如下的错来提示我们。

Caused by: org.springframework.context.annotation.ConflictingBeanDefinitionException: Annotation-specified bean name 'myBean' for bean class [xxx2.MyBean] 
        conflicts with existing, non-compatible bean definition of same name and class [xxx1.MyBean]
    at org.springframework.context.annotation.ClassPathBeanDefinitionScanner.checkCandidate(ClassPathBeanDefinitionScanner.java:349)
    at org.springframework.context.annotation.ClassPathBeanDefinitionScanner.doScan(ClassPathBeanDefinitionScanner.java:287)
    at org.springframework.context.annotation.ComponentScanAnnotationParser.parse(ComponentScanAnnotationParser.java:128)
    at org.springframework.context.annotation.ConfigurationClassParser.doProcessConfigurationClass(ConfigurationClassParser.java:295)
    at org.springframework.context.annotation.ConfigurationClassParser.processConfigurationClass(ConfigurationClassParser.java:249)
    at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:206)
    at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:174)
    ... 8 more

这种情况是因为同时想把xxx1.MyBeanxxx2.MyBean都注册相同名字的bean到同一个Spring容器中,如果分别注册到不同的容器中,这是可以的,但是问题也来了,不同的容器的话,通过其中任何一个容器,都只能获取当前容器里面的bean,这也是不太方便的,但如果是父子容器,问题就迎刃而解了,其中一个bean注册到父容器,另外一个bean注册到子容器,此时通过子容器,xxx1.MyBeanxxx2.MyBeanbean就都能访问到了。

现在视野打开一点,假如有一个第三方包,这个第三方包如果要发挥自己的作用的话,需要往Spring容器注册一些bean,第三方包在工作时,会依赖这些bean,那么问题就出现了,这些第三方包里面的bean直接往原Spring容器注册的话,保不准就和业务代码里面的bean里面的名字一样,此时*~轻则功能异常,重则程序启动失败~* ,所以这种时候,第三方包如果能把自己的bean注册到自己的容器中,然后让程序原有的容器成为自己的父,那么第三方包就可以通过子容器安全的操作自己的bean以及父容器中的bean

3. 示例演示

来简单搭建一个示例demo,演示一下父子容器的使用。工程目录结构如下。

父子容器示例工程目录结构图

pom文件如下所示。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-parent</artifactId>
        <version>2.7.6</version>
    </parent>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <groupId>com.lee.learn.container</groupId>
    <artifactId>learn-parent-child-container</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
    </dependencies>

</project>

com.lee.learn.container.service1目录下的MyBeanService1Config如下所示。

public class MyBean {

    public MyBean() {
        System.out.println("Service1 MyBean Created.");
    }

}
@Configuration
public class Service1Config {

    @Bean
    public MyBean myBean() {
        return new MyBean();
    }

}

com.lee.learn.container.service2目录下的MyBeanService2Config如下所示。

public class MyBean {

    public MyBean() {
        System.out.println("Service2 MyBean Created.");
    }

}
@Configuration
public class Service2Config {

    @Bean
    public MyBean myBean() {
        return new MyBean();
    }

}

最后是MainTest,如下所示。

public class MainTest {

    public static void main(String[] args) {
        // 基于Service1Config构建父容器
        AnnotationConfigApplicationContext parentApplicationContext
                    = new AnnotationConfigApplicationContext(Service1Config.class);
        // 基于Service2Config构建子容器
        AnnotationConfigApplicationContext childApplicationContext
                    = new AnnotationConfigApplicationContext(Service2Config.class);
        // 建立起父子容器关系
        childApplicationContext.setParent(parentApplicationContext);

        // 从父容器可以获取到com.lee.learn.container.service1.MyBean
        parentApplicationContext.getBean(com.lee.learn.container.service1.MyBean.class);
        // 从子容器可以获取到com.lee.learn.container.service2.MyBean
        childApplicationContext.getBean(com.lee.learn.container.service2.MyBean.class);
        // 也可以从子容器获取到父容器中的com.lee.learn.container.service1.MyBean
        childApplicationContext.getBean(com.lee.learn.container.service1.MyBean.class);

        // 但父容器获取不到子容器中的com.lee.learn.container.service2.MyBean
        // 执行到这里会报错NoSuchBeanDefinitionException
        parentApplicationContext.getBean(com.lee.learn.container.service2.MyBean.class);
    }

}

运行到最后一行时会报如下错误。

Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: 
        No qualifying bean of type 'com.lee.learn.container.service2.MyBean' available
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:351)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:342)
    at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1172)
    at com.lee.learn.container.MainTest.main(MainTest.java:28)

也就是父容器获取不到子容器中的bean

二. NamedContextFactory

NamedContextFactory叫做 命名的容器上下文工厂,什么意思呢,也就是通过给每个容器上下文关联一个名字,通过这个名字就能获取到对应的容器上下文,容器上下文间基于name实现了隔离。

1. 概念简析

NamedContextFactory中有一个字段叫做contexts,字段签名如下所示。

// Map[容器上下文名字,容器上下文]
private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>();

也就是每个容器上下文和自己的名字以键值对的形式保存在contexts中。同时NamedContextFactory还有一个字段叫做configurations,字段签名如下所示。

// Map[容器上下文名字,容器上文的配置]
private Map<String, Specification> configurations = new ConcurrentHashMap<>();

每个容器上下文创建出来后,都需要使用配置类去初始化,所以容器上下文配置类和容器上下文的名字以键值对的形式保存在configurations中,但是这里说的配置类,并不是我们认为的Springboot里面的那种由@Configuration注解修饰的配置类,而是如下这么一个接口。

public interface Specification {

    String getName();

    Class<?>[] getConfiguration();

}

Specification中会保存若干个真正的配置类的Class对象,这里所谓的真正的配置类,就等价于我们常用的由@Configuration注解修饰的配置类。

所以通常情况下,NamedContextFactory创建出来的一个容器上下文,都应该有一个独属于自己的Specification,在初始化容器上下文时,就会拿Specification中的若干个真正的配置类来初始化这个容器上下文。

此外,NamedContextFactory创建出来一个容器上下文后,除了会拿其独有的Specification来初始化容器上下文,还会拿如下两部分内容来初始化容器上下文。

  1. NamedContextFactoryconfigurations中名字以default.* 开头的Specification*。我们可以向configurations中加入若干个名字以default. 开头的Specification,那么每创建一个容器上下文时,都会用这些名字以default. 开头的Specification来初始化容器上下文;
  2. NamedContextFactorydefaultConfigType字段*。defaultConfigType字段就是一个真正的配置类的Class对象,在创建NamedContextFactory时就需要指定defaultConfigType*,后续每创建一个容器上下文,都需要使用defaultConfigType来初始化。

那么NamedContextFactory创建出来的容器上下文,会被三部分内容来初始化,其一是独属于自己的Specification,其二是名字以default. 开头的Specification,其三就是NamedContextFactorydefaultConfigType字段代表的真正的配置类,那么啥叫初始化呢,其实就是给容器上下文一个真正的配置类,然后容器上下文通过这个真正的配置类来加载bean

NamedContextFactory的一个简单概念图示如下所示。

Springcloud-NamedContextFactory示意图.jpg

最后补充一点,NamedContextFactory中的所有容器上下文,都有一个共同的父容器,就是Spring应用中的主容器。

2. 源码简析

其实如果理解了上一小节的内容,那么是不需要看NamedContextFactory的源码的,因为NamedContextFactory的核心方法getContext(),本质就是在完成上一小节所阐述的事情,但是看了源码后,才能更有底气基于NamedContextFactory做扩展,所以本小节就针对NamedContextFactorygetContext() 方法简单分析一下。

NamedContextFactorygetContext() 方法实现如下。

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

有句话怎么说的来着,NamedContextFactory本没有容器上下文,但是调用getContext() 方法次数多了,容器上下文就有了。NamedContextFactory一开始创建出来时,其contexts就是空的,每次调用getContext() 方法获取容器上下文时,如果contexts中没有对应name的容器上下文,那么就会去调用createContext() 方法创建一个出来并放到contexts中,如果有对应name的容器上下文,那么就直接把contexts中的这个容器上下文返回。所以容器上下文创建的关键,需要继续跟进createContext() 方法,如下所示。

protected AnnotationConfigApplicationContext createContext(String name) {
    // 创建一个空的容器上下文出来
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    if (this.configurations.containsKey(name)) {
        // 使用name对应的Specification来初始化容器上下文
        for (Class<?> configuration : this.configurations.get(name).getConfiguration()) {
            context.register(configuration);
        }
    }
    // 使用所有名字以default.开头的Specification来初始化容器上下文
    for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
        if (entry.getKey().startsWith("default.")) {
            for (Class<?> configuration : entry.getValue().getConfiguration()) {
                context.register(configuration);
            }
        }
    }
    // 使用defaultConfigType来初始化容器上下文
    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) {
        // 将主容器设置为父容器
        context.setParent(this.parent);
        context.setClassLoader(this.parent.getClassLoader());
    }
    context.setDisplayName(generateDisplayName(name));
    // 刷新容器上下文
    context.refresh();
    return context;
}

上述创建容器上下文的源码流程,和第1小节的概念完全符合,但这里有一点要补充一下,就是NamedContextFactory实现了ApplicationContextAware接口,并且在实现的setApplicationContext() 方法中将主容器赋值给了NamedContextFactoryparent字段,而在createContext() 方法中,每创建一个容器上下文时,都会将其父设置为NamedContextFactoryparent字段,所以NamedContextFactory中的所有容器上下文,都是主容器的子容器,换言之,每个NamedContextFactory中的容器上下文,都能够访问主容器中的bean

3. 示例演示

本小节以一个示例demo,来演示如何使用NamedContextFactory。示例工程目录结构如下所示。

NamedContextFactory示例工程目录结构图

pom文件如下所示。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-parent</artifactId>
        <version>2.7.6</version>
    </parent>

    <groupId>com.named.contextfactory</groupId>
    <artifactId>learn-named-context-factory</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter</artifactId>
            <version>3.1.1</version>
        </dependency>
    </dependencies>

</project>

因为NamedContextFactorySpringcloud组件,所以需要引入Springcloud基础包。

Application是一个极简的配置类,主要用于初始化父容器,如下所示。

@ComponentScan
@Configuration
public class Application {
}

com.named.contextfactory.service包目录下有四个几乎没有任何逻辑的业务类,如下所示。

public class DefaultBean {

    public DefaultBean() {
        System.out.println("默认Bean加载");
    }

}
public class DefaultService {

    public DefaultService() {
        System.out.println("默认服务加载");
    }

}
public class LoginService {

    public void login() {
        System.out.println("登陆成功");
    }

}
public class LogoutService {

    public void logout() {
        System.out.println("登出成功");
    }

}

com.named.contextfactory.configuration目录下有四个真正的配置类,如下所示。

@Configuration
public class DefaultBeanConfig {

    @Bean
    public DefaultBean defaultBean() {
        return new DefaultBean();
    }

}
@Configuration
public class DefaultConfig {

    @Bean
    public DefaultService defaultService() {
        return new DefaultService();
    }

}
@Configuration
public class LoginConfig {

    @Bean
    public LoginService loginService() {
        return new LoginService();
    }

}
@Configuration
public class LogoutConfig {

    @Bean
    public LogoutService logoutService() {
        return new LogoutService();
    }

}

自定义的Specification接口的实现类如下所示。

public class MySpecification implements NamedContextFactory.Specification {

    private final String name;

    private final Class<?>[] configurations;

    public MySpecification(String name, Class<?>[] configurations) {
        this.name = name;
        this.configurations = configurations;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public Class<?>[] getConfiguration() {
        return configurations;
    }

}

MyNamedContext继承于NamedContextFactory,其只干一件事情,就是在构造函数中指定defaultConfigTypeDefaultConfig.class,如下所示。

public class MyNamedContext extends NamedContextFactory<MySpecification> {

    private static final String PSN = "PSN";
    private static final String PN = "PN";

    public MyNamedContext() {
        super(DefaultConfig.class, PSN, PN);
    }

}

最后就是测试程序MainTest,如下所示。

public class MainTest {

    private static final String LOGIN = "login";
    private static final String LOGOUT = "logout";
    private static final String DEFAULT = "default.1";

    public static void main(String[] args) {
        // 创建父容器
        AnnotationConfigApplicationContext applicationContext
                = new AnnotationConfigApplicationContext(Application.class);

        // 创建出来所有的Specification
        List<MySpecification> mySpecifications = new ArrayList<>(Arrays.asList(
                new MySpecification(LOGIN, new Class<?>[]{LoginConfig.class}),
                new MySpecification(LOGOUT, new Class<?>[]{LogoutConfig.class}),
                new MySpecification(DEFAULT, new Class<?>[]{DefaultBeanConfig.class})
        ));

        // 创建自定义的NamedContextFactory
        MyNamedContext myNamedContext = new MyNamedContext();
        // 为自定义的NamedContextFactory添加配置
        myNamedContext.setConfigurations(mySpecifications);
        // 为自定义的NamedContextFactory设置父容器
        myNamedContext.setApplicationContext(applicationContext);

        // 从name为login的子容器中获取LoginService的bean
        LoginService loginService = myNamedContext.getInstance(LOGIN, LoginService.class);
        loginService.login();

        // 从name为logout的子容器中获取LogoutService的bean
        LogoutService logoutService = myNamedContext.getInstance(LOGOUT, LogoutService.class);
        logoutService.logout();
    }

}

运行测试程序,关键打印信息如下所示。

......
默认Bean加载
......
默认服务加载
登陆成功
......
默认Bean加载
......
默认服务加载
登出成功

在第一次获取名字为LOGIN的容器上下文时,触发了LOGIN容器上下文创建,此时DefaultConfigDefaultBeanConfig默认会用于初始化LOGIN容器上下文,同时独属于LOGIN容器上下文的LoginConfig也会用于初始化LOGIN容器上下文。

同理,在第一次获取名字为LOGOUT的容器上下文时,触发了LOGOUT容器上下文创建,同样的DefaultConfigDefaultBeanConfig默认会用于初始化LOGOUT容器上下文,同时独属于LOGOUT容器上下文的LogoutConfig也会用于初始化LOGOUT容器上下文。

4. Openfeign里面的NamedContextFactory

Openfeign里面,每一个feignClient都有一个独属于自己的容器上下文,同时每个feignClient除了依赖自己容器上下文里面的bean外,还会使用到主容器里面的bean,所以Openfeign底层原理里面,大量使用到了NamedContextFactory,现在我们来对号入座一下。

NamedContextFactory原生概念 Openfeign对应的实现 NamedContextFactory FeignContext NamedContextFactorydefaultConfigType字段 FeignClientsConfiguration.class Specification接口 FeignClientSpecification

首先FeignContext中持有每一个feignClient的容器上下文。

其次FeignClientsConfiguration里面会注册一些例如DecoderEncoderbean,这些是每个feignClient都会使用到的,故每个feignClient的容器上下文都会使用FeignClientsConfiguration来初始化,所以FeignContextdefaultConfigType字段被设置为了FeignClientsConfigurationClass对象。

除此之外,开启Openfeign功能的@EnableFeignClients注解的defaultConfiguration属性可以指定一组真实的配置类的Class对象,后续会基于这组真实的配置类的Class对象创建出来一个名字以default. 开头的FeignClientSpecification出来,所以通过@EnableFeignClients注解指定的配置是应用于所有feignClient的。

最后,用于标记feignClient的@FeignClient注解的configuration属性也可以指定一组真实的配置类的Class对象,后续会基于这组真实的配置类的Class对象创建出来一个名字和feignClient名字相同的FeignClientSpecification出来,所以通过@FeignClient注解指定的配置仅应用于对应的feignClient

三. Openfeign

1. 示例演示

我们需要搭建一个示例工程,方便后续演示和代码调试。工程目录结构如下所示。

Openfeign示例工程目录结构图

首先pom文件如下所示。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-parent</artifactId>
        <version>2.7.6</version>
    </parent>

    <groupId>com.lee.learn.openfeign</groupId>
    <artifactId>learn-openfeign</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>3.1.1</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

</project>

两个feignClient接口如下所示。

@FeignClient(name = "login", url = "127.0.0.1:8080")
public interface LoginClient {

    @GetMapping("/api/v1/login")
    String login();

    @GetMapping("/api/v1/fakeLogin")
    String fakeLogin();

}
@FeignClient(name = "logout", url = "127.0.0.1:8080")
public interface LogoutClient {

    @GetMapping("/api/v1/logout")
    String logout();

    @GetMapping("/api/v1/fakeLogout")
    String fakeLogout();

}

自定义的Capability实现如下。

public class LoginClientCapability implements Capability {

    @Override
    public Request.Options enrich(Request.Options options) {
        return new Request.Options(20, TimeUnit.SECONDS, 25, TimeUnit.SECONDS, false);
    }

}

两个测试Controller实现如下。

@Slf4j
@RestController
public class ReceiveController {

    @GetMapping("/api/v1/login")
    public String login() {
        String message = "登陆成功";
        log.info(message);
        return message;
    }

    @GetMapping("/api/v1/logout")
    public String logout() {
        String message = "登出成功";
        log.info(message);
        return message;
    }

}
@Slf4j
@RestController
public class SendController {

    @Autowired
    private LoginClient loginClient;

    @Autowired
    private LogoutClient logoutClient;

    @GetMapping("/api/v1/send/login")
    public String sendLogin() {
        return loginClient.login();
    }

    @GetMapping("/api/v1/send/logout")
    public String sendLogout() {
        return logoutClient.logout();
    }

}

启动类如下所示。

@EnableFeignClients
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

配置文件内容如下。

feign:
  client:
    config:
      login:
        connectTimeout: 5000
        readTimeout: 120000
        capabilities:
          - com.lee.learn.openfeign.config.LoginClientCapability
      logout:
        connectTimeout: 5500
        readTimeout: 120500

2. feignClient的BeanDefinition注册源码

要启用Openfeign,需要在Springboot启动类上添加@EnableFeignClients注解,所以feignClientBeanDefinition注册源码的分析,就从@EnableFeignClients注解开始。

@EnableFeignClients注解如下所示。

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

    // 等同于basePackages
    String[] value() default {};

    // 扫描feignClient的包路径
    String[] basePackages() default {};

    // 指定若干个类
    // 每个类所在包路径都会被扫描
    Class<?>[] basePackageClasses() default {};

    // 指定生效于所有feignClient的配置类
    Class<?>[] defaultConfiguration() default {};

    // 直接指定feignClient
    Class<?>[] clients() default {};

}

下面简单分析一下@EnableFeignClients注解的属性。

首先是valuebasePackagesbasePackageClasses属性,这三个属性都用于指定若干个扫描的包路径,路径下的所有的由@FeignClient注解修饰的feignClient都会被加载,这三个属性可以同时使用来共同指定扫描的包路径。

然后是defaultConfiguration属性,这个用于指定一组配置类,这一组配置类会被创建为一个名字以default. 开头的FeignClientSpecification,后续每一个feignClient自己的容器都会使用这个以default. 开头的FeignClientSpecification来初始化。

最后是clients属性,通过该属性可以直接指定若干个feignClient,但是被指定的类一定要由@FeignClient注解修饰,特别注意feignClient属性和value,basePackages及basePackageClasses属性是互斥的,且feignClient属性优先级更高

@EnableFeignClients注解的核心其实是其通过@Import注解引入的FeignClientsRegistrarFeignClientsRegistrar继承于ImportBeanDefinitionRegistrar,所以FeignClientsRegistrar主要用于向Spring容器注册bean,其registerBeanDefinitions() 方法如下所示。

@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    // 注册默认的FeignClientSpecification到容器中
    registerDefaultConfiguration(metadata, registry);
    // 扫描并注册所有feignClient到容器中
    registerFeignClients(metadata, registry);
}

registerDefaultConfiguration() 方法就是读取@EnableFeignClients注解的defaultConfiguration属性,然后构造默认的FeignClientSpecification并将名字以default. 开头注册到Spring容器中。

registerFeignClients() 方法就是扫描所有由@FeignClient注解修饰的类,然后加载为feignClientregisterFeignClients() 方法实现如下。

public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();
    Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
    // 拿到通过@EnableFeignClients注解的clients属性指定的所有feignClient的类对象
    final Class<?>[] clients = attrs == null ? null : (Class<?>[]) attrs.get("clients");
    if (clients == null || clients.length == 0) {
        // 如果没有通过@EnableFeignClients注解的clients属性指定feignClient类对象
        // 则去指定的包路径下扫描得到所有feignClient的类对象
        ClassPathScanningCandidateComponentProvider scanner = getScanner();
        scanner.setResourceLoader(this.resourceLoader);
        scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
        // 这里的包路径是如下路径的叠加
        // 1. @EnableFeignClients注解的value指定的路径
        // 2. @EnableFeignClients注解的basePackages指定的路径
        // 3. @EnableFeignClients注解的basePackageClasses指定的路径
        // 4. @EnableFeignClients修饰的类所在的路径
        Set<String> basePackages = getBasePackages(metadata);
        for (String basePackage : basePackages) {
            candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
        }
    } else {
        for (Class<?> clazz : clients) {
            candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
        }
    }
    // 到这里的话candidateComponents中已经存放着所有feignClient的类对象了
    for (BeanDefinition candidateComponent : candidateComponents) {
        if (candidateComponent instanceof AnnotatedBeanDefinition) {
            AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
            AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
            // @FeignClient不能修饰非接口
            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);
            // @FeignClient的configuration属性可以指定当前feignClient的配置
            // 这里就是基于configuration属性构造出FeignClientSpecification并注册到Spring容器中
            // 这里的FeignClientSpecification在容器中的名字以feignClient的contextId.开头
            registerClientConfiguration(registry, name, attributes.get("configuration"));
            // 注册feignClient到容器中
            registerFeignClient(registry, annotationMetadata, attributes);
        }
    }
}

虽然上面方法代码看着一大堆,但是其实就干三件事情。

  1. 拿到所有的feignClient的类对象。优先使用@EnableFeignClients注解的clients属性,其次使用@EnableFeignClients注解的valuebasePackagesbasePackageClasses属性;
  2. 为每个feignClient注册专属的FeignClientSpecification到容器中。@FeignClient注解修饰的接口就是feignClient,@FeignClient注解的configuration属性构造出来的FeignClientSpecification就是专属于这个feignClient的配置,这个配置会用于后续对应feignClient的容器的初始化;
  3. 注册feignClient到容器中

上述第3点,就是生成feignClientbean对应的BeanDefinition并完成注册,下面跟进一下registerFeignClient() 方法的实现。

private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata,
                                 Map<String, Object> attributes) {
    String className = annotationMetadata.getClassName();
    Class clazz = ClassUtils.resolveClassName(className, null);
    ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory
            ? (ConfigurableBeanFactory) registry : null;
    // contextId用于区分不同的feignClient的容器上下文和配置
    String contextId = getContextId(beanFactory, attributes);
    String name = getName(attributes);
    // FeignClientFactoryBean用于生产feignClient的bean
    FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();
    factoryBean.setBeanFactory(beanFactory);
    factoryBean.setName(name);
    factoryBean.setContextId(contextId);
    factoryBean.setType(clazz);
    factoryBean.setRefreshableClient(isClientRefreshEnabled());
    // 这里BeanDefinitionBuilder的genericBeanDefinition方法的第二个参数是Supplier
    // 在创建bean的时候会调用到BeanDefinition持有的Supplier的get()方法
    BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> {
        // 在创建feignClient的bean的时候就会调用到这里
        // 这里执行完毕返回的对象就是feignClient的bean

        // 这里的getUrl()和getPath()方法就是在做占位符替换
        factoryBean.setUrl(getUrl(beanFactory, attributes));
        factoryBean.setPath(getPath(beanFactory, attributes));
        factoryBean.setDecode404(Boolean.parseBoolean(String.valueOf(attributes.get("decode404"))));
        Object fallback = attributes.get("fallback");
        if (fallback != null) {
            factoryBean.setFallback(fallback instanceof Class ? (Class<?>) fallback
                    : ClassUtils.resolveClassName(fallback.toString(), null));
        }
        Object fallbackFactory = attributes.get("fallbackFactory");
        if (fallbackFactory != null) {
            factoryBean.setFallbackFactory(fallbackFactory instanceof Class ? (Class<?>) fallbackFactory
                    : ClassUtils.resolveClassName(fallbackFactory.toString(), null));
        }
        // 最终调用到FeignClientFactoryBean得到feignClient的bean
        return factoryBean.getObject();
    });
    definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
    definition.setLazyInit(true);
    validate(attributes);

    AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
    beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
    beanDefinition.setAttribute("feignClientsRegistrarFactoryBean", factoryBean);

    boolean primary = (Boolean) attributes.get("primary");

    beanDefinition.setPrimary(primary);

    String[] qualifiers = getQualifiers(attributes);
    if (ObjectUtils.isEmpty(qualifiers)) {
        qualifiers = new String[] { contextId + "FeignClient" };
    }

    BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers);
    // 注册feignClient的BeanDefinition
    BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);

    registerOptionsBeanDefinition(registry, contextId);
}

上面方法看着更是一大堆,下面挑重点概括一下。

  1. 得到contextId。我们可以直接通过@FeignClient注解的contextId属性来指定这个值,如果不指定,则会依次取namevalue属性的值。contextId的作用是用于区分不同的feignClient的容器上下文和配置;
  2. 创建FeignClientFactoryBeanFeignClientFactoryBean继承于FactoryBean,用于创建feignClientbean
  3. feignClient创建持有SupplierBeanDefinition。每一个feignClientBeanDefinition都持有一个Supplier,在创建feignClientbean的时候就会调用到Supplierget() 方法,上述的Supplierget() 方法最终会调用到FeignClientFactoryBeangetObject() 方法,所以feignClientbean其实就还是FeignClientFactoryBean来创建出来的;
  4. 注册每个feignClientBeanDefinition

至此注册feignClientBeanDefinition的逻辑就分析完毕。

3. FeignContext初始化源码

在继续分析FeignClientFactoryBeangetObject() 方法前,我们需要回顾下Openfeign里面的NamedContextFactory,也就是FeignContext

通过第二节第4小节可以知道,FeignContext用于创建并保存每一个feignClient的容器上下文,在Openfeign的核心包里面通过自动装配类FeignAutoConfiguration完成了FeignContext的初始化,如下所示。

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

    ......

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

    ......

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

}

创建出来的FeignContext也是放到了Spring主容器中,并且创建FeignContext时使用到的配置类FeignClientSpecification也是从Spring主容器中获取的。

4. feignClient的bean实例化源码

现在继续分析FeignClientFactoryBeangetObject() 方法,这里就是在真正的实例化feignClient,我们提供的大部分配置也是在这个阶段应用到feignClient的。

在正式开始前得先看看FeignClientFactoryBeanget()getOptional() 方法,长下面这样。

protected <T> T get(FeignContext context, Class<T> type) {
    // 1. 根据contextId获取到当前feignClient的容器上下文
    // 2. 从feignClient的容器上下文里获取type类型的bean
    T instance = context.getInstance(contextId, type);
    if (instance == null) {
        throw new IllegalStateException("No bean found of type " + type + " for " + contextId);
    }
    return instance;
}
// 本质还是get()方法
protected <T> T getOptional(FeignContext context, Class<T> type) {
    return context.getInstance(contextId, type);
}

所以只要看到有调用get()getOptional() 方法的地方,那么就都是在从当前feignClient的容器上下文获取某个bean,这个bean是当前feignClient正常提供功能需要使用的组件。

现在正式开始分析FeignClientFactoryBeangetObject() 方法,源码如下所示。

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

继续跟进getTarget() 方法的实现。

<T> T getTarget() {
    // 从主容器中把FeignContext的bean获取出来
    FeignContext context = beanFactory != null ? beanFactory.getBean(FeignContext.class)
            : applicationContext.getBean(FeignContext.class);
    // 1. 从子容器中获取到Feign.Builder
    // 2. 从子容器中获取日志打印器并设置给Feign.Builder
    // 3. 从子容器中获取Encoder并设置给Feign.Builder
    // 4. 从子容器中获取Decoder并设置给Feign.Builder
    // 5. 从子容器中获取Contract并设置给Feign.Builder
    // 6. 从主容器中获取FeignClientProperties并完成对Feign.Builder的配置
    // 上述第5点中的Contract实际是SpringMvcContract
    // 用于提供Openfeign支持SpringMvc注解的功能
    Feign.Builder builder = feign(context);

    if (!StringUtils.hasText(url)) {

        // 没有配置url则默认feignClient会使用负载均衡器
        // 忽略没有配置url的这部分代码以降低源码难度
        ......

    }
    // 配置的url可以不以http:// 开头因为这里会自动为我们加上
    if (StringUtils.hasText(url) && !url.startsWith("http")) {
        url = "http://" + url;
    }
    // 将url拼接上path
    String url = this.url + cleanPath();
    // 从子容器和主容器中获取Client
    // 默认情况下这里获取为null
    Client client = getOptional(context, Client.class);
    if (client != null) {
        if (client instanceof FeignBlockingLoadBalancerClient) {
            client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
        }
        if (client instanceof RetryableFeignBlockingLoadBalancerClient) {
            client = ((RetryableFeignBlockingLoadBalancerClient) client).getDelegate();
        }
        builder.client(client);
    }

    applyBuildCustomizers(context, builder);

    // 从主容器中获取Targeter
    // 默认是DefaultTargeter
    Targeter targeter = get(context, Targeter.class);
    // 创建feignClient
    return (T) targeter.target(this, builder, context, new HardCodedTarget<>(type, name, url));
}

上述方法主要在为创建feignClient做准备,概括如下。

  1. 获取Feign.BuilderFeign.Builder顾名思义就是用于构造feignClient,获取到Feign.Builder后还会将用于编解码的EncoderDecoder,以及用于解析SpringMVC注解的Contract都设置给它;
  2. 获取ClientfeignClient底层实际还是会使用一个具体的HTTP客户端来发送请求,而我们常用的HTTP客户端有例如HttpClientokhttp3.OkHttpClient。如果我们使用HttpClient,则需要引入feign-httpclient包,此时自动装配类FeignAutoConfiguration会基于HttpClient创建一个ApacheHttpClient实现了Client接口)然后添加到Spring容器中。如果我们使用okhttp3.OkHttpClient,则需要引入feign-okhttp包,此时自动装配类FeignAutoConfiguration会基于okhttp3.OkHttpClient创建一个feign.okhttp.OkHttpClient实现了Client接口)然后添加到Spring容器中。但如果我们没有引入任何HTTP客户端的依赖,那么Spring容器就没有任何Client接口的实现类的bean,所以上面代码注释中提及默认情况下获取到的Clientnull,这种情况就会使用默认的HTTP客户端feign.Client.Default
  3. 获取Targeter。默认获取到DefaultTargeter,而DefaultTargeter在创建feignClient时会直接调用Feign.Buildertarget() 方法,除此之外没有任何其它逻辑。

所以其实就是获取到Feign.Builder然后将创建feignClient时会使用到的一些关键组件例如ContractClient等设置给Feign.Builder,最后通过Feign.Builder来创建feignClient。而我们是通过定义接口来定义feignClient,所以用脚趾头想都能想到feignClient其实是对应接口的动态代理对象。

DefaultTargetertarget() 方法如下所示。

@Override
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
        Target.HardCodedTarget<T> target) {
    return feign.target(target);
}

继续跟进Feign.Buildertarget() 方法。

public <T> T target(Target<T> target) {
    // 这里的target就是HardCodedTarget
    // 代表我们的feign接口的信息
    // 这里要生成的就是feign接口的动态代理对象
    return build().newInstance(target);
}

上述build() 方法会返回一个ReflectiveFeign,最终调用ReflectiveFeignnewInstance() 方法来生成feign接口的动态代理对象作为feignClient,其中方法入参target就是HardCodedTarget,其type字段是feign接口的全限定名,其name字段是feignClient的名字,其url就是请求路径。所以下面分两部分来分析,先是build() 方法做了什么,其次如何创建动态代理对象。

首先看一下build() 方法,如下所示。

public Feign build() {
    // 基于Capability对Builder持有的组件做一下增强
    Client client = Capability.enrich(this.client, capabilities);
    Retryer retryer = Capability.enrich(this.retryer, capabilities);
    List<RequestInterceptor> requestInterceptors = this.requestInterceptors.stream()
            .map(ri -> Capability.enrich(ri, capabilities))
            .collect(Collectors.toList());
    Logger logger = Capability.enrich(this.logger, capabilities);
    Contract contract = Capability.enrich(this.contract, capabilities);
    // 很典型的就是对feignClient的配置做增强
    Request.Options options = Capability.enrich(this.options, capabilities);
    Encoder encoder = Capability.enrich(this.encoder, capabilities);
    Decoder decoder = Capability.enrich(this.decoder, capabilities);
    InvocationHandlerFactory invocationHandlerFactory =
            Capability.enrich(this.invocationHandlerFactory, capabilities);
    QueryMapEncoder queryMapEncoder = Capability.enrich(this.queryMapEncoder, capabilities);
    // 将Builder的属性赋值给SynchronousMethodHandler.Factory
    // 用于后续解析每个feign接口的方法为MethodHandler
    SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
            new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
                    logLevel, decode404, closeAfterDecode, propagationPolicy, forceDecoding);
    // 将Builder的属性和SynchronousMethodHandler.Factory赋值给ReflectiveFeign.ParseHandlersByName
    // 用于后续解析每个feign接口的方法为MethodHandler
    ReflectiveFeign.ParseHandlersByName handlersByName =
            new ReflectiveFeign.ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,
                    errorDecoder, synchronousMethodHandlerFactory);
    // 基于ReflectiveFeign.ParseHandlersByNamec创建ReflectiveFeign并返回
    return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
}

build() 方法中,首先就是会基于配置的CapabilityBuilder持有的一些组件做增强,然后创建ReflectiveFeign.ParseHandlersByName并基于ReflectiveFeign.ParseHandlersByName创建ReflectiveFeign,这里的ReflectiveFeign.ParseHandlersByName会在生成动态代理对象之前,把feign接口里面定义的每个方法解析成MethodHandler,这一点后面马上就会分析到。

现在继续看一下ReflectiveFeignnewInstance() 方法,如下所示。

@Override
public <T> T newInstance(Target<T> target) {
    // 基于ReflectiveFeign.ParseHandlersByName将feign接口的每个方法解析成MethodHandler
    // SpringMVC注解的解析也是在这里基于SpringMvcContract完成
    // 每个MethodHandler持有一个RequestTemplate.Factory用于创建RequestTemplate
    Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
    Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
    List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();

    // 最终feign接口每个方法对象Method都会对应一个MethodHandler
    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)));
        }
    }
    // 这里创建JDK动态里面的InvocationHandler
    // 实际类型是ReflectiveFeign.FeignInvocationHandler
    // 持有所有的MethodHandler
    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;
}

feign接口创建动态代理对象时,是基于JDK动态代理,所以最重要的就是InvocationHandler,这里的InvocationHandler实际为ReflectiveFeign.FeignInvocationHandler,其最关键的就是持有了所有feign接口的方法的MethodHandler,所以用脚趾头想都能想到当调用feignClient的方法来发起请求时,肯定是通过ReflectiveFeign.FeignInvocationHandler调用到对应的MethodHandler,然后在MethodHandler里面构建出Request再通过Client完成发送,所以在解析feign接口的方法为MethodHandler时,很关键的点就是通过SpringMvcContract解析SpringMVC注解信息然后创建出来BuildTemplateByResolvingArgs实现了RequestTemplate.Factory接口)。

至此,feignClientbean实例化源码大致分析完毕,这里进行一个小结。

  1. 获取Feign.Builder并为其设置创建feignClient的各种组件。要创建feignClient,首先需要有一个建造者,这里就是Feign.Builder,同时创建feignClient不是一个简单的事情,光靠Feign.Builder自己是搞不定的,所以还需要从容器中把SpringMvcContractClient等组件获取出来并给到Feign.Builder
  2. 基于Feign.Builder得到ReflectiveFeignFeign.Builder会创建一个ReflectiveFeign并把自己持有的各种组件给到ReflectiveFeign,最终是由ReflectiveFeign来实例化feignClient
  3. ReflectiveFeign中解析feign接口的方法得到MethodHandlerfeignClient是用来发起HTTP请求的,每个接口里的方法最终被调用时都会发起一个请求,所以每个feign接口的方法都会被创建出来一个MethodHandler,这个MethodHandler会完成HTTP请求的执行;
  4. feign接口基于JDK动态代理生成动态代理对象作为feignClient。在ReflectiveFeign中会先基于feign接口所有方法对应的MethodHandler创建得到JDK动态代理里面的InvocationHandler,这里实际类型为ReflectiveFeign.FeignInvocationHandler,然后基于创建得到的InvocationHandler生成JDK动态代理对象,就得到了feign接口对应的feignClient

5. feignClient方法的执行

因为feignClientJDK动态代理对象,所以调用feignClient的方法时,会调用到feignClient持有的InvocationHandlerinvoke() 方法,这里的InvocationHandler实际是ReflectiveFeign.FeignInvocationHandler,下面看一下其invoke() 方法的实现。

// 键为feign接口的方法的Method对象
// 值为feign接口方法对应的MethodHandler对象
private final Map<Method, MethodHandler> dispatch;

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
    // 省略部分代码

    // 根据Method对象从map里获取到对应的MethodHandler
    // 这里实际会调用到SynchronousMethodHandler的invoke()方法
    return dispatch.get(method).invoke(args);

}

上述方法中,先通过当前调用方法的Method对象获取到对应的MethodHandler,然后调用其invoke() 方法,这里的MethodHandler实际为SynchronousMethodHandler,继续跟进SynchronousMethodHandlerinvoke() 方法,如下所示。

@Override
public Object invoke(Object[] argv) throws Throwable {
    // 构建请求模板对象RequestTemplate
    // 这里初步包含了请求uri和请求方法等信息
    RequestTemplate template = buildTemplateFromArgs.create(argv);
    // 获取请求的配置信息
    // connectTimeout
    // readTimeout
    Request.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,此时包含了请求方法,请求uricharset等请求数据,但此时是不包含目标地址target的,target要在实际执行请求前才确定。此外上述方法还会获取出请求的建连超时connectTimeout和读取超时readTimeout配置,在实际执行请求时会携带上这些配置以控制超时时间。下面看一下实际执行请求的方法executeAndDecode(),如下所示。

Object executeAndDecode(RequestTemplate template, Request.Options options) throws Throwable {
    // 先调用每个拦截器RequestInterceptor的apply()方法来处理RequestTemplate
    // 然后设置RequestTemplate的target字段以确定请求的目标地址信息
    // 最后构建出真正的请求Request对象
    Request request = targetRequest(template);

    if (logLevel != Logger.Level.NONE) {
        logger.logRequest(metadata.configKey(), logLevel, request);
    }

    Response response;
    long start = System.nanoTime();
    try {
        // 使用HTTP客户端发起请求
        response = client.execute(request, options);
        response = response.toBuilder()
                .request(request)
                .requestTemplate(template)
                .build();
    } catch (IOException e) {
        if (logLevel != Logger.Level.NONE) {
            logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
        }
        throw errorExecuting(request, e);
    }
    long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);


    if (decoder != null)
        return decoder.decode(response, metadata.returnType());

    CompletableFuture<Object> resultFuture = new CompletableFuture<>();
    // 处理响应体为feign接口方法的返回类型
    asyncResponseHandler.handleResponse(resultFuture, metadata.configKey(), response,
            metadata.returnType(),
            elapsedTime);

    try {
        if (!resultFuture.isDone())
            throw new IllegalStateException("Response handling not done");

        return resultFuture.join();
    } catch (CompletionException e) {
        Throwable cause = e.getCause();
        if (cause != null)
            throw cause;
        throw e;
    }
}

上述方法首先是使用RequestTemplate构建出请求对象Request,在构建前,会对RequestTemplate做如下处理。

  1. 应用拦截器RequestInterceptor。这里会执行所有配置给当前feignClient的拦截器的逻辑,所以Openfeign里面的拦截器,其实只能在请求发起前,对请求的一些信息做一下处理,例如设置header
  2. 设置请求的目标地址target。这里主要就是基于HardCodedTargeturl完成目标地址的确认,然后设置给RequestTemplatetarget字段。

处理完RequestTemplate后,就会基于RequestTemplate构建出Request对象,通过Request对象,可以确定本次请求的 请求方法请求url请求头请求体等信息。

得到Request对象后,就会使用当前feignClient底层使用的HTTP客户端发起请求,然后再把响应体内容处理成对应的结构,最后返回处理后的对象。

总结

Openfeign里面的feignClient,实际就是由@FeignClient注解修饰的接口的动态代理对象,这个动态代理对象,其生成路径可以概括为下图。

Springcloud-feignClient动态代理对象生成路径图

后续在调用到feignClient的方法时,调用了哪个方法,对应方法的MethodHandler的逻辑就会执行,也就是生成Request然后调用到底层的实际发送HTTP请求的客户端完成请求发送,因为我们对feignClient的配置最终会体现在MethodHandler中,所以例如配置的连接超时或读取超时,都会在MethodHandler中实际发起请求时生效。

一个feignClient的创建使用到了很多bean,这些bean大部分都是通过FeignContext获取的,而FeignContext就是一个NamedContextFactory,所以每个feignClient都有自己的容器上下文,因此Openfeign其实就是Spring父子容器的典型应用场景。