Spring Boot Feign 使用與源碼學(xué)習(xí)

Feign 的使用

服務(wù)拆分后拄踪,在一個服務(wù)中會經(jīng)常需要調(diào)用到另外的服務(wù)蝇恶。這種情況,除了使用 Dubbo 等 RPC 框架外惶桐,最簡單的方法是通過 Spring Cloud Feign 來進(jìn)行服務(wù)間的調(diào)用撮弧。
Feign 最終是通過代理使用 http 請求服務(wù)返回編碼后的內(nèi)容。使用 Feign 可以通過簡單的申明去除手動發(fā)起 http 和編解碼等復(fù)雜過程姚糊。

先看看 如何簡單的使用 Feign贿衍。

  • 首先引入 spring-cloud-dependenciesspring-cloud-starter-openfeign
// 這里貼出了 feign 使用必須的包
<dependencyManagement>
<dependencies>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-dependencies</artifactId>
    <version>Greenwich.SR4</version>
    <type>pom</type>
    <scope>import</scope>
  </dependency>
</dependencies>
</dependencyManagement>
<dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-openfeign</artifactId>
      <version>2.2.3.RELEASE</version>
      <scope>compile</scope>
    </dependency>
    // ...
</dependencies>
  • 啟動類上添加注解 @EnableFeignClients
@EnableFeignClients
public class MsApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext context =
            SpringApplication.run(MsApplication.class, args);
    }
}
  • 定義 Client 類
// 如果配置的 url 不為空 ,實際會用 url+value 作為實際請求的地址
// 如果 url 為空救恨,實際會請求 http://{name}+value 作為實際請求地址贸辈。這里 name 一般是注冊中心對應(yīng)的服務(wù)名
@FeignClient(name = "MyClient", url = "http://api.xxxx.cn/")
public interface MyClient {
    // url+value 是遠(yuǎn)程服務(wù)的調(diào)用路徑
    @RequestMapping(method = RequestMethod.GET, value = "/api/path")
    String getInfo(@RequestParam Long id);
}
  • 使用 Client 類調(diào)用遠(yuǎn)程服務(wù),這樣調(diào)用使邏輯看上去是在面向?qū)ο缶幊坛Σ郏挥迷偃ナ謩犹幚?Http 請求擎淤。
// 實際調(diào)用遠(yuǎn)程服務(wù)
private MyClient myClient;
String res = myClient.getInfo(1L);

Feign 源碼解讀

從項目啟動、獲取 Client 實例秸仙、實際方法調(diào)用 3 個過程分析 Feign 相關(guān)源碼嘴拢。

啟動

  • spring-cloud-openfeign-core 包中通過 SPI 機(jī)制,運行 FeignAutoConfiguration 文件寂纪。根據(jù)配置生成 org.springframework.cloud.openfeign.FeignContext席吴、feign.Clientorg.springframework.cloud.openfeign.Targeter 3 個主要的類
@Configuration
@ConditionalOnClass({Feign.class})
@EnableConfigurationProperties({FeignClientProperties.class, FeignHttpClientProperties.class})
public class FeignAutoConfiguration {
    @Bean
    public FeignContext feignContext() {
        FeignContext context = new FeignContext();
        context.setConfigurations(this.configurations);
        return context;
    }

    @Configuration
    // 當(dāng)項目中存在 OkHttpClient.class 類(項目引入了 OkHttpClient 的包)的時候會初始化下面類
    @ConditionalOnClass({OkHttpClient.class})
    @ConditionalOnMissingBean({okhttp3.OkHttpClient.class})
    @ConditionalOnProperty({"feign.okhttp.enabled"})
    protected static class OkHttpFeignConfiguration {
        // OkHttpClient 可以使用連接池,這樣可以減少 tcp 多次連接的開銷抢腐。默認(rèn)的 Client 是沒有的
        @Bean
        @ConditionalOnMissingBean({ConnectionPool.class})
        public ConnectionPool httpClientConnectionPool(FeignHttpClientProperties httpClientProperties, OkHttpClientConnectionPoolFactory connectionPoolFactory) {
            Integer maxTotalConnections = httpClientProperties.getMaxConnections();
            Long timeToLive = httpClientProperties.getTimeToLive();
            TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit();
            return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit);
        }
        // ....

