【OpenFeign】@FeignClient 代理物件的建立原始碼分析

酷酷-發表於2024-03-23

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 看完,再詳細闡明,有理解不對的地方歡迎指正哈。

相關文章