前言
feign 是目前微服務間通信的主流方式,是springCloud中一個非常重要的組件。他涉及到了負載均衡、限流等組件,是服務之間的信使。系列文章都是以2.1.3版本作為學習依據。
一、feign的使用
feign 的使用和dubbo的使用本質上非常相似。dubbo的理念是:像調用本地方法一樣調用遠程方法。那么套在feign上同樣適用:像調用本地接口一樣調用遠程接口。
使用feign只需要2步:
- 定義一個接口並用FeignClient注解說明接口所在服務和路徑
- 服務啟動類上添加@EnableFeignClients。
1.1,定義一個feign接口
@FeignClient(contextId = "order", name = "order", path = "/app")
public interface OrderApiFeignClient {
/**
* 獲取訂單列表
* @return
*/
@RequestMapping("order/list")
BaseResponse<List<OrderVO>> obtaining(@PathVariable("userId") Long userId);
}
1.2,再啟動類上添加注解
@EnableSwagger2
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients("com.xxx.*")
@ComponentScan(value={"com.xxx"})
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication .class, args);
}
}
二、feign 接口如何被實例化到spring容器的?
首先按照一般的思路,我們會猜測基於接口生成代理類,然后對接口的調用實際上調的是代理對象,那真的是這樣么? 我們帶着猜想往下看。
2.1 @EnableFeignClients 注解都做了些什么?
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
}
可以看到注解本身主要定義了要掃描的feign接口包路徑以及配置,但是注解本身又有注解Import ,可以看到他引入了FeignClientsRegistrar到容器。從名字看這個類就應該是在將feign接口注冊到容器中,接下來我們具體看一下這個類干了些什么。
/**
* @author Spencer Gibb
* @author Jakub Narloch
* @author Venil Noronha
* @author Gang Li
*/
class FeignClientsRegistrar
implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
可以看到FeignClientsRegistrar實現了ImportBeanDefinitionRegistrar接口,但凡是實現了這個接口的類被注入到容器后,spring容器在啟用過程中都會去調用它的void registerBeanDefinitions(AnnotationMetadata var1, BeanDefinitionRegistry var2)方法,可以確定的是FeignClientsRegistrar肯定重寫了此方法,我們接下來看一下該方法的實現。
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
registerDefaultConfiguration(metadata, registry);
registerFeignClients(metadata, registry);
}
可以看到在這個方法中做了兩件事:
- 注冊feign配置。
- 注冊feign接口。
我們這里抓一下重點,看一下feign接口是怎么注冊的?
public void registerFeignClients(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);
Set<String> basePackages;
Map<String, Object> attrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName());
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
FeignClient.class);
final Class<?>[] clients = attrs == null ? null
: (Class<?>[]) attrs.get("clients");
if (clients == null || clients.length == 0) {
// 限定只掃描FeingClient注解
scanner.addIncludeFilter(annotationTypeFilter);
basePackages = getBasePackages(metadata);
}
else {
final Set<String> clientClasses = new HashSet<>();
basePackages = new HashSet<>();
for (Class<?> clazz : clients) {
basePackages.add(ClassUtils.getPackageName(clazz));
clientClasses.add(clazz.getCanonicalName());
}
AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
@Override
protected boolean match(ClassMetadata metadata) {
String cleaned = metadata.getClassName().replaceAll("\\$", ".");
return clientClasses.contains(cleaned);
}
};
scanner.addIncludeFilter(
new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
}
for (String basePackage : basePackages) {
Set<BeanDefinition> candidateComponents = scanner
.findCandidateComponents(basePackage);
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) {
// verify annotated class is an interface
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
Assert.isTrue(annotationMetadata.isInterface(),
"@FeignClient can only be specified on an interface");
Map<String, Object> attributes = annotationMetadata
.getAnnotationAttributes(
FeignClient.class.getCanonicalName());
String name = getClientName(attributes);
registerClientConfiguration(registry, name,
attributes.get("configuration"));
// 這里生成bean並且注冊到容器
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
}
上面這段代碼概括起來就是: 先找了包路徑basePackages , 然后在從這些包路徑中查找帶有FeignClient注解的接口,最后將注解的信息解析出來作為屬性手動構建beanDefine注入到容器中。(這里有一個類ClassPathScanningCandidateComponentProvider,它可以根據filter掃描指定包下面的class對象,十分好用,建議收藏)。包路徑的獲取以及掃描feign相對簡單,這里不做闡述,我們看一下它生成bean的過程,關注上面代碼中的registerFeignClient方法。
private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientFactoryBean.class);
validate(attributes);
definition.addPropertyValue("url", getUrl(attributes));
definition.addPropertyValue("path", getPath(attributes));
String name = getName(attributes);
definition.addPropertyValue("name", name);
String contextId = getContextId(attributes);
definition.addPropertyValue("contextId", contextId);
definition.addPropertyValue("type", className);
definition.addPropertyValue("decode404", attributes.get("decode404"));
definition.addPropertyValue("fallback", attributes.get("fallback"));
definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
// 這里省略部分代碼
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
代碼中通過BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class)生成的BeanDefine(請記住這里設置得FeignClientFactoryBean.type就是feign接口對應得class對象)。那么所有的feign接口最終注冊到容器中的都是FeignClientFactoryBean對應的一個實例(注意實際上注冊到容器中壓根就不是FeignClientFactoryBean對應的實例化對象,具體原因看下文),到此feign接口對應的實例注冊過程已經完成。那么回到一開始的問題為什么我們調用接口的方法最終發起了請求? 是否有代理類的生成呢? 我們接下來看看FeignClientFactoryBean類的特殊之處
2.2 FeignClientFactoryBean 類,feign接口代理生成類
由上文知,每一個feign接口實際上最終都會生成FeignClientFactoryBean ,最終由FeignClientFactoryBean生成具體的bean實例注冊到容器中。
/**
* @author Spencer Gibb
* @author Venil Noronha
* @author Eko Kurniawan Khannedy
* @author Gregor Zurowski
*/
class FeignClientFactoryBean
implements FactoryBean<Object>, InitializingBean, ApplicationContextAware
可以看到該類實現了FactoryBean接口,這意味着當Spring注冊該bean實例到容器中時,實際是調用其getObject方法,那么FeignClientFactoryBean一定是重寫了getObject()方法,接下來我們看一下getObject()干了什么事情:
public Object getObject() throws Exception {
return getTarget();
}
我們繼續追蹤getTarget()方法:
<T> T getTarget() {
FeignContext context = this.applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context);
// 省略部分代碼...
Targeter targeter = get(context, Targeter.class);
return (T) targeter.target(this, builder, context,
new HardCodedTarget<>(this.type, this.name, url));
}
顯然最終的bean是通過target.target()方法生成,我們繼續往下看:
@Override
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
FeignContext context, Target.HardCodedTarget<T> target) {
return feign.target(target);
}
顯然最終的bean是通過feign.target(target)生成。我們繼續往下看:
public <T> T target(Target<T> target) {
return build().newInstance(target);
}
顯然最終得bean是通過build().newInstance(target)生成。我們繼續往下看:
public <T> T newInstance(Target<T> target) {
// 省略部分代碼
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;
}
可以看到Proxy.newProxyInstance這個熟悉得身影了,沒錯他就是基於JDK原生得動態代理生成了FeignClientFactoryBean.type屬性對應得class對應得代理類。從前文我們知道FeignClientFactoryBean.type就是feign接口得class對象。所以最終我們調用feign接口得方法實際上調用得是InvocationHandler方法。
三、小結
總結起來,先通過@enableFeignClient上面的@import掃描feign接口,然后利用BeanDefinitionBuilder創建一個FeignClientFactoryBean,最后在FeignClientFactoryBean的getObject()方法中生成代理類。希望通過本文的閱讀能讓大家閱讀源碼的能力得到提升,也不在對feign有一種黑盒子的感覺。可能篇幅看起來較少,其實feign的注冊過程牽涉到框架層面的知識還是蠻多的,包括springIoc、BeanDefine、動態代理等等,仔細看明白的話收獲應該還是有蠻多的。哈哈,懂得都懂。順手提一句:讀源碼一定要對SPI等等特別熟悉,要不然你會無從下手,沒有方向,抓不到重點。后續會更新文章講feign怎么實現負載均衡等。