        // 如果沒有自定義 Client 時姑曙,這里會生成 OkHttpClient 作為 feign.Client 進(jìn)行 http 調(diào)用的客戶端
        @Bean
        @ConditionalOnMissingBean({Client.class})
        public Client feignClient(okhttp3.OkHttpClient client) {
            return new OkHttpClient(client);
        }
    }

    @Configuration
    // 同理,這里使用 ApacheHttpClient 的包
    @ConditionalOnClass({ApacheHttpClient.class})
    @ConditionalOnMissingClass({"com.netflix.loadbalancer.ILoadBalancer"})
    @ConditionalOnMissingBean({CloseableHttpClient.class})
    @ConditionalOnProperty(
        value = {"feign.httpclient.enabled"},
        matchIfMissing = true
    )
    protected static class HttpClientFeignConfiguration {
        // 使用連接池時定時的清除過期的連接
        private final Timer connectionManagerTimer = new Timer("FeignApacheHttpClientConfiguration.connectionManagerTimer", true);

        // 連接池的配置
        @Bean
        @ConditionalOnMissingBean({HttpClientConnectionManager.class})
        public HttpClientConnectionManager connectionManager(ApacheHttpClientConnectionManagerFactory connectionManagerFactory, FeignHttpClientProperties httpClientProperties) {
            final HttpClientConnectionManager connectionManager = connectionManagerFactory.newConnectionManager(httpClientProperties.isDisableSslValidation(), httpClientProperties.getMaxConnections(), httpClientProperties.getMaxConnectionsPerRoute(), httpClientProperties.getTimeToLive(), httpClientProperties.getTimeToLiveUnit(), this.registryBuilder);
            // 初始化連接池過期連接的清除任務(wù)
            this.connectionManagerTimer.schedule(new TimerTask() {
                public void run() {
                    connectionManager.closeExpiredConnections();
                }
            }, 30000L, (long)httpClientProperties.getConnectionTimerRepeat());
            return connectionManager;
        }

        // 如果沒有自定義 Client 時迈倍,這里會生成 ApacheHttpClient 作為 feign.Client 進(jìn)行 http 調(diào)用的客戶端
        @Bean
        @ConditionalOnMissingBean({Client.class})
        public Client feignClient(HttpClient httpClient) {
            return new ApacheHttpClient(httpClient);
        }
    }

    @Configuration
    @ConditionalOnMissingClass({"feign.hystrix.HystrixFeign"})
    protected static class DefaultFeignTargeterConfiguration {
        protected DefaultFeignTargeterConfiguration() {
        }

        // 默認(rèn)使用的 Targeter 類
        @Bean
        @ConditionalOnMissingBean
        public Targeter feignTargeter() {
            return new DefaultTargeter();
        }
    }

    @Configuration
    @ConditionalOnClass(
        name = {"feign.hystrix.HystrixFeign"}
    )
    protected static class HystrixFeignTargeterConfiguration {
        protected HystrixFeignTargeterConfiguration() {
        }

        @Bean
        @ConditionalOnMissingBean
        public Targeter feignTargeter() {
            return new HystrixTargeter();
        }
    }
}

  • @EnableFeignClients 注解中通過 @Import 導(dǎo)入 FeignClientsRegistrar.class 類佩研,在類中注冊 FeigntClient 的默認(rèn)配置和掃描并注冊 Client 類到容器中。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({FeignClientsRegistrar.class})
public @interface EnableFeignClients {
    // ...
}

FeignClientsRegistrar 類解讀

//
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
    // 注冊配置和 client 的 beanDefinition
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        // 注冊 Feign 的默認(rèn)配置
        this.registerDefaultConfiguration(metadata, registry);
        // 通過掃描有 `FeignClient.class` 注解的類燥撞,并注入到容器中
        this.registerFeignClients(metadata, registry);
    }

    private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
        String className = annotationMetadata.getClassName();
        // 每個 Client 都是 FeignClientFactoryBean.class 類投剥,獲取類實例的時候是通過調(diào)用 FeignClientFactoryBean 類的 getObject() 方法獲得
        BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);

        BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[]{alias});
        BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
    }
}

獲取 Client 實例

根據(jù)啟動時注入的內(nèi)容可知, 從容器獲得 client 的實例是通過 FeignClientFactoryBean 類的 getObject() 方法獲取迹鹅。
最終得到的是一個 Proxy 代理卦洽,實例化過程見下圖:


