1 前言
我們從上節 【OpenFeign】@FeignClient 注入過程原始碼分析 繼續,來看看它代理物件的建立,以及請求的執行過程。
我們就從它的 FeignClientFactoryBean 看起,那我們這裡簡單回憶下它都設定了哪些屬性,我簡單畫了個圖。
這些屬性不瞭解的話,就先看看上節哈,有詳細的說明,我這裡就不再囉嗦了哈。
2 原始碼分析
2.1 入口 FeignClientFactoryBean
看到 FactoryBean 就先看它的 getObject():
// FeignClient#getObject @Override public Object getObject() { // 呼叫內部 getTarget() return getTarget(); }
繼續進去看 getTarget()方法:
/** * @param <T> the target type of the Feign client * @return a {@link Feign} client created with the specified data and the context * information */ <T> T getTarget() { // 先拿到上下文中的 FeignContext Bean出來 這個哪來的呢? FeignContext context = beanFactory != null ? beanFactory.getBean(FeignContext.class) : applicationContext.getBean(FeignContext.class); // 拿到構建起物件 裡邊會設定編碼器 解碼器 聯結器等等 Feign.Builder builder = feign(context); // 如果沒有設定 url 屬性 if (!StringUtils.hasText(url)) { // 列印下日誌 if (LOG.isInfoEnabled()) { LOG.info("For '" + name + "' URL not provided. Will try picking an instance via load-balancing."); } // 不是 http 開頭的 給你補一下 if (!name.startsWith("http")) { url = "http://" + name; } else { url = name; } url += cleanPath(); // 服務呼叫 return (T) loadBalance(builder, context, new HardCodedTarget<>(type, name, url)); } // 設定了 url 屬性的 一樣不是 http打頭的 就給你補一下 看說明 openfeign 最後基本都是走的 http呼叫是不是 if (StringUtils.hasText(url) && !url.startsWith("http")) { url = "http://" + 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(); } if (client instanceof RetryableFeignBlockingLoadBalancerClient) { // not load balancing because we have a url, // but Spring Cloud LoadBalancer is on the classpath, so unwrap client = ((RetryableFeignBlockingLoadBalancerClient) client) .getDelegate(); } builder.client(client); } // 開始呼叫 Targeter targeter = get(context, Targeter.class); return (T) targeter.target(this, builder, context, new HardCodedTarget<>(type, name, url)); }
可以看到大致分三步:
(1)從上下文獲取到 FeignContext 物件,然後構建屬於當前 FeignClient 的上下文物件
(2)從屬於當前 FeignClient 的上下文物件中,根據當前 FeignClient 的配置建立 Feign 的構建器
(3)根據 URL 的是否設定,來建立相應的代理物件,落點都在 Targeter 物件的 target 方法來建立物件
那我們接下來看看這三步都做了什麼。
2.2 FeignContext 的由來
關於 FeignContext 我之前單獨寫了一節【OpenFeign】【NamedContextFactory】深入剖析 NamedContextFactory 的原理以及使用 ,不知道的可以先看看它是幹什麼呢,有什麼作用,以及怎麼獲取。主要就是針對每個 @FeignClient 做不同的配置,把它們的配置都隔離到了自己的上下文物件裡來達到效果,我們這裡就不再囉嗦了。
我們這裡就主要看下它是怎麼來的,以及它其中的配置的來源。首先,他能從上下文中獲取,那麼思考一下這個物件是什麼跑到上下文的呢?就在 Feign 的自動裝配類裡,我們看一下:
有個配置列表,誰會提供這個列表呢?或者他的來源是哪裡呢?這就需要回想到我們上節看到的 FeignClient 的 BeanDefinition 的注入過程了,我這裡直接在上節的圖裡點綴一下,看紅色的兩根線:
我這裡 debug 看了下,看配置列表會有一個預設的以及我的兩個 @FeignClient 的配置:
好,這就是 FeignContext 的作用,主要是構建屬於當前 FeignClient 的一個上下文物件。
2.3 Builder 構建器
那我們繼續看看構建器物件的建立:
protected Builder feign(FeignContext context) { // 從上下文中獲取日誌 FeignLoggerFactory loggerFactory = (FeignLoggerFactory)this.get(context, FeignLoggerFactory.class); // 建立日誌物件 Logger logger = loggerFactory.create(this.type); // 創造構建器 編碼器 解碼器 聯結器啥的 Builder builder = ((Builder)this.get(context, Builder.class)).logger(logger).encoder((Encoder)this.get(context, Encoder.class)).decoder((Decoder)this.get(context, Decoder.class)).contract((Contract)this.get(context, Contract.class)); // 配置 this.configureFeign(context, builder); return builder; }
在預設什麼都不配置的情況下,什麼日誌工廠、編碼器、解碼器,都是走的預設的配置,就是 FeignContext 建立該 Feign 的上下文物件中,註冊了一些預設的配置,可以看 FeignClientsConfiguration。
2.4 代理物件建立
我們看到根據 URL 屬性設定的有無,會走不同的邏輯,但是最後都是透過 Targeter 物件的 target 方法來建立,而 target 方法又是基於構建器物件裡的 Feign 物件來建立的:
最後可以看到它是透過 JDK 動態代理的方式建立的。
3 小結
鐵子們,我發現這個建立的過程,鏈路有點長啊,裡邊的配置很多,會根據不同的配置構建 feign,但是大體思路上我們基本可以看到首先是根據全域性預設配置@EnableFeignClients和@FeignClient屬性的配置,來構建當前 Feign 的上下文物件,然後從當前上下文中建立它的構造器,繼而建立他的代理物件。後邊的還沒仔細看,不敢斷言,所以就草草帶過了= =,後續慢慢 debug 看完,再詳細闡明,有理解不對的地方歡迎指正哈。