Feign源码解析二
前言
上文中,我们搭建了两个服务,一个user-service,一个order-service,来模拟利用FeignCliet发起远程调用的实现
本文会基于Feign源码,看看Feign到底是怎么实现远程调用
源码
@EnableFeignClients注解
上文中,我们的user-service
服务需要调用远程的order-service
服务完成一定的业务逻辑,而基本实现是order-service提供一个spi的jar包给user-service依赖,并且在user-service的启动类上添加了一个注解
这个注解就是@EnableFeignClients,接下来我们就从这个注解入手,一步一步解开Feign的神秘面纱
- 类描述
/**
* Scans for interfaces that declare they are feign clients (via
* {@link org.springframework.cloud.openfeign.FeignClient} <code>@FeignClient</code>).
* Configures component scanning directives for use with
* {@link org.springframework.context.annotation.Configuration}
* <code>@Configuration</code> classes.
*
* @author Spencer Gibb
* @author Dave Syer
* @since 1.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
...
}
该注解类上的注释大概的意思就是:
扫描那些被声明为Feign Clients
(只要有org.springframework.cloud.openfeign.FeignClient
注解修饰的接口都是Feign Clients接口)的接口
- 重要属性
-
basePackages
值类型:String[]
规定了扫描的基础包位置 -
clients
值类型:Class<?>[]
规定了扫描带有@FeignClient注解的指定类全路径Class的集合
下面我们继续追踪源码,看看到底什么地方用到了这个注解
利用IDEA的查找调用链快捷键,可以发现在.class类型的文件中只有一个文件用到了这个注解
OK,下面主要就是看这个类做了什么
FeignClientsRegistrar.class
- UML类图
通过UML图我们发现该类分别实现了ImportBeanDefinitionRegistrar
, ResourceLoaderAware
以及EnvironmentAware
接口
这三个接口均是spring-framework框架的spring-context模块下的接口,都是和spring上下文相关,具体作用下文会分析
- 重要属性(
property
)
// 资源加载器,可通过该资源加载器加载classpath下的所有文件
private ResourceLoader resourceLoader;
// 上下文环境,可通过该环境获取当前应用配置属性等
private Environment environment;
总结下来就是利用这两个重要属性,一个获取应用配置属性,一个可以加载classpath下的文件,那么FeignClientsRegistrar持有这两个东西之后要做什么呢?
- 重要方法(
Method
)
// 1. 初始化当前上下文环境属性
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
// 2. 初始化资源加载器属性
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
// 3. 最重要的一个来了,注册bean定义
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
// 注册默认配置
registerDefaultConfiguration(metadata,registry);
// 注册FeignClients
registerFeignClients(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拼接开头
name = "default." + metadata.getClassName();
}
registerClientConfiguration(registry, name,
defaultAttrs.get("defaultConfiguration"));
}
}
- 这里的注册默认配置方法,读取启动类上面 @EnableFeignClients注解中的
defaultConfiguration
,默认name为default,一般情况下无需配置。用默认的FeignAutoConfiguration即可。 上面有个比较重要的方法:注册配置registerClientConfiguration(...)
,启动流程一共有两处读取feign的配置,这是第一处。根据该方法看一下
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());
}
上面将bean配置类包装成FeignClientSpecification
,注入到容器。该对象非常重要,包含FeignClient需要的重试策略
,超时策略
,日志
等配置,如果某个FeignClient服务没有设置独立的配置类,则读取默认的配置,可以将这里注册的bean理解为整个应用中所有feign的默认配置
- 题外话:
由于FeignClientsRegistrar
实现了ImportBeanDefinitionRegistrar
接口,这里简单提下这个接口的作用
我们知道在spring框架中,我们如果想注册一个bean的话主要由两种方式:自动注册/手动注册
-
类上增加@Component,@Service,@Controller等注解,由Spring
自动
帮我们注册
成bean -
配置文件(.xml中增加<bean>...</bean>标签)或配置类(@Configuration)中增加@Bean注解的类,也会由Spring帮我们
自动注册
成bean -
通过实现
ImportBeanDefinitionRegistrar
接口,并实现其registerBeanDefinitions方法,手动注册
bean
知道了ImportBeanDefinitionRegistrar
接口的作用,下面就来看下FeignClientsRegistrar
类是何时被加载实例化的
通过IDEA工具搜索引用链,发现该类是在注解@EnableFeignClients上被import进来的,文章开始的图片中有
@Import(FeignClientsRegistrar.class)
这里提下@Import注解的作用
/**
* Indicates one or more {@link Configuration @Configuration} classes to import.
*
* <p>Provides functionality equivalent to the {@code <import/>} element in Spring XML.
* Allows for importing {@code @Configuration} classes, {@link ImportSelector} and
* {@link ImportBeanDefinitionRegistrar} implementations, as well as regular component
* classes (as of 4.2; analogous to {@link AnnotationConfigApplicationContext#register}).
*
* <p>{@code @Bean} definitions declared in imported {@code @Configuration} classes should be
* accessed by using {@link org.springframework.beans.factory.annotation.Autowired @Autowired}
* injection. Either the bean itself can be autowired, or the configuration class instance
* declaring the bean can be autowired. The latter approach allows for explicit, IDE-friendly
* navigation between {@code @Configuration} class methods.
*
* <p>May be declared at the class level or as a meta-annotation.
*
* <p>If XML or other non-{@code @Configuration} bean definition resources need to be
* imported, use the {@link ImportResource @ImportResource} annotation instead.
*
* @author Chris Beams
* @author Juergen Hoeller
* @since 3.0
* @see Configuration
* @see ImportSelector
* @see ImportResource
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {
/**
* {@link Configuration}, {@link ImportSelector}, {@link ImportBeanDefinitionRegistrar}
* or regular component classes to import.
*/
Class<?>[] value();
}
该注解仅有一个属性value,使用该注解表明导入一个或者多个@Configuration类,其作用和.xml文件中的<import>等效,其允许导入@Configuration类,ImportSelector接口/ImportBeanDefinitionRegistrar接口的实现,也同样可以导入一个普通的组件类
注意,如果是XML或非@Configuration的bean定义资源需要被导入的话,需要使用@ImportResource注解代替
这里我们导入的FeignClientsRegistrar类正是一个ImportBeanDefinitionRegistrar接口的实现
FeignClientsRegistrar重写了该接口的registerBeanDefinitions
方法,该方法有两个参数注解元数据metadata
和bean定义注册表registry
该方法会由spring负责调用,继而注册所有标注为@FeignClient注解的bean定义
registerFeignClients(...)
下面看registerBeanDefinitions方法中的第二个方法,在该方法中完成了所有@FeignClient注解接口的扫描工作,以及注册到spring中,注意这里注册bean的类型为FeignClientFactoryBean
,下面细说
public void registerFeignClients(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
// 获取ClassPath扫描器
ClassPathScanningCandidateComponentProvider scanner = getScanner();
// 为扫描器设置资源加载器
scanner.setResourceLoader(this.resourceLoader);
Set<String> basePackages;
// 1. 从@EnableFeignClients注解中获取到配置的各个属性值
Map<String, Object> attrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName());
// 2. 注解类型过滤器,只过滤@FeignClient
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
FeignClient.class);
// 3. 从1. 中的属性值中获取clients属性的值
final Class<?>[] clients = attrs == null ? null
: (Class<?>[]) attrs.get("clients");
if (clients == null || clients.length == 0) {
// 扫描器设置过滤器且获取需要扫描的基础包集合
scanner.addIncludeFilter(annotationTypeFilter);
basePackages = getBasePackages(metadata);
}
else {
// clients属性值不为null,则将其clazz路径转为包路径
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)));
}
// 3. 扫描基础包,且满足过滤条件下的接口封装成BeanDefinition
for (String basePackage : basePackages) {
Set<BeanDefinition> candidateComponents = scanner
.findCandidateComponents(basePackage);
// 遍历扫描到的bean定义
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) {
// 并校验扫描到的bean定义类是一个接口
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
Assert.isTrue(annotationMetadata.isInterface(),
"@FeignClient can only be specified on an interface");
// 获取@FeignClient注解上的各个属性值
Map<String, Object> attributes = annotationMetadata
.getAnnotationAttributes(
FeignClient.class.getCanonicalName());
String name = getClientName(attributes);
// 可以看到这里也注册了一个FeignClient的配置bean
registerClientConfiguration(registry, name,
attributes.get("configuration"));
// 注册bean定义到spring中
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
}
总结一下该方法,就是扫描@EnableFeignClients注解上指定的basePackage或clients值,获取所有@FeignClient注解标识的接口,然后将这些接口一一调用以下两个重要方法
完成注册configuration配置bean
和注册FeignClient bean
// ...省略部分代码
Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(FeignClient.class.getCanonicalName());
// 获取@FeignClient注解中指定的name
String name = getClientName(attributes);
// 1. 获取@FeignClient注解中指定的configuration,并以FeignClientSpecification类注册bean
registerClientConfiguration(registry, name, attributes.get("configuration"));
// 2. 注册FeignClient bean,以FeignClientFactoryBean类注册
registerFeignClient(registry, annotationMetadata, attributes);
- 可以理解为每个单独的FeignClient接口都会注册一个自己的configuration bean定义到spring的beanDefinitionMap中,key是@FeignClient注解上指定的serviceId/name/value值,value则是一个类型为
FeignClientSpecification
的BeanDefinition
- 中完成了每个FeignClient接口的client bean的注册,beanDefinitionMap中的key是@FeignClient注解的每个接口的全限定名,value则是类型是
FeignClientFactoryBean
的BeanDefinition
断点位置相当重要
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
这里是利用了spring的代理工厂来生成代理类,即这里将所有的 feignClient的描述信息BeanDefinition
设定为 FeignClientFactoryBean
类型,该类继承自FactoryBean,因此这是一个代理类,FactoryBean是一个工厂bean,用作创建代理bean,所以得出结论,feign将所有的feignClient
bean定义的类型包装成 FeignClientFactoryBean
最终其实就是存入了BeanFactory的beanDefinitionMap中
DefaultListableBeanFactory.png那么代理类什么时候会触发生成呢? 在spring刷新容器时
,会根据beanDefinition去实例化bean,如果beanDefinition的beanClass类型为代理bean,则会调用其T getObject() throws Exception;
方法生成代理bean,而我们实际利用注入进来的FeignClient接口就是这些一个个代理类
总结
这里有坑...
这里有一个需要注意的点,也是开发中会遇到的一个启动报错点
如果我们同时定义了两个不同名称的接口
(同一个包下/或依赖方指定全部扫描我们提供的@FeignClient
),且这两个@FeignClient
接口注解的value/name/serviceId
值一样的话,依赖方拿到我们的提供的spi依赖,启动类上@EnableFeignClients
注解扫描能同时扫描到这两个接口,就会启动报错
原因就是Feign会为每个@FeignClient注解标识的接口都注册一个以serviceId/name/value为key,FeignClientSpecification类型的bean定义为value去spring注册bean定义,又默认不允许覆盖bean定义,所以报错
报错信息.png官方提示给出的解决方法要么改个@FeignClient注解的serviceId,name,value属性值,要么就开启spring允许bean定义覆写
spring.main.allow-bean-definition-overriding=true
至此我们知道利用在springboot的启动类上添加的@EnableFeignClients注解,该注解中import进来了一个手动注册bean的FeignClientsRegistrar注册器
,该注册器会由spring加载其registerBeanDefinitions方法
,由此来扫描所有@EnableFeignClients注解定义的basePackages包路径下的所有标注为@FeignClient注解的接口,并将其注册到spring的bean定义Map中,并实例化bean
下一篇博文中,我会分析为什么我们在调用(@Resource)这些由@FeignClient注解的bean的方法时会发起远程调用