Feign實例化過程.png
  • FeignContext、HystrixTargeter 都是啟動的時候生成到容器的 Bean
  • Feign.Builder 是 Feign Client 的構(gòu)建者
  • SynchronousMethodHandler.Factory 是 Client 中定義的方法攔截器的創(chuàng)建工廠斜棚,Client 中每個方法對應(yīng)一個 SynchronousMethodHandler 處理器
  • ReflectiveFeign.ParseHandlersByName 將 Client 中的方法名解析得到不同的 SynchronousMethodHandler
  • ReflectiveFeign 是 Feign 類的唯一實現(xiàn)
  • InvocationHandlerFactory.Default 方法處理創(chuàng)建工廠的默認(rèn)實現(xiàn)阀蒂,生成代理類方法的處理實現(xiàn)
  • ReflectiveFeign.FeignInvocationHandler 代理類方法處理的默認(rèn)類,是代理類攔截后的處理類弟蚀,代理的方法會在它的 invoke() 方法中實現(xiàn)蚤霞,它又是將攔截的方法轉(zhuǎn)發(fā)到不同的 SynchronousMethodHandler 中進(jìn)行處理

Client 方法調(diào)用

從上面獲取 Client 實例的過程可以知道,在調(diào) client 的方法時义钉,實例調(diào)用的是 Proxy 類的方法昧绣,會對應(yīng)的 SynchronousMethodHandler 攔截執(zhí)行實際的邏輯。SynchronousMethodHandler 執(zhí)行的邏輯如下:

public Object invoke(Object[] argv) throws Throwable {
    RequestTemplate template = buildTemplateFromArgs.create(argv);
    Options options = findOptions(argv);
    // 重試器捶闸,默認(rèn)是 Retryer.Default 會重試 5次
    Retryer retryer = this.retryer.clone();
    while (true) {
      try {
        // 由啟動時注入的 httpclient 發(fā)起 http 請求夜畴,并編碼返回的內(nèi)容
        return executeAndDecode(template, options);
      } catch (RetryableException e) {
        try {
          retryer.continueOrPropagate(e);
        } catch (RetryableException th) {
          Throwable cause = th.getCause();
          if (propagationPolicy == UNWRAP && cause != null) {
            throw cause;
          } else {
            throw th;
          }
        }
        if (logLevel != Logger.Level.NONE) {
          logger.logRetry(metadata.configKey(), logLevel);
        }
        continue;
      }
    }
  }
  • client 默認(rèn)是 Client.Default 使用的是 HttpURLConnection 發(fā)起 http 調(diào)用
  • 推薦在實際過程中換成 OkHttpClient 或者 ApacheHttpClient ,它們可以使用到連接池

如有疑問删壮,歡迎留言交流

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末贪绘,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子央碟,更是在濱河造成了極大的恐慌税灌,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件硬耍,死亡現(xiàn)場離奇詭異垄琐,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)经柴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進(jìn)店門狸窘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人坯认,你說我怎么就攤上這事翻擒∶セ粒” “怎么了?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵陋气,是天一觀的道長劳吠。 經(jīng)常有香客問我,道長巩趁,這世上最難降的妖魔是什么痒玩? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮议慰,結(jié)果婚禮上蠢古,老公的妹妹穿的比我還像新娘。我一直安慰自己别凹,他們只是感情好草讶,可當(dāng)我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著炉菲,像睡著了一般堕战。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上拍霜,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天嘱丢,我揣著相機(jī)與錄音,去河邊找鬼沉御。 笑死屿讽,一個胖子當(dāng)著我的面吹牛昭灵,可吹牛的內(nèi)容都是我干的吠裆。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼烂完,長吁一口氣:“原來是場噩夢啊……” “哼试疙!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起抠蚣,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤祝旷,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后嘶窄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體怀跛,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年柄冲,在試婚紗的時候發(fā)現(xiàn)自己被綠了吻谋。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡现横,死狀恐怖漓拾,靈堂內(nèi)的尸體忽然破棺而出阁最,到底是詐尸還是另有隱情,我是刑警寧澤骇两,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布速种,位于F島的核電站,受9級特大地震影響低千,放射性物質(zhì)發(fā)生泄漏配阵。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一示血、第九天 我趴在偏房一處隱蔽的房頂上張望闸餐。 院中可真熱鬧,春花似錦矾芙、人聲如沸舍沙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拂铡。三九已至,卻和暖如春葱绒,著一層夾襖步出監(jiān)牢的瞬間感帅,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工地淀, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留失球,地道東北人。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓帮毁,卻偏偏與公主長得像实苞,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子烈疚,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容