从Spring父子容器到Openfeign原理解析
前言
本文将从Spring父子容器开始,一步一步推进到Openfeign的底层原理,让我们在学习Openfeign的底层原理时,不至于被一个又一个陌生的概念所困扰。
本文也将尽量减少怼着源码无效输出,更有效的方式是找出 平时我们接触Feign客户端最多的一些东西是怎么体现在源码中的 。
Springcloud版本是3.1.1,思维导图如下。
注意,Feign是Springcloud的一个Http客户端组件,而Openfeign是在Feign的基础上增加支持了SpringMVC的注解,现在都是使用的Openfeign,所以提到Feign其实就是指Openfeign。
正文
一. Spring父子容器
Openfeign的底层实现中,处处都有父子容器的使用,但是处处又都不明显,所以这里先回味一下Spring父子容器,我盲猜应该还是有人没太关注过这个概念的。
1. 概念速览
Spring的容器,是有父子概念的,子容器可以获取父容器的bean,但是反过来父容器不能获取子容器中的bean。
2. 使用场景
视野先狭窄一点,考虑同一个工程不同的包目录下有两个名字一样的类,那么默认情况下,这两个类不能同时都注册bean到Spring容器中的,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.MyBean和xxx2.MyBean都注册相同名字的bean到同一个Spring容器中,如果分别注册到不同的容器中,这是可以的,但是问题也来了,不同的容器的话,通过其中任何一个容器,都只能获取当前容器里面的bean,这也是不太方便的,但如果是父子容器,问题就迎刃而解了,其中一个bean注册到父容器,另外一个bean注册到子容器,此时通过子容器,xxx1.MyBean和xxx2.MyBean的bean就都能访问到了。
现在视野打开一点,假如有一个第三方包,这个第三方包如果要发挥自己的作用的话,需要往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目录下的MyBean和Service1Config如下所示。
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目录下的MyBean和Service2Config如下所示。
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来初始化容器上下文,还会拿如下两部分内容来初始化容器上下文。
- NamedContextFactory的configurations中名字以default. 开头的Specification。我们可以向configurations中加入若干个名字以default. 开头的Specification,那么每创建一个容器上下文时,都会用这些名字以default. 开头的Specification来初始化容器上下文;
- NamedContextFactory的defaultConfigType字段。defaultConfigType字段就是一个真正的配置类的Class对象,在创建NamedContextFactory时就需要指定defaultConfigType,后续每创建一个容器上下文,都需要使用defaultConfigType来初始化。
那么NamedContextFactory创建出来的容器上下文,会被三部分内容来初始化,其一是独属于自己的Specification,其二是名字以default. 开头的Specification,其三就是NamedContextFactory的defaultConfigType字段代表的真正的配置类,那么啥叫初始化呢,其实就是给容器上下文一个真正的配置类,然后容器上下文通过这个真正的配置类来加载bean。
NamedContextFactory的一个简单概念图示如下所示。
最后补充一点,NamedContextFactory中的所有容器上下文,都有一个共同的父容器,就是Spring应用中的主容器。
2. 源码简析
其实如果理解了上一小节的内容,那么是不需要看NamedContextFactory的源码的,因为NamedContextFactory的核心方法getContext(),本质就是在完成上一小节所阐述的事情,但是看了源码后,才能更有底气基于NamedContextFactory做扩展,所以本小节就针对NamedContextFactory的getContext() 方法简单分析一下。
NamedContextFactory的getContext() 方法实现如下。
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() 方法中将主容器赋值给了NamedContextFactory的parent字段,而在createContext() 方法中,每创建一个容器上下文时,都会将其父设置为NamedContextFactory的parent字段,所以NamedContextFactory中的所有容器上下文,都是主容器的子容器,换言之,每个NamedContextFactory中的容器上下文,都能够访问主容器中的bean。
3. 示例演示
本小节以一个示例demo,来演示如何使用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>
因为NamedContextFactory是Springcloud组件,所以需要引入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,其只干一件事情,就是在构造函数中指定defaultConfigType为DefaultConfig.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容器上下文创建,此时DefaultConfig和DefaultBeanConfig默认会用于初始化LOGIN容器上下文,同时独属于LOGIN容器上下文的LoginConfig也会用于初始化LOGIN容器上下文。
同理,在第一次获取名字为LOGOUT的容器上下文时,触发了LOGOUT容器上下文创建,同样的DefaultConfig和DefaultBeanConfig默认会用于初始化LOGOUT容器上下文,同时独属于LOGOUT容器上下文的LogoutConfig也会用于初始化LOGOUT容器上下文。
4. Openfeign里面的NamedContextFactory
在Openfeign里面,每一个feignClient都有一个独属于自己的容器上下文,同时每个feignClient除了依赖自己容器上下文里面的bean外,还会使用到主容器里面的bean,所以Openfeign底层原理里面,大量使用到了NamedContextFactory,现在我们来对号入座一下。
NamedContextFactory原生概念 | Openfeign对应的实现 |
---|---|
NamedContextFactory | FeignContext |
NamedContextFactory的defaultConfigType字段 | FeignClientsConfiguration.class |
Specification接口 | FeignClientSpecification |
首先,FeignContext中持有每一个feignClient的容器上下文。
其次,FeignClientsConfiguration里面会注册一些例如Decoder和Encoder的bean,这些是每个feignClient都会使用到的,故每个feignClient的容器上下文都会使用FeignClientsConfiguration来初始化,所以FeignContext的defaultConfigType字段被设置为了FeignClientsConfiguration的Class对象。
除此之外,开启Openfeign功能的@EnableFeignClients注解的defaultConfiguration属性可以指定一组真实的配置类的Class对象,后续会基于这组真实的配置类的Class对象创建出来一个名字以default. 开头的FeignClientSpecification出来,所以通过@EnableFeignClients注解指定的配置是应用于所有feignClient的。
最后,用于标记feignClient的@FeignClient注解的configuration属性也可以指定一组真实的配置类的Class对象,后续会基于这组真实的配置类的Class对象创建出来一个名字和feignClient名字相同的FeignClientSpecification出来,所以通过@FeignClient注解指定的配置仅应用于对应的feignClient。
三. Openfeign
1. 示例演示
我们需要搭建一个示例工程,方便后续演示和代码调试。工程目录结构如下所示。
首先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注解,所以feignClient的BeanDefinition注册源码的分析,就从@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注解的属性。
首先是value,basePackages和basePackageClasses属性,这三个属性都用于指定若干个扫描的包路径,路径下的所有的由@FeignClient注解修饰的feignClient都会被加载,这三个属性可以同时使用来共同指定扫描的包路径。
然后是defaultConfiguration属性,这个用于指定一组配置类,这一组配置类会被创建为一个名字以default. 开头的FeignClientSpecification,后续每一个feignClient自己的容器都会使用这个以default. 开头的FeignClientSpecification来初始化。
最后是clients属性,通过该属性可以直接指定若干个feignClient,但是被指定的类一定要由@FeignClient注解修饰,特别注意feignClient属性和value,basePackages及basePackageClasses属性是互斥的,且feignClient属性优先级更高。
@EnableFeignClients注解的核心其实是其通过@Import注解引入的FeignClientsRegistrar。FeignClientsRegistrar继承于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注解修饰的类,然后加载为feignClient,registerFeignClients() 方法实现如下。
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);
}
}
}
虽然上面方法代码看着一大堆,但是其实就干三件事情。
- 拿到所有的feignClient的类对象。优先使用@EnableFeignClients注解的clients属性,其次使用@EnableFeignClients注解的value,basePackages和basePackageClasses属性;
- 为每个feignClient注册专属的FeignClientSpecification到容器中。@FeignClient注解修饰的接口就是feignClient,@FeignClient注解的configuration属性构造出来的FeignClientSpecification就是专属于这个feignClient的配置,这个配置会用于后续对应feignClient的容器的初始化;
- 注册feignClient到容器中。
上述第3点,就是生成feignClient的bean对应的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);
}
上面方法看着更是一大堆,下面挑重点概括一下。
- 得到contextId。我们可以直接通过@FeignClient注解的contextId属性来指定这个值,如果不指定,则会依次取name和value属性的值。contextId的作用是用于区分不同的feignClient的容器上下文和配置;
- 创建FeignClientFactoryBean。FeignClientFactoryBean继承于FactoryBean,用于创建feignClient的bean;
- 为feignClient创建持有Supplier的BeanDefinition。每一个feignClient的BeanDefinition都持有一个Supplier,在创建feignClient的bean的时候就会调用到Supplier的get() 方法,上述的Supplier的get() 方法最终会调用到FeignClientFactoryBean的getObject() 方法,所以feignClient的bean其实就还是FeignClientFactoryBean来创建出来的;
- 注册每个feignClient的BeanDefinition。
至此注册feignClient的BeanDefinition的逻辑就分析完毕。
3. FeignContext初始化源码
在继续分析FeignClientFactoryBean的getObject() 方法前,我们需要回顾下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实例化源码
现在继续分析FeignClientFactoryBean的getObject() 方法,这里就是在真正的实例化feignClient,我们提供的大部分配置也是在这个阶段应用到feignClient的。
在正式开始前得先看看FeignClientFactoryBean的get() 和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正常提供功能需要使用的组件。
现在正式开始分析FeignClientFactoryBean的getObject() 方法,源码如下所示。
@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做准备,概括如下。
- 获取Feign.Builder。Feign.Builder顾名思义就是用于构造feignClient,获取到Feign.Builder后还会将用于编解码的Encoder和Decoder,以及用于解析SpringMVC注解的Contract都设置给它;
- 获取Client。feignClient底层实际还是会使用一个具体的HTTP客户端来发送请求,而我们常用的HTTP客户端有例如HttpClient和okhttp3.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,所以上面代码注释中提及默认情况下获取到的Client为null,这种情况就会使用默认的HTTP客户端feign.Client.Default;
- 获取Targeter。默认获取到DefaultTargeter,而DefaultTargeter在创建feignClient时会直接调用Feign.Builder的target() 方法,除此之外没有任何其它逻辑。
所以其实就是获取到Feign.Builder然后将创建feignClient时会使用到的一些关键组件例如Contract和Client等设置给Feign.Builder,最后通过Feign.Builder来创建feignClient。而我们是通过定义接口来定义feignClient,所以用脚趾头想都能想到feignClient其实是对应接口的动态代理对象。
DefaultTargeter的target() 方法如下所示。
@Override
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
Target.HardCodedTarget<T> target) {
return feign.target(target);
}
继续跟进Feign.Builder的target() 方法。
public <T> T target(Target<T> target) {
// 这里的target就是HardCodedTarget
// 代表我们的feign接口的信息
// 这里要生成的就是feign接口的动态代理对象
return build().newInstance(target);
}
上述build() 方法会返回一个ReflectiveFeign,最终调用ReflectiveFeign的newInstance() 方法来生成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() 方法中,首先就是会基于配置的Capability对Builder持有的一些组件做增强,然后创建ReflectiveFeign.ParseHandlersByName并基于ReflectiveFeign.ParseHandlersByName创建ReflectiveFeign,这里的ReflectiveFeign.ParseHandlersByName会在生成动态代理对象之前,把feign接口里面定义的每个方法解析成MethodHandler,这一点后面马上就会分析到。
现在继续看一下ReflectiveFeign的newInstance() 方法,如下所示。
@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接口)。
至此,feignClient的bean实例化源码大致分析完毕,这里进行一个小结。
- 获取Feign.Builder并为其设置创建feignClient的各种组件。要创建feignClient,首先需要有一个建造者,这里就是Feign.Builder,同时创建feignClient不是一个简单的事情,光靠Feign.Builder自己是搞不定的,所以还需要从容器中把SpringMvcContract和Client等组件获取出来并给到Feign.Builder;
- 基于Feign.Builder得到ReflectiveFeign。Feign.Builder会创建一个ReflectiveFeign并把自己持有的各种组件给到ReflectiveFeign,最终是由ReflectiveFeign来实例化feignClient;
- 在ReflectiveFeign中解析feign接口的方法得到MethodHandler。feignClient是用来发起HTTP请求的,每个接口里的方法最终被调用时都会发起一个请求,所以每个feign接口的方法都会被创建出来一个MethodHandler,这个MethodHandler会完成HTTP请求的执行;
- 为feign接口基于JDK动态代理生成动态代理对象作为feignClient。在ReflectiveFeign中会先基于feign接口所有方法对应的MethodHandler创建得到JDK动态代理里面的InvocationHandler,这里实际类型为ReflectiveFeign.FeignInvocationHandler,然后基于创建得到的InvocationHandler生成JDK动态代理对象,就得到了feign接口对应的feignClient。
5. feignClient方法的执行
因为feignClient是JDK动态代理对象,所以调用feignClient的方法时,会调用到feignClient持有的InvocationHandler的invoke() 方法,这里的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,继续跟进SynchronousMethodHandler的invoke() 方法,如下所示。
@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,此时包含了请求方法,请求uri和charset等请求数据,但此时是不包含目标地址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做如下处理。
- 应用拦截器RequestInterceptor。这里会执行所有配置给当前feignClient的拦截器的逻辑,所以Openfeign里面的拦截器,其实只能在请求发起前,对请求的一些信息做一下处理,例如设置header;
- 设置请求的目标地址target。这里主要就是基于HardCodedTarget的url完成目标地址的确认,然后设置给RequestTemplate的target字段。
处理完RequestTemplate后,就会基于RequestTemplate构建出Request对象,通过Request对象,可以确定本次请求的 请求方法,请求url,请求头和 请求体等信息。
得到Request对象后,就会使用当前feignClient底层使用的HTTP客户端发起请求,然后再把响应体内容处理成对应的结构,最后返回处理后的对象。
总结
Openfeign里面的feignClient,实际就是由@FeignClient注解修饰的接口的动态代理对象,这个动态代理对象,其生成路径可以概括为下图。
后续在调用到feignClient的方法时,调用了哪个方法,对应方法的MethodHandler的逻辑就会执行,也就是生成Request然后调用到底层的实际发送HTTP请求的客户端完成请求发送,因为我们对feignClient的配置最终会体现在MethodHandler中,所以例如配置的连接超时或读取超时,都会在MethodHandler中实际发起请求时生效。
一个feignClient的创建使用到了很多bean,这些bean大部分都是通过FeignContext获取的,而FeignContext就是一个NamedContextFactory,所以每个feignClient都有自己的容器上下文,因此Openfeign其实就是Spring父子容器的典型应用场景。
转载自:https://juejin.cn/post/7364764922339000354