文章概览
- spring cloud starter openfeign如何使用
- 简单解析spring cloud starter openfeign的工作原理
spring cloud feign是如何使用的,跟feign的原生使用有什么不同
- spring cloud feign提供了2个关键注解
- @FeignClient
- @EnableFeignClients
- spring cloud feign使用的注解规范遵循spring mvc
- feign直接使用Buider构建得到实例,spring cloud feign直接依赖注入
从@EnableFeignClients去看feign客户端怎么生成和被spring管理
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {}
- FeignClientsRegistrar 是Feign的扫描工具和注册器
class FeignClientsRegistrar
implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
//注册全局默认配置
//@EnableFeignClients中有属性Class<?>[] defaultConfiguration() default {},就是默认一个全局
//配置类,那在配置类中可以注入什么bean,可以参考feign.Feign.Builder中的实例对象
registerDefaultConfiguration(metadata, registry);
//注册feign客户端
//这里面的逻辑大概就是把所有被@FeignClient注解的接口扫描出来,解析注解源数据
//构造BeanDefinition注入到BeanFactory中,class类为org.springframework.cloud.openfeign.FeignClientFactoryBean
registerFeignClients(metadata, registry);
}
}
- FeignClientsRegistrar 的职责是对FeignClient注解的接口进行属性解析,然后转换成BeanDefinition纳入容器管理,到这里它的职责已经完成,那么接下来就要看Spring依赖注入时候,如何解析@FeignClient注册的接口,答案是FeignClientFactoryBean
FeignClientFactoryBean
- 工厂Bean就直接看getObject
@Override
public Object getObject() throws Exception {
return getTarget();
}
<T> T getTarget() {
// feign builder构建关键看这2行
FeignContext context = this.applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context);
// 省略N行代码
// 返回代理对象,这部分跟feign core分析的路径是一样的
}
FeignContext
- 看FeignContext之前,先抛出一些问题
- feign core原生使用时,我们是通过手动构造实例来使用的,可以回顾Demo
Api target = Feign.builder().target(Api.class, "https://www.baidu.com");
- 可以看到,builder()方法链出来之后我们可以为feign的各个组件设置单独的实例对象,但是spring cloud feign是怎么为每个feign的组件构造单独的对象的呢?
- 简单分析FeignContext
- 源码,关注3个类【FeignClientsConfiguration/NamedContextFactory/FeignClientSpecification】
public class FeignContext extends NamedContextFactory<FeignClientSpecification> {
public FeignContext() {
super(FeignClientsConfiguration.class, "feign", "feign.client.name");
}
}
- NamedContextFactory
- 直译就是命名上下文工厂,作用:为某个Bean类的组件化独立出上下文
- NamedContextFactory很容易理解,要关注它的属性变量和关键方法
// 这是它的3个关键变量
private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap();
private Map<String, C> configurations = new ConcurrentHashMap();
private ApplicationContext parent;
// 这是它的关键方法
public <T> T getInstance(String name, Class<T> type) {
AnnotationConfigApplicationContext context = this.getContext(name);
return BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, type).length > 0 ? context.getBean(type) : null;
}
- contexts变量是一个集合,它主要用于存储N个互不干扰的上下文,这个上下文就是spring的上下文
- configurations变量其实就是关联着每个独立上下文独立持有的配置类
- parent其实就是当前运行的spring应用的上下文
- FeignClientSpecification
- 它实现了NamedContextFactory.Specification,其实就是定义了上文讲的独立上下文的名字,以及关联的配置文件
- FeignClientsConfiguration
- 这个配置类,其实就是定义了Feign的各个核心组件,比如Decoder,Encoder,Contract等
- 简单回复上面提出的一个观点,那就是spring cloud feign使用的注解是基于springmvc的规范的,那么这部分是如何实现的呢?答案就是前面说的Contract,spring cloud openfeign复写了feign提供的默认Contract,实现了对spring mvc注解的兼容(org.springframework.cloud.openfeign.support.SpringMvcContract)
- 再一次回到getTarget,我们再看FeignContext是什么时候纳入Spring管理的,答案是:FeignAutoConfiguration,FeignAutoConfiguration是通过Spring cloud的装配文件发现的
- 观察一下FeignAutoConfiguration的关键代码(这个configuration是怎么来的呢?其实就是FeignClientsRegistrar 在注册FeignClient的时候为每个客户端注入的一份默认配置)
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;
}
// 省略N行代码
}
- FeignClientFactoryBean#feign 开始构造Feign.Builder实例
protected Feign.Builder feign(FeignContext context) {
// 注意观察这里的get()方法,调用的就是我上面说的getInstance
Feign.Builder builder = get(context, Feign.Builder.class)
.logger(logger)
.encoder(get(context, Encoder.class))
.decoder(get(context, Decoder.class))
.contract(get(context, Contract.class));
// 为feign配置日志级别,重试策略,超时时间等
// 这个方法里面的细分方法执行顺序需要重点关注,这里跟我们后面要讲的遇到的坑有关系
configureFeign(context, builder);
return builder;
}
- 分析到这里,其实FeignContext的作用已经展示地比较清楚了,并且Builder对象已经构建完成了,接下来就是设置Target,然后返回代理对象了
NamedContextFactory
-
为什么单独讲NamedContextFactory
- 我认为NamedContextFactory是一个比较不错的设计,它把上下文隔开,提供了一套机制让我们去设计一些“大组件包小组件”,包括spring cloud ribbon也是用到这个NamedContextFactory
- NamedContextFactory的源码简单,方便阅读
-
介绍一个重要的流程
public <T> T getInstance(String name, Class<T> type) {
// 这里会根据feign的name去获得它的独立上下文
AnnotationConfigApplicationContext context = getContext(name);
if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context,
type).length > 0) {
// 这个代码其实就是经典的spring上下文获取bean的方法
return context.getBean(type);
}
return null;
}
protected AnnotationConfigApplicationContext getContext(String name) {
if (!this.contexts.containsKey(name)) {
synchronized (this.contexts) {
if (!this.contexts.containsKey(name)) {
// 在检查到集合不包含当前name的独立上下文时进入创建的动作
this.contexts.put(name, createContext(name));
}
}
}
return this.contexts.get(name);
}
// 创建的代码非常经典,像是刚学习spring的时候,那种手动创建上下文的感觉
protected AnnotationConfigApplicationContext createContext(String name) {
// 创建一个上下文实例
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
// 把所有配置类都register进去,这个时候就会 对配置类进行 扫描,执行BeanDefinition的注册
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.")) {
for (Class<?> configuration : entry.getValue().getConfiguration()) {
context.register(configuration);
}
}
}
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) {
// 这一步是关键,设置了父级上下文,也就是说虽然是独立上下文
// 但是在获取一些Bean不存在时,会向上进行递归,这个时候就是多个上下文共享一个bean的情况了
// 这种机制很巧妙,但是也很容易遇到坑,后面会讲
context.setParent(this.parent);
context.setClassLoader(this.parent.getClassLoader());
}
context.setDisplayName(generateDisplayName(name));
// 刷新上下文,进行bean和各种处理器的初始化
context.refresh();
return context;
}
- 走到这里可以回答上面的问题(spring cloud feign是怎么为每个feign的组件构造单独的对象的呢)
- NamedContextFactory就是解决方案,每个feign有自己的独立上下文,所以feign客户端下的每个组件都是独立的