通過上篇我們瞭解OpenFeign他也可以完成遠端通訊,但是它並不是真正義意上的RPC通訊,因為他是通過封裝代理來實現的,下面和以前一樣,知道了怎麼用就來看下他是怎麼實現的。
一、思考Feign要做的事情
有了ribbon的鋪墊現在看OpenFeign應該很清楚的知道,這玩意就是通過註解拿到服務名,然後通過服務名獲取服務列表,進行解析和負載最終拼接出一個URI路徑進行代理請求,那麼他要完成這一系列動作他就要做下面幾件事。
- 引數的解析和裝載
- 針對指定的FeignClient,生成動態代理
- 針對FeignClient中的方法描述進行解析
- 組裝出一個Request物件,發起請求
二、原始碼分析
看過我寫的ribbon的應該清楚,如果想要找到進入原始碼的入口那麼應該要找的是FeignClient,但是FeignClient是在哪裡被解析的呢,在應用篇中我在啟動類中加了個@EnableFeignClients註解,這 個註解的作用其實就是開啟了一個FeignClient的掃描,那麼點選啟動類的@EnableFeignClients註解看下他是怎麼開啟FeignClient的掃描的,進去後發現裡面有個@Import(FeignClientsRegistrar.class)這個FeignClientsRegistrar跟Bean的動態裝載有關
點選進去有個registerBeanDefinitions方法通過名稱可以知道是一個Bean的注入方法
下面我寫一個簡單的例子來描述他是如何實現動態載入的,學FeignClientsRegistrar類 implements ImportBeanDefinitionRegistrar介面並實現registerBeanDefinitions方法
這一步搞完後,定義一個註解,把@EnableFeignClients註解上的註解都抄過來並把@Import註解裡面的類改成我們自己定義的類
然後在啟動類上用上自定義的註解,那麼在啟動類時就可以進行一個Bean的動態裝載了
通過這個概念已經很清楚原始碼中FeignClientsRegistrar類的FeignClientsRegistrar是怎麼完成Bean的動態載入了
- registerDefaultConfifiguration 方法內部從 SpringBoot 啟動類上檢查是否有@EnableFeignClients, 有該註解的話, 則完成 Feign 框架相關的一些配置內容註冊
- registerFeignClients 方法內部從 classpath 中, 掃描獲得 @FeignClient 修飾的類, 將類的內容解析為 BeanDefifinition , 最終通過呼叫 Spring 框架中的BeanDefifinitionReaderUtils.resgisterBeanDefifinition 將解析處理過的 FeignClientBeanDeififinition 新增到 spring 容器中.
@Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
//註冊預設配置資訊 registerDefaultConfiguration(metadata, registry);
//註冊FeignClients(可能有多個) registerFeignClients(metadata, registry); }
進入registerFeignClients(metadata, registry);這玩意是幹啥的呢,在啟動類中的@EnableFeignClients是可以定義多個basePackers的如果定義了多個那就要掃描FeignClients,下面就是掃描處理過程,看過spring原始碼的人就知道前面是什麼註解解析
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) { 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))); } //bssePackages是解析不同basePackage路徑下的FeignClient的宣告 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);
//如果每個包路徑下都有一個或多個FeignClient的話就要載入 一次動態configuration配置所以這就是為什麼前面已經載入過了這裡還要載入一次的原因 registerClientConfiguration(registry, name, attributes.get("configuration")); //註冊Feignclient registerFeignClient(registry, annotationMetadata, attributes); } } } }
點選registerFeignClient(registry, annotationMetadata, attributes);看下做了啥事,這裡面的邏輯其實就幹了一件事,就是通過BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);注入一個Bean;這個注入的過程中有個比較重要的程式碼BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);這是一個構造者,構造一個BeanDefinition,裡面把FeignClientFactoryBean.class給傳了進去
下面進入.genericBeanDefinition(FeignClientFactoryBean.class);看把 FeignClientFactoryBean.class類傳進去幹嘛,發現註冊的Bean就是引數中自己傳進來的beanClass,這個傳進去的beanClass是工廠Bean
Spring Cloud FengnClient實際上是利用Spring的代理工廠來生成代理類,所以在這裡地方才會把所有的FeignClient的BeanDefifinition設定為FeignClientFactoryBean型別,而FeignClientFactoryBean繼承自FactoryBean,它是一個工廠Bean。在Spring中,FactoryBean是一個工廠Bean,用來建立代理Bean。工廠 Bean 是一種特殊的 Bean, 對於 Bean 的消費者來說, 他邏輯上是感知不到這個 Bean 是普通的 Bean 還是工廠 Bean, 只是按照正常的獲取 Bean 方式去呼叫, 但工廠bean 最後返回的例項不是工廠Bean 本身, 而是執行工廠 Bean 的 getObject 邏輯返回的示例。
點選這個工廠Bean的FeignClientFactoryBean類中發現裡面有個getObject()方法,這個工廠Bean就是通過這個getTarget();返回一個真正的例項
畫下時序圖
前面說到了在啟動時會通過@EnableFeignClients去掃描所有指定路徑下的@FeignClient註解宣告的一個介面,然後在掃描到以後要去生成一個動態代理的類,這個動態代理的生成就是在呼叫getObject()時完成 ,而且getObject()又會呼叫他方法裡面的getTarget()去完成這件事,
<T> T getTarget() {
//FeignContext註冊到容器是在FeignAutoConfiguration上完成的
//在初始化FeignContext時,會把configurations在容器中放入FeignContext中。configurations的
//來源就是在前面registerFeignClients方法中將@FeignClient的配置configuration。
FeignContext context = this.applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context);//構建Builder物件
//如果url為空,則走負載均衡,生成有負載均衡功能的代理類
if (!StringUtils.hasText(this.url)) {
if (!this.name.startsWith("http")) {
this.url = "http://" + this.name;
}
else {
this.url = this.name;
}
this.url += cleanPath();
return (T) loadBalance(builder, context,
new HardCodedTarget<>(this.type, this.name, this.url));
}
//如果指定了url,則生成預設的代理類
if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
this.url = "http://" + this.url;
}
String url = this.url + cleanPath();
Client client = getOptional(context, Client.class);
if (client != null) {
if (client instanceof LoadBalancerFeignClient) {
// not load balancing because we have a url,
// but ribbon is on the classpath, so unwrap
client = ((LoadBalancerFeignClient) client).getDelegate();
}
if (client instanceof FeignBlockingLoadBalancerClient) {
// not load balancing because we have a url,
// but Spring Cloud LoadBalancer is on the classpath, so unwrap
client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
}
builder.client(client);
}//生成預設代理類
Targeter targeter = get(context, Targeter.class);
return (T) targeter.target(this, builder, context,
new HardCodedTarget<>(this.type, this.name, url));
}
上面有段程式碼Feign.Builder builder = feign(context);是構建Builder物件
protected Feign.Builder feign(FeignContext context) { FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class); Logger logger = loggerFactory.create(this.type); // @formatter:off Feign.Builder builder = get(context, Feign.Builder.class) // required values
//開啟日誌級別 .logger(logger)
//編碼 .encoder(get(context, Encoder.class))
//解碼 .decoder(get(context, Decoder.class))
//連線。這個連線用在解析模板的,後面會提 .contract(get(context, Contract.class)); // @formatter:on configureFeign(context, builder); return builder; }
上面的builder構造完後繼續向下走,配置完Feign.Builder之後,再判斷是否需要LoadBalance,如果需要,則通過LoadBalance的方法來設定。實際上他們最終呼叫的是Target.target()方法。
@Import({ HttpClientFeignLoadBalancedConfiguration.class,
OkHttpFeignLoadBalancedConfiguration.class,
DefaultFeignLoadBalancedConfiguration.class })
protected <T> T loadBalance(Feign.Builder builder, FeignContext context, HardCodedTarget<T> target) {
//針對某一個服務的client Client client = getOptional(context, Client.class); if (client != null) {
//將client設定進去相當於增加了客戶端負載均衡解析的機制 builder.client(client); Targeter targeter = get(context, Targeter.class); return targeter.target(this, builder, context, target); } throw new IllegalStateException( "No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?"); }
點選上圖的targeter.target(this, builder, context, target);因為熔斷準備在後面講,所以在選tartget的實現時選擇DefaultTarget.target
點選feign.target()往下走,走到這裡其實就已經到了核心邏輯了,前面不一直說動態代理嗎,前面走的都是人生最長的套路,前面自己寫的控制層程式碼通過@Resource註解注入的UserOpenFeign他最終會呼叫下面的方法返回一個例項,那麼下面看下這newInstance()方法做了啥,發現這玩意有兩個實現,至於選擇哪個就要看build()返回的是什麼了,向下看發現build()返回的是ReflectiveFeign,所以選第二個
public <T> T newInstance(Target<T> target) { //根據介面類和Contract協議解析方式,解析介面類上的方法和註解,轉換成內部的MethodHandler處理方式 Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target); Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>(); List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>(); 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))); } } // 基於Proxy.newProxyInstance 為介面類建立動態實現,將所有的請求轉換給InvocationHandler 處理。 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; }
下面通過debugger驗證下,會看到userOpenFeign的返回的是代理類,通過下圖可以知道當呼叫userOpenFeign時他其實是呼叫ReflectiveFeign中的handler,而通過Debugger發現這個handler是FeginInvocationHandler,
竟然是走了代理那麼他一定是走了ReflectiveFeign的代理方法invoke()方法